Navigation::getUrlForBlock()   D
last analyzed

Complexity

Conditions 23
Paths 16

Size

Total Lines 80
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 114.7608

Importance

Changes 0
Metric Value
cc 23
eloc 32
c 0
b 0
f 0
nc 16
nop 4
dl 0
loc 80
ccs 23
cts 52
cp 0.4423
crap 114.7608
rs 4.1666

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Frontend\Core\Engine;
4
5
use ForkCMS\App\KernelLoader;
6
use Frontend\Core\Language\Language;
7
use Symfony\Component\HttpKernel\KernelInterface;
8
use Backend\Modules\Pages\Engine\Model as BackendPagesModel;
9
use Frontend\Core\Engine\Model as FrontendModel;
10
use Frontend\Modules\Profiles\Engine\Authentication as FrontendAuthentication;
11
12
/**
13
 * This class will be used to build the navigation
14
 */
15
class Navigation extends KernelLoader
16
{
17
    /**
18
     * The excluded page ids. These will not be shown in the menu.
19
     *
20
     * @var array
21
     */
22
    private static $excludedPageIds = [];
23
24
    /**
25
     * The selected pageIds
26
     *
27
     * @var array
28
     */
29
    private static $selectedPageIds = [];
30
31
    /**
32
     * TwigTemplate instance
33
     *
34
     * @var TwigTemplate
35
     */
36
    protected $template;
37
38
    /**
39
     * URL instance
40
     *
41
     * @var Url
42
     */
43
    protected $url;
44
45 26
    public function __construct(KernelInterface $kernel)
46
    {
47 26
        parent::__construct($kernel);
48
49 26
        $this->template = $this->getContainer()->get('templating');
50 26
        $this->url = $this->getContainer()->get('url');
51
52
        // set selected ids
53 26
        $this->setSelectedPageIds();
54 26
    }
55
56
    /**
57
     * Creates a Backend URL for a given action and module
58
     * If you don't specify a language the current language will be used.
59
     *
60
     * @param string $action The action to build the URL for.
61
     * @param string $module The module to build the URL for.
62
     * @param string $language The language to use, if not provided we will use the working language.
63
     * @param array $parameters GET-parameters to use.
64
     * @param bool $urlencode Should the parameters be urlencoded?
65
     *
66
     * @return string
67
     */
68
    public static function getBackendUrlForBlock(
69
        string $action,
70
        string $module,
71
        string $language = null,
72
        array $parameters = null,
73
        bool $urlencode = true
74
    ): string {
75
        $language = $language ?? LANGUAGE;
76
77
        // add at least one parameter
78
        if (empty($parameters)) {
79
            $parameters['token'] = 'true';
80
        }
81
82
        if ($urlencode) {
83
            $parameters = array_map('rawurlencode', $parameters);
0 ignored issues
show
Bug introduced by
It seems like $parameters can also be of type null; however, parameter $array of array_map() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

83
            $parameters = array_map('rawurlencode', /** @scrutinizer ignore-type */ $parameters);
Loading history...
84
        }
85
86
        $queryString = '?' . http_build_query($parameters);
0 ignored issues
show
Bug introduced by
It seems like $parameters can also be of type null; however, parameter $data of http_build_query() does only seem to accept array|object, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

86
        $queryString = '?' . http_build_query(/** @scrutinizer ignore-type */ $parameters);
Loading history...
87
88
        // build the URL and return it
89
        return FrontendModel::get('router')->generate(
90
            'backend',
91
            ['_locale' => $language, 'module' => $module, 'action' => $action]
92
        ) . $queryString;
93
    }
94
95
    /**
96
     * Get the first child for a given parent
97
     *
98
     * @param int $pageId The pageID wherefore we should retrieve the first child.
99
     *
100
     * @return int
101
     */
102
    public static function getFirstChildId(int $pageId): int
103
    {
104
        // init var
105
        $navigation = self::getNavigation();
106
107
        // loop depths
108
        foreach ($navigation as $parent) {
109
            // no available, skip this element
110
            if (!isset($parent[$pageId])) {
111
                continue;
112
            }
113
114
            // get keys
115
            $keys = array_keys($parent[$pageId]);
116
117
            // get first item
118
            if (isset($keys[0])) {
119
                return $keys[0];
120
            }
121
        }
122
123
        // fallback
124
        return false;
125
    }
126
127
    /**
128
     * Get all footer links
129
     *
130
     * @return array
131
     */
132 26
    public static function getFooterLinks(): array
133
    {
134
        // get the navigation
135 26
        $navigation = self::getNavigation();
136 26
        $footerLinks = [];
137
138
        // validate
139 26
        if (!isset($navigation['footer'][0])) {
140
            return $footerLinks;
141
        }
142
143 26
        foreach ($navigation['footer'][0] as $id => $data) {
144
            // skip hidden pages
145 26
            if ($data['hidden']) {
146
                continue;
147
            }
148
149
            // if data
150 26
            if (isset($data['data'])) {
151
                $data['data'] = unserialize($data['data']);
152
            }
153
154
            // add
155 26
            $footerLinks[] = [
156 26
                'id' => $id,
157 26
                'url' => self::getUrl($id),
158 26
                'title' => $data['title'],
159 26
                'navigation_title' => $data['navigation_title'],
160 26
                'selected' => in_array($id, self::$selectedPageIds, true),
161 26
                'link_class' => (isset($data['data']['link_class'])) ? $data['data']['link_class'] : null,
162
            ];
163
        }
164
165 26
        return $footerLinks;
166
    }
167
168
    /**
169
     * Get the page-keys
170
     *
171
     * @param string $language The language wherefore the navigation should be loaded,
172
     *                         if not provided we will load the language that was provided in the URL.
173
     *
174
     * @return array
175
     */
176 27
    public static function getKeys(string $language = null): array
177
    {
178 27
        return BackendPagesModel::getCacheBuilder()->getKeys($language ?? LANGUAGE);
179
    }
180
181
    /**
182
     * Get the navigation-items
183
     *
184
     * @param string $language The language wherefore the keys should be loaded,
185
     *                         if not provided we will load the language that was provided in the URL.
186
     *
187
     * @return array
188
     */
189 27
    public static function getNavigation(string $language = null): array
190
    {
191 27
        return BackendPagesModel::getCacheBuilder()->getNavigation($language ?? LANGUAGE);
192
    }
193
194
    /**
195
     * Check if we have meta navigation and that it is enabled
196
     *
197
     * @param array $navigation
198
     *
199
     * @return bool
200
     */
201 9
    private static function hasMetaNavigation(array $navigation): bool
202
    {
203 9
        return isset($navigation['meta']) && Model::get('fork.settings')->get('Pages', 'meta_navigation', true);
204
    }
205
206
    /**
207
     * Get navigation HTML
208
     *
209
     * @param string $type The type of navigation the HTML should be build for.
210
     * @param int $parentId The parentID to start of.
211
     * @param int $depth The maximum depth to parse.
212
     * @param array $excludeIds PageIDs to be excluded.
213
     * @param string $template The template that will be used.
214
     * @param int $depthCounter A counter that will hold the current depth.
215
     *
216
     * @throws Exception
217
     *
218
     * @return string
219
     */
220 26
    public static function getNavigationHTML(
221
        string $type = 'page',
222
        int $parentId = 0,
223
        int $depth = null,
224
        array $excludeIds = [],
225
        string $template = 'Core/Layout/Templates/Navigation.html.twig',
226
        int $depthCounter = 1
227
    ): string {
228
        // get navigation
229 26
        $navigation = self::getNavigation();
230
231
        // merge the exclude ids with the previously set exclude ids
232 26
        $excludeIds = array_merge($excludeIds, self::$excludedPageIds);
233
234
        // meta-navigation is requested but meta isn't enabled
235 26
        if ($type === 'meta' && !self::hasMetaNavigation($navigation)) {
236 9
            return '';
237
        }
238
239
        // validate
240 26
        if (!isset($navigation[$type])) {
241
            throw new Exception(
242
                'This type (' . $type . ') isn\'t a valid navigation type. Possible values are: page, footer, meta.'
243
            );
244
        }
245
246 26
        if (!isset($navigation[$type][$parentId])) {
247
            throw new Exception('The parent (' . $parentId . ') doesn\'t exists.');
248
        }
249
250
        // special construction to merge home with its immediate children
251 26
        $mergedHome = false;
252 26
        while (true) {
253
            // loop elements
254 26
            foreach ($navigation[$type][$parentId] as $id => $page) {
255
                // home is a special item, it should live on the same depth
256 26
                if (!$mergedHome && (int) $page['page_id'] === FrontendModel::HOME_PAGE_ID) {
257
                    // extra checks otherwise exceptions will wbe triggered.
258 26
                    if (!isset($navigation[$type][$parentId])
259 26
                        || !is_array($navigation[$type][$parentId])) {
260
                        $navigation[$type][$parentId] = [];
261
                    }
262 26
                    if (!isset($navigation[$type][$page['page_id']])
263 26
                        || !is_array($navigation[$type][$page['page_id']])
264
                    ) {
265
                        $navigation[$type][$page['page_id']] = [];
266
                    }
267
268
                    // add children
269 26
                    $navigation[$type][$parentId] = array_merge(
270 26
                        $navigation[$type][$parentId],
271 26
                        $navigation[$type][$page['page_id']]
272
                    );
273
274
                    // mark as merged
275 26
                    $mergedHome = true;
276
277
                    // restart loop
278 26
                    continue 2;
279
                }
280
281
                // not hidden and not an action
282 26
                if ($page['hidden'] || $page['tree_type'] === 'direct_action') {
283
                    unset($navigation[$type][$parentId][$id]);
284
                    continue;
285
                }
286
287
                // authentication
288 26
                if (isset($page['data'])) {
289
                    // unserialize data
290
291
                    $page['data'] = unserialize($page['data'], ['allowed_classes' => false]);
292
                    // add link class if needed
293
                    if (isset($page['data']['link_class'])) {
294
                        $navigation[$type][$parentId][$id]['link_class'] = $page['data']['link_class'];
295
                    }
296
297
                    // if auth_required isset and is true
298
                    if (isset($page['data']['auth_required']) && $page['data']['auth_required']) {
299
                        // is profile logged? unset
300
                        if (!FrontendAuthentication::isLoggedIn()) {
301
                            unset($navigation[$type][$parentId][$id]);
302
                            continue;
303
                        }
304
                        // check if group auth is set
305
                        if (!empty($page['data']['auth_groups'])) {
306
                            $inGroup = false;
307
                            // loop group and set value true if one is found
308
                            foreach ($page['data']['auth_groups'] as $group) {
309
                                if (FrontendAuthentication::getProfile()->isInGroup($group)) {
310
                                    $inGroup = true;
311
                                }
312
                            }
313
                            // unset page if not in any of the groups
314
                            if (!$inGroup) {
315
                                unset($navigation[$type][$parentId][$id]);
316
                            }
317
                        }
318
                    }
319
                }
320
321
                // some ids should be excluded
322 26
                if (in_array($page['page_id'], $excludeIds)) {
323
                    unset($navigation[$type][$parentId][$id]);
324
                    continue;
325
                }
326
327
                // if the item is in the selected page it should get an selected class
328 26
                $navigation[$type][$parentId][$id]['selected'] = in_array(
329 26
                    $page['page_id'],
330 26
                    self::$selectedPageIds
331
                );
332
333
                // add nofollow attribute if needed
334 26
                $navigation[$type][$parentId][$id]['nofollow'] = $page['no_follow'];
335
336
                // meta and footer subpages have the "page" type
337 26
                $subType = ($type === 'meta' || $type === 'footer') ? 'page' : $type;
338
339
                // fetch children if needed
340 26
                if (($depthCounter + 1 <= $depth || $depth === null)
341 26
                    && (int) $page['page_id'] !== FrontendModel::HOME_PAGE_ID
342 26
                    && isset($navigation[$subType][$page['page_id']])
343
                ) {
344
                    $navigation[$type][$parentId][$id]['children'] = self::getNavigationHTML(
345
                        $subType,
346
                        $page['page_id'],
347
                        $depth,
348
                        (array) $excludeIds,
349
                        $template,
350
                        $depthCounter + 1
351
                    );
352
                } else {
353 26
                    $navigation[$type][$parentId][$id]['children'] = false;
354
                }
355
356 26
                $navigation[$type][$parentId][$id]['parent_id'] = $parentId;
357 26
                $navigation[$type][$parentId][$id]['depth'] = $depthCounter;
358 26
                $navigation[$type][$parentId][$id]['link'] = static::getUrl($page['page_id']);
359
360
                // is this an internal redirect?
361 26
                if (isset($page['redirect_page_id']) && $page['redirect_page_id'] !== '') {
362
                    $navigation[$type][$parentId][$id]['link'] = static::getUrl(
363
                        (int) $page['redirect_page_id']
364
                    );
365
                }
366
367
                // is this an external redirect?
368 26
                if (isset($page['redirect_url']) && $page['redirect_url'] !== '') {
369 26
                    $navigation[$type][$parentId][$id]['link'] = $page['redirect_url'];
370
                }
371
            }
372
373
            // break the loop (it is only used for the special construction with home)
374 26
            break;
375
        }
376
377
        // return parsed content
378 26
        return Model::get('templating')->render(
379 26
            $template,
380 26
            ['navigation' => $navigation[$type][$parentId]]
381
        );
382
    }
383
384
    /**
385
     * Get a menuId for an specified URL
386
     *
387
     * @param string $url The URL wherefore you want a pageID.
388
     * @param string $language The language wherefore the pageID should be retrieved,
389
     *                          if not provided we will load the language that was provided in the URL.
390
     *
391
     * @return int
392
     */
393 26
    public static function getPageId(string $url, string $language = null): int
394
    {
395
        // redefine
396 26
        $url = trim($url, '/');
397
398
        // get menu items array
399 26
        $keys = self::getKeys($language ?? LANGUAGE);
400
401
        // get key
402 26
        $key = array_search($url, $keys, true);
403
404
        // return 404 if we don't known a valid Id
405 26
        if ($key === false) {
406
            return FrontendModel::ERROR_PAGE_ID;
407
        }
408
409
        // return the real Id
410 26
        return (int) $key;
411
    }
412
413
    /**
414
     * Get more info about a page
415
     *
416
     * @param int $requestedPageId The pageID wherefore you want more information.
417
     *
418
     * @return array|bool
419
     */
420 26
    public static function getPageInfo(int $requestedPageId)
421
    {
422
        // get navigation
423 26
        $navigation = self::getNavigation();
424
425
        // loop levels
426 26
        foreach ($navigation as $level) {
427
            // loop parents
428 26
            foreach ($level as $parentId => $children) {
429
                // loop children
430 26
                foreach ($children as $pageId => $page) {
431
                    // return if this is the requested page
432 26
                    if ($requestedPageId === (int) $pageId) {
433
                        // set return
434 26
                        $pageInfo = $page;
435 26
                        $pageInfo['page_id'] = $pageId;
436 26
                        $pageInfo['parent_id'] = $parentId;
437
438
                        // return
439 26
                        return $pageInfo;
440
                    }
441
                }
442
            }
443
        }
444
445
        // fallback
446
        return false;
447
    }
448
449
    /**
450
     * Get URL for a given pageId
451
     *
452
     * @param int $pageId The pageID wherefore you want the URL.
453
     * @param string $language The language wherein the URL should be retrieved,
454
     *                         if not provided we will load the language that was provided in the URL.
455
     *
456
     * @return string
457
     */
458 27
    public static function getUrl(int $pageId, string $language = null): string
459
    {
460 27
        $language = $language ?? LANGUAGE;
461
462
        // init URL
463 27
        $url = FrontendModel::getContainer()->getParameter('site.multilanguage') ? '/' . $language . '/' : '/';
464
465
        // get the menuItems
466 27
        $keys = self::getKeys($language);
467
468
        // get the URL, if it doesn't exist return 404
469 27
        if ($pageId !== FrontendModel::ERROR_PAGE_ID && !isset($keys[$pageId])) {
470
            return self::getUrl(FrontendModel::ERROR_PAGE_ID, $language);
471
        }
472
473 27
        if (empty($keys)) {
474
            return urldecode($url . FrontendModel::ERROR_PAGE_ID);
475
        }
476
477
        // return the URL
478 27
        return urldecode($url . $keys[$pageId]);
479
    }
480
481
    /**
482
     * Get the URL for a give module & action combination
483
     *
484
     * @param string $module The module wherefore the URL should be build.
485
     * @param string $action The specific action wherefore the URL should be build.
486
     * @param string $language The language wherein the URL should be retrieved,
487
     *                         if not provided we will load the language that was provided in the URL.
488
     * @param array $data An array with keys and values that partially or fully match the data of the block.
489
     *                    If it matches multiple versions of that block it will just return the first match.
490
     *
491
     * @return string
492
     */
493 27
    public static function getUrlForBlock(
494
        string $module,
495
        string $action = null,
496
        string $language = null,
497
        array $data = null
498
    ): string {
499 27
        $language = $language ?? LANGUAGE;
500
        // init var
501 27
        $pageIdForUrl = null;
502
503
        // get the menuItems
504 27
        $navigation = self::getNavigation($language);
505
506 27
        $dataMatch = false;
507
        // loop types
508 27
        foreach ($navigation as $level) {
509
            // loop level
510 27
            foreach ($level as $pages) {
511
                // loop pages
512 27
                foreach ($pages as $pageId => $properties) {
513
                    // only process pages with extra_blocks that are visible
514 27
                    if (!isset($properties['extra_blocks']) || $properties['hidden']) {
515
                        continue;
516
                    }
517
518
                    // loop extras
519 27
                    foreach ($properties['extra_blocks'] as $extra) {
520
                        // direct link?
521 27
                        if ($extra['module'] === $module && $extra['action'] === $action && $extra['action'] !== null) {
522
                            // if there is data check if all the requested data matches the extra data
523
                            if ($data !== null && isset($extra['data'])
524
                                && array_intersect_assoc($data, (array) $extra['data']) !== $data) {
525
                                // It is the correct action but has the wrong data
526
                                continue;
527
                            }
528
                            // exact page was found, so return
529
                            return self::getUrl($properties['page_id'], $language);
530
                        }
531
532 27
                        if ($extra['module'] === $module && $extra['action'] === null) {
533
                            // if there is data check if all the requested data matches the extra data
534 27
                            if ($data !== null && isset($extra['data'])) {
535
                                if (array_intersect_assoc($data, (array) $extra['data']) !== $data) {
536
                                    // It is the correct module but has the wrong data
537
                                    continue;
538
                                }
539
540
                                $pageIdForUrl = (int) $pageId;
541
                                $dataMatch = true;
542
                            }
543
544 27
                            if ($data === null && $extra['data'] === null) {
545 27
                                $pageIdForUrl = (int) $pageId;
546 27
                                $dataMatch = true;
547
                            }
548
549 27
                            if (!$dataMatch) {
550 27
                                $pageIdForUrl = (int) $pageId;
551
                            }
552
                        }
553
                    }
554
                }
555
            }
556
        }
557
558
        // pageId still null?
559 27
        if ($pageIdForUrl === null) {
560
            return self::getUrl(FrontendModel::ERROR_PAGE_ID, $language);
561
        }
562
563
        // build URL
564 27
        $url = self::getUrl($pageIdForUrl, $language);
565
566
        // append action
567 27
        if ($action !== null) {
568 16
            $url .= '/' . Language::act(\SpoonFilter::toCamelCase($action));
569
        }
570
571
        // return the URL
572 27
        return $url;
573
    }
574
575
    /**
576
     * Fetch the first direct link to an extra id
577
     *
578
     * @param int $id The id of the extra.
579
     * @param string $language The language wherein the URL should be retrieved,
580
     *                         if not provided we will load the language that was provided in the URL.
581
     *
582
     * @return string
583
     */
584
    public static function getUrlForExtraId(int $id, string $language = null): string
585
    {
586
        $language = $language ?? LANGUAGE;
587
        // get the menuItems
588
        $navigation = self::getNavigation($language);
589
590
        // loop types
591
        foreach ($navigation as $level) {
592
            // loop level
593
            foreach ($level as $pages) {
594
                // loop pages
595
                foreach ($pages as $properties) {
596
                    // no extra_blocks available, so skip this item
597
                    if (!isset($properties['extra_blocks'])) {
598
                        continue;
599
                    }
600
601
                    // loop extras
602
                    foreach ($properties['extra_blocks'] as $extra) {
603
                        // direct link?
604
                        if ((int) $extra['id'] === $id) {
605
                            // exact page was found, so return
606
                            return self::getUrl($properties['page_id'], $language);
607
                        }
608
                    }
609
                }
610
            }
611
        }
612
613
        // fallback
614
        return self::getUrl(FrontendModel::ERROR_PAGE_ID, $language);
615
    }
616
617
    /**
618
     * This function lets you add ignored pages
619
     *
620
     * @param mixed $pageIds This can be a single page id or this can be an array with page ids.
621
     */
622
    public static function setExcludedPageIds($pageIds): void
623
    {
624
        $pageIds = (array) $pageIds;
625
626
        // go trough the page ids to add them to the excluded page ids for later usage
627
        foreach ($pageIds as $pageId) {
628
            self::$excludedPageIds[] = $pageId;
629
        }
630
    }
631
632 26
    public function setSelectedPageIds(): void
633
    {
634
        // get pages
635 26
        $pages = (array) $this->url->getPages();
636
637
        // no pages, means we're at the homepage
638 26
        if (empty($pages)) {
639
            self::$selectedPageIds[] = FrontendModel::HOME_PAGE_ID;
640
641
            return;
642
        }
643
644
        // loop pages
645 26
        while (!empty($pages)) {
646
            // get page id
647 26
            $pageId = self::getPageId((string) implode('/', $pages));
648
649
            // add pageId into selected items
650 26
            if ($pageId !== false) {
651 26
                self::$selectedPageIds[] = $pageId;
652
            }
653
654
            // remove last element
655 26
            array_pop($pages);
656
        }
657 26
    }
658
}
659