Page   F
last analyzed

Complexity

Total Complexity 77

Size/Duplication

Total Lines 536
Duplicated Lines 0 %

Test Coverage

Coverage 48%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 215
c 3
b 0
f 0
dl 0
loc 536
ccs 156
cts 325
cp 0.48
rs 2.24
wmc 77

24 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
A handlePage() 0 29 4
A getPageRecord() 0 10 2
A getStatusCode() 0 3 1
A getExtras() 0 3 1
A getRecord() 0 3 1
A allPositionsAreEmpty() 0 17 6
B checkAuthentication() 0 28 8
A getId() 0 3 1
A getPageContent() 0 19 4
A redirectToLogin() 0 5 1
A parseLanguages() 0 19 3
A parsePositions() 0 20 3
A addAlternateLinks() 0 8 2
A parseBlock() 0 20 3
A processExtra() 0 9 3
A display() 0 41 5
A parsePrivacyConsents() 0 7 1
A processPage() 0 38 4
A redirect() 0 3 1
A getExtraForBlock() 0 21 3
B assignPageMeta() 0 39 9
A load() 0 17 4
A addAlternateLinkForLanguage() 0 31 6

How to fix   Complexity   

Complex Class

Complex classes like Page often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Page, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Frontend\Core\Engine;
4
5
use Common\Exception\RedirectException;
6
use ForkCMS\App\KernelLoader;
7
use ForkCMS\Privacy\ConsentDialog;
8
use Frontend\Core\Engine\Block\ModuleExtraInterface;
9
use Frontend\Core\Header\Header;
10
use Frontend\Core\Language\Language;
11
use Symfony\Component\HttpFoundation\RedirectResponse;
12
use Symfony\Component\HttpFoundation\Response;
13
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
14
use Symfony\Component\HttpKernel\KernelInterface;
15
use Frontend\Core\Engine\Block\ExtraInterface as FrontendBlockExtra;
16
use Frontend\Core\Engine\Block\Widget as FrontendBlockWidget;
17
use Backend\Core\Engine\Model as BackendModel;
18
use Frontend\Modules\Profiles\Engine\Authentication as FrontendAuthenticationModel;
19
use Symfony\Component\Security\Core\Exception\InsufficientAuthenticationException;
20
21
/**
22
 * Frontend page class, this class will handle everything on a page
23
 */
24
class Page extends KernelLoader
25
{
26
    /**
27
     * Breadcrumb instance
28
     *
29
     * @var Breadcrumb
30
     */
31
    protected $breadcrumb;
32
33
    /**
34
     * Array of extras linked to this page
35
     *
36
     * @var array
37
     */
38
    protected $extras = [];
39
40
    /**
41
     * Footer instance
42
     *
43
     * @var Footer
44
     */
45
    protected $footer;
46
47
    /**
48
     * Header instance
49
     *
50
     * @var Header
51
     */
52
    protected $header;
53
54
    /**
55
     * The current pageId
56
     *
57
     * @var int
58
     */
59
    protected $pageId;
60
61
    /**
62
     * Content of the page
63
     *
64
     * @var array
65
     */
66
    protected $record = [];
67
68
    /**
69
     * The path of the template to show
70
     *
71
     * @var string
72
     */
73
    protected $templatePath;
74
75
    /**
76
     * The statuscode
77
     *
78
     * @var int
79
     */
80
    protected $statusCode = Response::HTTP_OK;
81
82
    /**
83
     * TwigTemplate instance
84
     *
85
     * @var TwigTemplate
86
     */
87
    protected $template;
88
89
    /**
90
     * URL instance
91
     *
92
     * @var Url
93
     */
94
    protected $url;
95
96 26
    public function __construct(KernelInterface $kernel)
97
    {
98 26
        parent::__construct($kernel);
99
100 26
        $this->getContainer()->set('page', $this);
101 26
        $this->template = $this->getContainer()->get('templating');
102 26
        $this->url = $this->getContainer()->get('url');
103 26
    }
104
105
    /**
106
     * Loads the actual components on the page
107
     */
108 26
    public function load(): void
109
    {
110
        // @deprecated remove this in Fork 6, the privacy consent dialog should be used
111 26
        if (!$this->getContainer()->get('fork.settings')->get('Core', 'show_consent_dialog', false)) {
112
            // set tracking cookie
113 26
            Model::getVisitorId();
0 ignored issues
show
Deprecated Code introduced by
The function Frontend\Core\Engine\Model::getVisitorId() has been deprecated: remove this in Fork 6, this should not be part of Fork. It should be implemented by the developer, and respect a visitors privacy preferences. ( Ignorable by Annotation )

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

113
            /** @scrutinizer ignore-deprecated */ Model::getVisitorId();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
114
        }
115
116
        // create header instance
117 26
        $this->header = new Header($this->getKernel());
118
119
        try {
120 26
            $this->handlePage(Navigation::getPageId(implode('/', $this->url->getPages())));
121
        } catch (NotFoundHttpException $notFoundHttpException) {
122
            $this->handlePage(Response::HTTP_NOT_FOUND);
123
        } catch (InsufficientAuthenticationException $insufficientAuthenticationException) {
124
            $this->redirectToLogin();
125
        }
126 26
    }
127
128 26
    private function handlePage(int $pageId)
129
    {
130
        // get page content from pageId of the requested URL
131 26
        $this->record = $this->getPageContent($pageId);
132
133 26
        if (empty($this->record)) {
134
            throw new NotFoundHttpException('No page was found for the page id:' . $pageId);
135
        }
136
137 26
        $this->checkAuthentication();
138
139
        // we need to set the correct id
140 26
        $this->pageId = (int) $this->record['id'];
141
142 26
        if ($this->pageId === Response::HTTP_NOT_FOUND) {
143 9
            $this->statusCode = Response::HTTP_NOT_FOUND;
144
145 9
            if (extension_loaded('newrelic')) {
146
                newrelic_name_transaction('404');
147
            }
148
        }
149
150 26
        $this->breadcrumb = new Breadcrumb($this->getKernel());
151 26
        $this->footer = new Footer($this->getKernel());
152
153 26
        $this->processPage();
154
155
        // execute all extras linked to the page
156 26
        array_map([$this, 'processExtra'], $this->extras);
157 26
    }
158
159 26
    private function checkAuthentication(): void
160
    {
161
        // no authentication needed
162 26
        if (!isset($this->record['data']['auth_required'])
163
            || !$this->record['data']['auth_required']
164 26
            || !BackendModel::isModuleInstalled('Profiles')
165
        ) {
166 26
            return;
167
        }
168
169
        if (!FrontendAuthenticationModel::isLoggedIn()) {
170
            throw new InsufficientAuthenticationException('You must log in to see this page');
171
        }
172
173
        // specific groups for auth?
174
        if (empty($this->record['data']['auth_groups'])) {
175
            // no further checks needed
176
            return;
177
        }
178
179
        foreach ($this->record['data']['auth_groups'] as $group) {
180
            if (FrontendAuthenticationModel::getProfile()->isInGroup($group)) {
181
                // profile is in a group that is allowed to see the page
182
                return;
183
            }
184
        }
185
186
        throw new NotFoundHttpException('The current page is not available to the logged in profile');
187
    }
188
189 26
    public function display(): Response
190
    {
191
        try {
192
            // assign the id so we can use it as an option
193 26
            $this->template->assignGlobal('isPage' . $this->pageId, true);
194 26
            $this->template->assignGlobal('isChildOfPage' . $this->record['parent_id'], true);
195
196
            // hide the cookiebar from within the code to prevent flickering
197
            // @deprecated remove this in Fork 6, the privacy consent dialog should be used
198 26
            $this->template->assignGlobal(
199 26
                'cookieBarHide',
200 26
                !$this->get('fork.settings')->get('Core', 'show_cookie_bar', false)
201 26
                || $this->getContainer()->get('fork.cookie')->hasHiddenCookieBar()
202
            );
203 26
            $this->parsePrivacyConsents();
204 26
            $this->parsePositions();
205
206
            // assign empty positions
207 26
            $unusedPositions = array_diff(
208 26
                $this->record['template_data']['names'],
209 26
                array_keys($this->record['positions'])
210
            );
211 26
            foreach ($unusedPositions as $position) {
212 26
                $this->template->assign('position' . \SpoonFilter::ucfirst($position), []);
213
            }
214
215 26
            $this->header->parse();
216 26
            $this->breadcrumb->parse();
217 26
            $this->parseLanguages();
218 26
            $this->footer->parse();
219
220 26
            return new Response(
221 26
                $this->template->getContent($this->templatePath),
222 26
                $this->statusCode
223
            );
224 9
        } catch (NotFoundHttpException $notFoundHttpException) {
225 9
            $this->handlePage(Response::HTTP_NOT_FOUND);
226
227 9
            return $this->display();
228
        } catch (InsufficientAuthenticationException $insufficientAuthenticationException) {
229
            $this->redirectToLogin();
230
        }
231
    }
232
233
    /**
234
     * Redirects to the login page in a way that the login page will redirect back to the current page after logging in
235
     */
236
    private function redirectToLogin(): void
237
    {
238
        $this->redirect(
239
            Navigation::getUrlForBlock('Profiles', 'Login') . '?queryString=' . Model::getRequest()->getRequestUri(),
240
            Response::HTTP_TEMPORARY_REDIRECT
241
        );
242
    }
243
244
    public function getExtras(): array
245
    {
246
        return $this->extras;
247
    }
248
249
    public function getId(): int
250
    {
251
        return $this->pageId;
252
    }
253
254 26
    private function getPageRecord(int $pageId): array
255
    {
256 26
        if ($this->url->getParameter('page_revision', 'int') === null) {
257 26
            return Model::getPage($pageId);
258
        }
259
260
        // add no-index to meta-custom, so the draft won't get accidentally indexed
261
        $this->header->addMetaData(['name' => 'robots', 'content' => 'noindex, nofollow'], true);
262
263
        return Model::getPageRevision($this->url->getParameter('page_revision', 'int'));
0 ignored issues
show
Bug introduced by
It seems like $this->url->getParameter('page_revision', 'int') can also be of type null; however, parameter $revisionId of Frontend\Core\Engine\Model::getPageRevision() does only seem to accept integer, 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

263
        return Model::getPageRevision(/** @scrutinizer ignore-type */ $this->url->getParameter('page_revision', 'int'));
Loading history...
264
    }
265
266 26
    protected function getPageContent(int $pageId): array
267
    {
268 26
        $record = $this->getPageRecord($pageId);
269
270 26
        if (empty($record)) {
271
            return [];
272
        }
273
274
        // redirect to the first child if the page is empty
275 26
        if ($this->allPositionsAreEmpty($record['positions'])) {
276
            $firstChildId = Navigation::getFirstChildId($record['id']);
277
278
            // check if we actually have a first child
279
            if ($firstChildId !== 0) {
280
                $this->redirect(Navigation::getUrl($firstChildId));
281
            }
282
        }
283
284 26
        return $record;
285
    }
286
287 26
    private function allPositionsAreEmpty(array $positions): bool
288
    {
289
        // loop positions to check if they are empty
290 26
        foreach ($positions as $blocks) {
291
            // loop blocks in position
292 26
            foreach ($blocks as $block) {
293
                // It isn't empty if HTML is provided, a decent extra is provided or a widget is provided
294 26
                if ($block['extra_type'] === 'block'
295 9
                    || $block['extra_type'] === 'widget'
296 26
                    || trim($block['html']) !== ''
297
                ) {
298 26
                    return false;
299
                }
300
            }
301
        }
302
303
        return true;
304
    }
305
306
    public function getRecord(): array
307
    {
308
        return $this->record;
309
    }
310
311
    public function getStatusCode(): int
312
    {
313
        return $this->statusCode;
314
    }
315
316 26
    protected function parseLanguages(): void
317
    {
318
        // just execute if the site is multi-language
319 26
        if (!$this->getContainer()->getParameter('site.multilanguage') || count(Language::getActiveLanguages()) === 1) {
320 26
            return;
321
        }
322
323
        $this->template->assignGlobal(
324
            'languages',
325
            array_map(
326
                function (string $language) {
327
                    return [
328
                        'url' => '/' . $language,
329
                        'label' => $language,
330
                        'name' => Language::msg(mb_strtoupper($language)),
331
                        'current' => $language === LANGUAGE,
332
                    ];
333
                },
334
                Language::getActiveLanguages()
335
            )
336
        );
337
    }
338
339 26
    protected function parsePrivacyConsents(): void
340
    {
341 26
        $consentDialog = $this->get(ConsentDialog::class);
342
343 26
        $this->template->assignGlobal('privacyConsentEnabled', $consentDialog->isDialogEnabled());
344 26
        $this->template->assignGlobal('privacyConsentDialogHide', !$consentDialog->shouldDialogBeShown());
345 26
        $this->template->assignGlobal('privacyConsentDialogLevels', $consentDialog->getLevels());
346
    }
347 26
348
    protected function parsePositions(): void
349
    {
350 26
        // init array to store parsed positions data
351
        $positions = [];
352
353 26
        // fetch variables from main template
354
        $mainVariables = $this->template->getAssignedVariables();
355
356 26
        // loop all positions
357
        foreach ($this->record['positions'] as $position => $blocks) {
358 26
            // loop all blocks in this position
359 26
            foreach ($blocks as $i => $block) {
360
                $positions[$position][$i] = $this->parseBlock($block, $mainVariables);
361
            }
362
363 26
            // assign position to template
364
            $this->template->assign('position' . \SpoonFilter::ucfirst($position), $positions[$position]);
365
        }
366 26
367 26
        $this->template->assign('positions', $positions);
368
    }
369 26
370
    private function parseBlock(array $block, array $mainVariables): array
371 26
    {
372 9
        if (!isset($block['extra'])) {
373 9
            $parsedBlock = $block;
374 9
            if (array_key_exists('blockContent', $block)) {
375
                $parsedBlock['html'] = $block['blockContent'];
376
            }
377 9
378
            return $parsedBlock;
379
        }
380 26
381 26
        $block['extra']->execute();
382 26
        $extraVariables = $block['extra']->getTemplate()->getAssignedVariables();
383 26
        $block['extra']->getTemplate()->assignArray($mainVariables);
384
        $block['extra']->getTemplate()->assignArray($extraVariables);
385
386 26
        return [
387
            'variables' => $block['extra']->getTemplate()->getAssignedVariables(),
388 26
            'blockIsEditor' => false,
389
            'html' => $block['extra']->getContent(),
390
        ];
391
    }
392 26
393
    protected function processExtra(ModuleExtraInterface $extra): void
394 26
    {
395 26
        $this->getContainer()->get('logger.public')->info(
396
            'Executing ' . get_class($extra) . " '{$extra->getAction()}' for module '{$extra->getModule()}'."
0 ignored issues
show
Bug introduced by
The method getAction() does not exist on Frontend\Core\Engine\Block\ModuleExtraInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Frontend\Core\Engine\Block\ModuleExtraInterface. ( Ignorable by Annotation )

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

396
            'Executing ' . get_class($extra) . " '{$extra->/** @scrutinizer ignore-call */ getAction()}' for module '{$extra->getModule()}'."
Loading history...
Bug introduced by
The method getModule() does not exist on Frontend\Core\Engine\Block\ModuleExtraInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Frontend\Core\Engine\Block\ModuleExtraInterface. ( Ignorable by Annotation )

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

396
            'Executing ' . get_class($extra) . " '{$extra->getAction()}' for module '{$extra->/** @scrutinizer ignore-call */ getModule()}'."
Loading history...
397
        );
398
399 26
        // overwrite the template
400
        if (is_callable([$extra, 'getOverwrite']) && $extra->getOverwrite()) {
0 ignored issues
show
Bug introduced by
The method getOverwrite() does not exist on Frontend\Core\Engine\Block\ModuleExtraInterface. It seems like you code against a sub-type of Frontend\Core\Engine\Block\ModuleExtraInterface such as Frontend\Core\Engine\Block\ExtraInterface. ( Ignorable by Annotation )

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

400
        if (is_callable([$extra, 'getOverwrite']) && $extra->/** @scrutinizer ignore-call */ getOverwrite()) {
Loading history...
401
            $this->templatePath = $extra->getTemplatePath();
402 26
        }
403
    }
404 26
405
    private function addAlternateLinks(): void
406
    {
407 26
        // no need for alternate links if there is only one language
408
        if (!$this->getContainer()->getParameter('site.multilanguage')) {
409
            return;
410
        }
411 26
412 26
        array_map([$this, 'addAlternateLinkForLanguage'], Language::getActiveLanguages());
413
    }
414 26
415
    private function addAlternateLinkForLanguage(string $language): void
416 26
    {
417 26
        if ($language === LANGUAGE) {
418
            return;
419
        }
420
421
        // Get page data
422
        $pageInfo = Model::getPage($this->pageId);
423
424
        // Check if hreflang is set for language
425
        if (isset($pageInfo['data']['hreflang_' . $language])) {
426
            $url = Navigation::getUrl($pageInfo['data']['hreflang_' . $language], $language);
427
        } else {
428
            $url = Navigation::getUrl($this->pageId, $language);
429
        }
430
431
        // remove last /
432
        $url = rtrim($url, '/\\');
433
434
        // Ignore 404 links
435
        if ($this->pageId !== Response::HTTP_NOT_FOUND
436
            && $url === Navigation::getUrl(Response::HTTP_NOT_FOUND, $language)) {
437
            return;
438
        }
439
440
        // Convert relative to absolute url
441
        if (strpos($url, '/') === 0) {
442
            $url = SITE_URL . $url;
443
        }
444
445
        $this->header->addLink(['rel' => 'alternate', 'hreflang' => $language, 'href' => $url]);
446
    }
447 26
448
    private function assignPageMeta(): void
449
    {
450 26
        // set pageTitle
451 26
        $this->header->setPageTitle(
452 26
            $this->record['meta_title'],
453
            $this->record['meta_title_overwrite']
454
        );
455
456 26
        // set meta-data
457 26
        $this->header->addMetaDescription(
458 26
            $this->record['meta_description'],
459
            $this->record['meta_description_overwrite']
460 26
        );
461 26
        $this->header->addMetaKeywords(
462 26
            $this->record['meta_keywords'],
463
            $this->record['meta_keywords_overwrite']
464 26
        );
465
        $this->header->setMetaCustom($this->record['meta_custom']);
466
467 26
        // set canonical url
468
        if (array_key_exists('meta_data', $this->record) &&
469
            is_array($this->record['meta_data']) &&
470
            array_key_exists('canonical_url_overwrite', $this->record['meta_data']) &&
471
            array_key_exists('canonical_url', $this->record['meta_data'])) {
472 26
            if ((bool)$this->record['meta_data']['canonical_url_overwrite'] &&
473
                !empty($this->record['meta_data']['canonical_url'])) {
474
                $this->header->setCanonicalUrl($this->record['meta_data']['canonical_url']);
475
            }
476
        }
477 26
478
        // advanced SEO-attributes
479 26
        if (isset($this->record['meta_seo_index'])) {
480
            $this->header->addMetaData(
481 26
                ['name' => 'robots', 'content' => $this->record['meta_seo_index']]
482 26
            );
483 26
        }
484
        if (isset($this->record['meta_seo_follow'])) {
485
            $this->header->addMetaData(
486 26
                ['name' => 'robots', 'content' => $this->record['meta_seo_follow']]
487 26
            );
488 26
        }
489
    }
490
491 26
    protected function processPage(): void
492
    {
493
        $this->assignPageMeta();
494 26
        new Navigation($this->getKernel());
495
        $this->addAlternateLinks();
496 26
497
        // assign content
498
        $pageInfo = Navigation::getPageInfo($this->record['id']);
499
        $this->record['has_children'] = $pageInfo['has_children'];
500 26
        $this->template->assignGlobal('page', $this->record);
501
502 26
        // set template path
503
        $this->templatePath = $this->record['template_path'];
504 9
505 9
        // loop blocks
506
        foreach ($this->record['positions'] as $position => &$blocks) {
507
            // position not known in template = skip it
508
            if (!in_array($position, $this->record['template_data']['names'])) {
509 26
                continue;
510
            }
511
512 26
            $blocks = array_map(
513
                function (array $block) {
514 26
                    if ($block['extra_id'] === null) {
515 26
                        return [
516 26
                            'blockIsEditor' => true,
517
                            'blockContent' => $block['html'],
518
                        ];
519 26
                    }
520
521 26
                    $block = ['extra' => $this->getExtraForBlock($block)];
522
523
                    // add to list of extras to parse
524 26
                    $this->extras[] = $block['extra'];
525 26
526
                    return $block;
527
                },
528
                $blocks
529 26
            );
530 26
        }
531 26
    }
532 26
533 26
    private function getExtraForBlock(array $block): ModuleExtraInterface
534
    {
535
        // block
536
        if ($block['extra_type'] === 'block') {
537 17
            if (extension_loaded('newrelic')) {
538 17
                newrelic_name_transaction($block['extra_module'] . '::' . $block['extra_action']);
539 17
            }
540 17
541 17
            return new FrontendBlockExtra(
542
                $this->getKernel(),
543
                $block['extra_module'],
544
                $block['extra_action'],
545
                $block['extra_data']
546
            );
547
        }
548
549
        return new FrontendBlockWidget(
550
            $this->getKernel(),
551
            $block['extra_module'],
552
            $block['extra_action'],
553
            $block['extra_data']
554
        );
555
    }
556
557
    private function redirect(string $url, int $code = RedirectResponse::HTTP_FOUND): void
558
    {
559
        throw new RedirectException('Redirect', new RedirectResponse($url, $code));
560
    }
561
}
562