AdminTemplate::parseMainMenuItem()   F
last analyzed

Complexity

Conditions 19
Paths 720

Size

Total Lines 67
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 19
eloc 40
nc 720
nop 3
dl 0
loc 67
rs 0.7388
c 0
b 0
f 0

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 Charcoal\Admin;
4
5
use Exception;
6
use InvalidArgumentException;
7
8
// From PSR-7
9
use Psr\Http\Message\RequestInterface;
10
11
// From Pimple
12
use Pimple\Container;
13
14
// From 'charcoal-factory'
15
use Charcoal\Factory\FactoryInterface;
16
17
// From 'charcoal-user'
18
use Charcoal\User\AuthAwareInterface;
19
use Charcoal\User\AuthAwareTrait;
20
21
// From 'charcoal-translator'
22
use Charcoal\Translator\TranslatorAwareTrait;
23
24
// From 'charcoal-app'
25
use Charcoal\App\Template\AbstractTemplate;
26
27
// From 'charcoal-admin'
28
use Charcoal\Admin\Ui\DashboardContainerInterface;
29
use Charcoal\Admin\Support\AdminTrait;
30
use Charcoal\Admin\Support\BaseUrlTrait;
31
use Charcoal\Admin\Support\SecurityTrait;
32
use Charcoal\Admin\Ui\FeedbackContainerTrait;
33
34
/**
35
 * Base class for all `admin` Templates.
36
 *
37
 * # Available (mustache) methods
38
 * - `title` (Translation) - The page title
39
 * - `subtitle` (Translation) The page subtitle
40
 * - `showMainMenu` (bool) - Display the main menu or not
41
 * - `mainMenu` (iterator) - The main menu data
42
 * - `showSystemMenu` (bool) - Display the footer menu or not
43
 * - `systemMenu` (iterator) - The footer menu data
44
 */
45
class AdminTemplate extends AbstractTemplate implements
46
    AuthAwareInterface
47
{
48
    use AdminTrait;
49
    use AuthAwareTrait;
50
    use BaseUrlTrait;
51
    use FeedbackContainerTrait;
52
    use SecurityTrait;
53
    use TranslatorAwareTrait;
54
55
    const GOOGLE_RECAPTCHA_CLIENT_URL = 'https://www.google.com/recaptcha/api.js';
56
57
    /**
58
     * The name of the project.
59
     *
60
     * @var Translation|string|null
0 ignored issues
show
Bug introduced by
The type Charcoal\Admin\Translation was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
61
     */
62
    private $siteName;
63
64
    /**
65
     * @var string $ident
66
     */
67
    private $ident;
68
69
    /**
70
     * @var Translation|string|null $label
71
     */
72
    protected $label;
73
74
    /**
75
     * @var Translation|string|null $title
76
     */
77
    protected $title;
78
79
    /**
80
     * @var Translation|string|null $subtitle
81
     */
82
    protected $subtitle;
83
84
    /**
85
     * @var boolean
86
     */
87
    private $showSecondaryMenu = true;
88
89
    /**
90
     * @var boolean
91
     */
92
    private $showMainMenu = true;
93
94
    /**
95
     * @var boolean
96
     */
97
    private $showSystemMenu = true;
98
99
    /**
100
     * @var boolean
101
     */
102
    protected $mainMenu;
103
104
    /**
105
     * @var boolean
106
     */
107
    protected $mainMenuIdentLoaded = false;
108
109
    /**
110
     * @var string|null
111
     */
112
    protected $mainMenuIdent;
113
114
    /**
115
     * @var boolean
116
     */
117
    protected $systemMenu;
118
119
    /**
120
     * @var SecondaryMenuWidgetInterface
0 ignored issues
show
Bug introduced by
The type Charcoal\Admin\SecondaryMenuWidgetInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
121
     */
122
    protected $secondaryMenu;
123
124
    /**
125
     * @var FactoryInterface $modelFactory
126
     */
127
    private $modelFactory;
128
129
    /**
130
     * @var FactoryInterface $widgetFactory
131
     */
132
    private $widgetFactory;
133
134
    /**
135
     * The cache of parsed template names.
136
     *
137
     * @var array
138
     */
139
    protected static $templateNameCache = [];
140
141
    /**
142
     * Template's init method is called automatically from `charcoal-app`'s Template Route.
143
     *
144
     * For admin templates, initializations is:
145
     *
146
     * - to start a session, if necessary
147
     * - to authenticate
148
     * - to initialize the template data with the PSR Request object
149
     *
150
     * @param RequestInterface $request The request to initialize.
151
     * @return boolean
152
     * @see \Charcoal\App\Route\TemplateRoute::__invoke()
153
     */
154
    public function init(RequestInterface $request)
155
    {
156
        if (!session_id()) {
157
            session_cache_limiter(false);
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type string expected by parameter $cache_limiter of session_cache_limiter(). ( Ignorable by Annotation )

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

157
            session_cache_limiter(/** @scrutinizer ignore-type */ false);
Loading history...
158
            session_start();
159
        }
160
161
        $this->setDataFromRequest($request);
162
        $this->authRedirect($request);
163
164
        return parent::init($request);
165
    }
166
167
    /**
168
     * Determine if the current user is authenticated, if not redirect them to the login page.
169
     *
170
     * @todo   Move auth-check and redirection to a middleware or dedicated admin route.
171
     * @param  RequestInterface $request The request to initialize.
172
     * @return void
173
     */
174
    protected function authRedirect(RequestInterface $request)
175
    {
176
        // Test if authentication is required.
177
        if ($this->authRequired() === false) {
0 ignored issues
show
introduced by
The condition $this->authRequired() === false is always false.
Loading history...
178
            return;
179
        }
180
181
        // Test if user is authorized to access this controller
182
        if ($this->isAuthorized() === true) {
183
            return;
184
        }
185
186
        $redirectTo = urlencode($request->getRequestTarget());
187
188
        header('HTTP/1.0 403 Forbidden');
189
        header('Location: '.$this->adminUrl('login?redirect_to='.$redirectTo));
190
        exit;
191
    }
192
193
    /**
194
     * Sets the template data from a PSR Request object.
195
     *
196
     * @param  RequestInterface $request A PSR-7 compatible Request instance.
197
     * @return self
198
     */
199
    protected function setDataFromRequest(RequestInterface $request)
200
    {
201
        $keys = $this->validDataFromRequest();
202
        if (!empty($keys)) {
203
            $this->setData($request->getParams($keys));
0 ignored issues
show
Bug introduced by
The method getParams() does not exist on Psr\Http\Message\RequestInterface. It seems like you code against a sub-type of Psr\Http\Message\RequestInterface such as Slim\Http\Request. ( Ignorable by Annotation )

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

203
            $this->setData($request->/** @scrutinizer ignore-call */ getParams($keys));
Loading history...
204
        }
205
206
        return $this;
207
    }
208
209
    /**
210
     * Retrieve the list of parameters to extract from the HTTP request.
211
     *
212
     * @return string[]
213
     */
214
    protected function validDataFromRequest()
215
    {
216
        return [
217
            // HTTP Handling
218
            'next_url',
219
            // Navigation Menusa
220
            'main_menu_item', 'secondary_menu_item', 'system_menu_item',
221
        ];
222
    }
223
224
    /**
225
     * @param mixed $ident Template identifier.
226
     * @return AdminTemplate Chainable
227
     */
228
    public function setIdent($ident)
229
    {
230
        $this->ident = $ident;
231
        return $this;
232
    }
233
234
    /**
235
     * @return string
236
     */
237
    public function ident()
238
    {
239
        return $this->ident;
240
    }
241
242
    /**
243
     * @param mixed $label Template label.
244
     * @return AdminTemplate Chainable
245
     */
246
    public function setLabel($label)
247
    {
248
        $this->label = $this->translator()->translation($label);
249
250
        return $this;
251
    }
252
253
    /**
254
     * @return Translation|string|null
255
     */
256
    public function label()
257
    {
258
        return $this->label;
259
    }
260
261
    /**
262
     * Set the title of the page.
263
     *
264
     * @param  mixed $title Template title.
265
     * @return AdminTemplate Chainable
266
     */
267
    public function setTitle($title)
268
    {
269
        $this->title = $this->translator()->translation($title);
270
271
        return $this;
272
    }
273
274
    /**
275
     * Retrieve the title of the page.
276
     *
277
     * @return Translation|string|null
278
     */
279
    public function title()
280
    {
281
        if ($this->title === null) {
282
            return $this->siteName();
283
        }
284
285
        return $this->title;
286
    }
287
288
    /**
289
     * Set the page's sub-title.
290
     *
291
     * @param mixed $subtitle Template subtitle.
292
     * @return AdminTemplate Chainable
293
     */
294
    public function setSubtitle($subtitle)
295
    {
296
        $this->subtitle = $this->translator()->translation($subtitle);
297
298
        return $this;
299
    }
300
301
    /**
302
     * Retrieve the page's sub-title.
303
     *
304
     * @return Translation|string|null
305
     */
306
    public function subtitle()
307
    {
308
        return $this->subtitle;
309
    }
310
311
    /**
312
     * @param boolean $show The show main menu flag.
313
     * @return AdminTemplate Chainable
314
     */
315
    public function setShowMainMenu($show)
316
    {
317
        $this->showMainMenu = !!$show;
318
        return $this;
319
    }
320
321
    /**
322
     * @return boolean
323
     */
324
    public function showMainMenu()
325
    {
326
        return ($this->isAuthorized() && $this->showMainMenu);
327
    }
328
329
    /**
330
     * Yield the main menu.
331
     *
332
     * @return array|Generator
0 ignored issues
show
Bug introduced by
The type Charcoal\Admin\Generator was not found. Did you mean Generator? If so, make sure to prefix the type with \.
Loading history...
333
     */
334
    public function mainMenu()
335
    {
336
        if ($this->mainMenu === null) {
337
            $options = null;
338
339
            if ($this instanceof DashboardContainerInterface) {
340
                $dashboardConfig = $this->dashboardConfig();
341
342
                if (isset($dashboardConfig['secondary_menu'])) {
343
                    $options = $dashboardConfig['secondary_menu'];
344
                }
345
            }
346
347
            $this->mainMenu = $this->createMainMenu($options);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->createMainMenu($options) of type array is incompatible with the declared type boolean of property $mainMenu.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
348
        }
349
350
        return $this->mainMenu;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->mainMenu also could return the type boolean which is incompatible with the documented return type Charcoal\Admin\Generator|array.
Loading history...
351
    }
352
353
    /**
354
     * @param boolean $show The show footer menu flag.
355
     * @return AdminTemplate Chainable
356
     */
357
    public function setShowSystemMenu($show)
358
    {
359
        $this->showSystemMenu = !!$show;
360
        return $this;
361
    }
362
363
    /**
364
     * @return boolean
365
     */
366
    public function showSystemMenu()
367
    {
368
        return ($this->isAuthorized() && $this->showSystemMenu && (count($this->systemMenu()) > 0));
369
    }
370
371
    /**
372
     * @return array
373
     */
374
    public function systemMenu()
375
    {
376
        if ($this->systemMenu === null) {
377
            $this->systemMenu = $this->createSystemMenu();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->createSystemMenu() of type array is incompatible with the declared type boolean of property $systemMenu.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
378
        }
379
380
        return new \ArrayIterator($this->systemMenu);
0 ignored issues
show
Bug Best Practice introduced by
The expression return new ArrayIterator($this->systemMenu) returns the type ArrayIterator which is incompatible with the documented return type array.
Loading history...
Bug introduced by
It seems like $this->systemMenu can also be of type boolean; however, parameter $array of ArrayIterator::__construct() 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

380
        return new \ArrayIterator(/** @scrutinizer ignore-type */ $this->systemMenu);
Loading history...
381
    }
382
383
    /**
384
     * @param  boolean $show The show secondary menu flag.
385
     * @return AdminTemplate Chainable
386
     */
387
    public function setShowSecondaryMenu($show)
388
    {
389
        $this->showSecondaryMenu = !!$show;
390
        return $this;
391
    }
392
393
    /**
394
     * @return boolean
395
     */
396
    public function showSecondaryMenu()
397
    {
398
        return ($this->isAuthorized() && $this->showSecondaryMenu);
399
    }
400
401
    /**
402
     * Retrieve the secondary menu.
403
     *
404
     * @return \Charcoal\Admin\Widget\SecondaryMenuWidgetInterface|null
405
     */
406
    public function secondaryMenu()
407
    {
408
        if ($this->secondaryMenu === null) {
409
            $this->secondaryMenu = $this->createSecondaryMenu();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->createSecondaryMenu() of type Charcoal\Admin\Widget\Se...ryMenuWidgetInterface[] is incompatible with the declared type Charcoal\Admin\SecondaryMenuWidgetInterface of property $secondaryMenu.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
410
        }
411
412
        return $this->secondaryMenu;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->secondaryMenu also could return the type Charcoal\Admin\Widget\Se...ryMenuWidgetInterface[] which is incompatible with the documented return type Charcoal\Admin\Widget\Se...enuWidgetInterface|null.
Loading history...
413
    }
414
415
    /**
416
     * @return string
417
     */
418
    public function mainMenuLogo()
419
    {
420
        $logo = $this->adminConfig('menu_logo');
421
        if (!empty($logo)) {
422
            return $logo;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $logo also could return the type Charcoal\Admin\Config which is incompatible with the documented return type string.
Loading history...
423
        }
424
425
        return 'assets/admin/images/identicon.png';
426
    }
427
428
    /**
429
     * @return string
430
     */
431
    public function navContainerCssClasses()
432
    {
433
        $classes = [ 'has-nav-logo' ];
434
435
        if ($this->showMainMenu()) {
436
            $classes[] = 'has-nav-main';
437
        }
438
439
        if ($this->showSecondaryMenu()) {
440
            $classes[] = 'has-nav-sub';
441
        }
442
443
        /** @see ::showSystemMenu() */
444
        if ($this->isAuthenticated()) {
445
            $classes[] = 'has-nav-system';
446
        }
447
448
        return implode(' ', $classes);
449
    }
450
451
    /**
452
     * Get the "Visit website" label.
453
     *
454
     * @return string|boolean The button's label,
455
     *     TRUE to use the default label,
456
     *     or FALSE to disable the link.
457
     */
458
    public function visitSiteLabel()
459
    {
460
        $label = $this->adminConfig('main_menu.visit_site');
461
        if ($label === false) {
462
            return false;
463
        }
464
465
        if (empty($label) || $label === true) {
466
            $label = $this->translator()->translate('Visit Site');
467
        } else {
468
            $label = $this->translator()->translate($label);
469
        }
470
471
        return $label;
472
    }
473
474
    /**
475
     * Retrieve the name of the project.
476
     *
477
     * @return Translation|string|null
478
     */
479
    public function siteName()
480
    {
481
        return $this->siteName;
482
    }
483
484
    /**
485
     * Retrieve the document title.
486
     *
487
     * @return Translation|string|null
488
     */
489
    public function documentTitle()
490
    {
491
        $siteName  = $this->siteName();
492
        $pageTitle = strip_tags($this->title());
493
494
        if ($pageTitle) {
495
            if ($pageTitle === $siteName) {
496
                return sprintf('%1$s &#8212; Charcoal', $pageTitle);
497
            } else {
498
                return sprintf('%1$s &lsaquo; %2$s &#8212; Charcoal', $pageTitle, $siteName);
499
            }
500
        }
501
502
        return $siteName;
503
    }
504
505
    /**
506
     * Retrieve the current language.
507
     *
508
     * @return string
509
     */
510
    public function lang()
511
    {
512
        return $this->translator()->getLocale();
513
    }
514
515
    /**
516
     * Retrieve the current language.
517
     *
518
     * @return string
519
     */
520
    public function locale()
521
    {
522
        $lang    = $this->lang();
523
        $locales = $this->translator()->locales();
524
525
        if (isset($locales[$lang]['locale'])) {
526
            $locale = $locales[$lang]['locale'];
527
            if (is_array($locale)) {
528
                $locale = implode(' ', $locale);
529
            }
530
        } else {
531
            $locale = 'en-US';
532
        }
533
534
        return $locale;
535
    }
536
537
    /**
538
     * Determine if a CAPTCHA test is available.
539
     *
540
     * For example, the "Login", "Lost Password", and "Reset Password" templates
541
     * can render the CAPTCHA test.
542
     *
543
     * @see    AdminAction::recaptchaEnabled() Duplicate
544
     * @return boolean
545
     */
546
    public function recaptchaEnabled()
547
    {
548
        $recaptcha = $this->apiConfig('google.recaptcha');
549
550
        if (empty($recaptcha) || (isset($recaptcha['active']) && $recaptcha['active'] === false)) {
551
            return false;
552
        }
553
554
        return (!empty($recaptcha['public_key'])  || !empty($recaptcha['key'])) &&
555
               (!empty($recaptcha['private_key']) || !empty($recaptcha['secret']));
556
    }
557
558
    /**
559
     * Determine if the CAPTCHA test is invisible.
560
     *
561
     * Note: Charcoal's implementation of Google reCAPTCHA defaults to "invisible".
562
     *
563
     * @return boolean
564
     */
565
    public function recaptchaInvisible()
566
    {
567
        $recaptcha = $this->apiConfig('google.recaptcha');
568
569
        $hasInvisible = isset($recaptcha['invisible']);
570
        if ($hasInvisible && $recaptcha['invisible'] === true) {
571
            return true;
572
        }
573
574
        $hasSize = isset($recaptcha['size']);
575
        if ($hasSize && $recaptcha['size'] === 'invisible') {
576
            return true;
577
        }
578
579
        if (!$hasInvisible && !$hasSize) {
580
            return true;
581
        }
582
583
        return false;
584
    }
585
586
    /**
587
     * Alias of {@see self::recaptchaSiteKey()}.
588
     *
589
     * @deprecated
590
     * @return string|null
591
     */
592
    public function recaptchaKey()
593
    {
594
        return $this->recaptchaSiteKey();
595
    }
596
597
    /**
598
     * Retrieve the Google reCAPTCHA public (site) key.
599
     *
600
     * @throws RuntimeException If Google reCAPTCHA is required but not configured.
601
     * @return string|null
602
     */
603
    public function recaptchaSiteKey()
604
    {
605
        $recaptcha = $this->apiConfig('google.recaptcha');
606
607
        if (!empty($recaptcha['public_key'])) {
608
            return $recaptcha['public_key'];
0 ignored issues
show
Bug Best Practice introduced by
The expression return $recaptcha['public_key'] also could return the type Charcoal\Config\AbstractConfig|mixed which is incompatible with the documented return type null|string.
Loading history...
609
        } elseif (!empty($recaptcha['key'])) {
610
            return $recaptcha['key'];
611
        }
612
613
        return null;
614
    }
615
616
    /**
617
     * Retrieve the parameters for the Google reCAPTCHA widget.
618
     *
619
     * @return string[]
620
     */
621
    public function recaptchaParameters()
622
    {
623
        $apiConfig = $this->apiConfig('google.recaptcha');
624
        $tplConfig = $this->get('recaptcha_options') ?: [];
625
626
        $params = [
627
            'sitekey'  => $this->recaptchaSiteKey(),
628
            'badge'    => null,
629
            'type'     => null,
630
            'size'     => 'invisible',
631
            'tabindex' => null,
632
            'callback' => null,
633
        ];
634
635
        if ($this->recaptchaInvisible() === false) {
636
            $params['size'] = null;
637
        }
638
639
        foreach ($params as $key => $val) {
640
            if ($val === null || $val === '') {
641
                if (isset($tplConfig[$key])) {
642
                    $val = $tplConfig[$key];
643
                } elseif (isset($apiConfig[$key])) {
644
                    $val = $apiConfig[$key];
645
                }
646
647
                $params[$key] = $val;
648
            }
649
        }
650
651
        return $params;
652
    }
653
654
    /**
655
     * Generate a string representation of HTML attributes for the Google reCAPTCHA tag.
656
     *
657
     * @return string
658
     */
659
    public function recaptchaHtmlAttr()
660
    {
661
        $params = $this->recaptchaParameters();
662
663
        $attributes = [];
664
        foreach ($params as $key => $val) {
665
            if ($val !== null) {
666
                $attributes[] = sprintf('data-%s="%s"', $key, htmlspecialchars($val, ENT_QUOTES));
667
            }
668
        }
669
670
        return implode(' ', $attributes);
671
    }
672
673
    /**
674
     * Set common dependencies (services) used in all admin templates.
675
     *
676
     * @param Container $container DI Container.
677
     * @return void
678
     */
679
    protected function setDependencies(Container $container)
680
    {
681
        parent::setDependencies($container);
682
683
        // Satisfies TranslatorAwareTrait dependencies
684
        $this->setTranslator($container['translator']);
685
686
        // Satisfies AuthAwareInterface + SecurityTrait dependencies
687
        $this->setAuthenticator($container['admin/authenticator']);
688
        $this->setAuthorizer($container['admin/authorizer']);
689
690
        // Satisfies AdminTrait dependencies
691
        $this->setDebug($container['config']);
692
        $this->setAppConfig($container['config']);
693
        $this->setAdminConfig($container['admin/config']);
694
695
        // Satisfies BaseUrlTrait dependencies
696
        $this->setBaseUrl($container['base-url']);
697
        $this->setAdminUrl($container['admin/base-url']);
698
699
        // Satisfies AdminTemplate dependencies
700
        $this->setSiteName($container['config']['project_name']);
701
702
        $this->setModelFactory($container['model/factory']);
703
        $this->setWidgetFactory($container['widget/factory']);
704
705
        $this->menuBuilder = $container['menu/builder'];
0 ignored issues
show
Bug Best Practice introduced by
The property menuBuilder does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
706
        $this->menuItemBuilder = $container['menu/item/builder'];
0 ignored issues
show
Bug Best Practice introduced by
The property menuItemBuilder does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
707
    }
708
709
    /**
710
     * @throws Exception If the factory is not set.
711
     * @return FactoryInterface The model factory.
712
     */
713
    protected function modelFactory()
714
    {
715
        if (!$this->modelFactory) {
716
            throw new Exception(
717
                sprintf('Model factory is not set for template "%s".', get_class($this))
718
            );
719
        }
720
        return $this->modelFactory;
721
    }
722
723
    /**
724
     * @throws Exception If the widget factory dependency was not previously set / injected.
725
     * @return FactoryInterface
726
     */
727
    protected function widgetFactory()
728
    {
729
        if ($this->widgetFactory === null) {
730
            throw new Exception(
731
                'Widget factory was not set.'
732
            );
733
        }
734
        return $this->widgetFactory;
735
    }
736
737
    /**
738
     * Set the name of the project.
739
     *
740
     * @param  string $name Name of the project.
741
     * @return AdminTemplate Chainable
742
     */
743
    protected function setSiteName($name)
744
    {
745
        $this->siteName = $this->translator()->translation($name);
746
        return $this;
747
    }
748
749
    /**
750
     * Create the main menu using the admin config.
751
     *
752
     * @param  mixed $options The main menu widget ID or config.
753
     * @throws InvalidArgumentException If the admin config is missing, invalid, or malformed.
754
     * @return array
755
     */
756
    protected function createMainMenu($options = null)
757
    {
758
        $mainMenuConfig = $this->adminConfig('main_menu');
759
760
        if (!isset($mainMenuConfig['items'])) {
761
            throw new InvalidArgumentException(
762
                'Missing "admin.main_menu.items"'
763
            );
764
        }
765
766
        $mainMenuIdent = $this->mainMenuIdent($options);
767
768
        $menu = $this->menuBuilder->build([]);
769
        $menuItems = [];
770
        foreach ($mainMenuConfig['items'] as $menuIdent => $menuItem) {
771
            $menuItem['menu'] = $menu;
772
            $test = $this->menuItemBuilder->build($menuItem);
773
774
            if ($test->isAuthorized() === false) {
775
                continue;
776
            }
777
            unset($menuItem['menu']);
778
779
            if (isset($menuItem['active']) && $menuItem['active'] === false) {
780
                continue;
781
            }
782
783
            $menuItems[] = $this->parseMainMenuItem($menuItem, $menuIdent, $mainMenuIdent);
784
        }
785
786
        return $menuItems;
787
    }
788
789
    /**
790
     * Determine and retrieve the active main menu item's identifier.
791
     *
792
     * @param  mixed $options The secondary menu widget ID or config.
793
     * @return string|null
794
     */
795
    private function mainMenuIdent($options = null)
796
    {
797
        if ($this->mainMenuIdentLoaded === false) {
798
            $mainMenuIdent = null;
799
800
            if (isset($this['main_menu_item'])) {
801
                $mainMenuIdent = $this['main_menu_item'];
802
            }
803
804
            if (!(empty($options) && !is_numeric($options))) {
805
                if (is_string($options)) {
806
                    $mainMenuIdent = $options;
807
                } elseif (is_array($options)) {
808
                    if (isset($options['widget_options']['ident'])) {
809
                        $mainMenuIdent = $options['widget_options']['ident'];
810
                    }
811
                }
812
            }
813
814
            // Get main menu from the obj_type
815
            $objType = filter_input(INPUT_GET, 'obj_type', FILTER_SANITIZE_STRING);
816
            if ($objType) {
817
                $secondaryMenuItems = $this->adminConfig('secondary_menu');
818
                foreach ($secondaryMenuItems as $main => $item) {
819
                    if ($this->isObjTypeInSecondaryMenuItem($objType, $item)) {
820
                        $mainMenuIdent = $main;
821
                        break;
822
                    }
823
                }
824
            }
825
826
            // Choose main menu with a get parameter
827
            $mainMenuFromRequest = filter_input(INPUT_GET, 'main_menu', FILTER_SANITIZE_STRING);
828
            if ($mainMenuFromRequest) {
829
                $mainMenuIdent = $mainMenuFromRequest;
830
            }
831
832
            $this->mainMenuIdent = $mainMenuIdent;
833
            $this->mainMenuIdentLoaded = true;
834
        }
835
836
        return $this->mainMenuIdent;
837
    }
838
839
    /**
840
     * Check for the objType in the secondary menu items
841
     * returning true as soon as it its.
842
     *
843
     * @param string      $objType The ObjType to search.
844
     * @param array|mixed $item    The secondary menu item to search in.
845
     * @return boolean
846
     */
847
    protected function isObjTypeInSecondaryMenuItem($objType, $item)
848
    {
849
        if (isset($item['links'])) {
850
            foreach ($item['links'] as $obj => $i) {
851
                if ($obj === $objType) {
852
                    return true;
853
                }
854
            }
855
        }
856
857
        if (isset($item['groups'])) {
858
            foreach ($item['groups'] as $group) {
859
                if ($this->isObjTypeInSecondaryMenuItem($objType, $group)) {
860
                    return true;
861
                }
862
            }
863
        }
864
865
        return false;
866
    }
867
868
    /**
869
     * @throws InvalidArgumentException If the secondary menu widget is invalid.
870
     * @return \Charcoal\Admin\Widget\SecondaryMenuWidgetInterface[]|
871
     */
872
    protected function createSecondaryMenu()
873
    {
874
        $secondaryMenu = [];
875
        $secondaryMenuItems = $this->adminConfig('secondary_menu');
876
877
        // Get the ident of the active main menu item
878
        $mainMenuIdent = $this->mainMenuIdent();
879
880
        foreach ($secondaryMenuItems as $ident => $options) {
881
            $options['ident'] = $ident;
882
883
            if (isset($this['secondary_menu_item'])) {
884
                $options['current_item'] = $this['secondary_menu_item'];
885
            }
886
887
            if (isset($this['main_menu_item'])) {
888
                $mainMenuIdent = $this['main_menu_item'];
889
            }
890
891
            if (is_string($options['ident'])) {
892
                $options['is_current'] = $options['ident'] === $mainMenuIdent;
893
894
                $widget = $this->widgetFactory()
895
                                ->create('charcoal/admin/widget/secondary-menu')
896
                                ->setData($options);
897
898
                $secondaryMenu[] = $widget;
899
            }
900
        }
901
902
        return $secondaryMenu;
903
    }
904
905
    /**
906
     * @param  mixed $options The secondary menu widget ID or config.
907
     * @throws InvalidArgumentException If the menu is missing, invalid, or malformed.
908
     * @return array|Generator
909
     */
910
    protected function createSystemMenu($options = null)
911
    {
912
        $menuConfig = $this->adminConfig('system_menu');
913
914
        if (!isset($menuConfig['items'])) {
915
            return [];
916
        }
917
918
        $currentIdent = null;
919
        if (isset($this['system_menu_item'])) {
920
            $currentIdent = $this['system_menu_item'];
921
        }
922
923
        if (!(empty($options) && !is_numeric($options))) {
924
            if (is_string($options)) {
925
                $currentIdent = $options;
926
            } elseif (is_array($options)) {
927
                $menuConfig = array_replace_recursive($menuConfig, $options);
0 ignored issues
show
Bug introduced by
It seems like $menuConfig can also be of type Charcoal\Admin\Config; however, parameter $array of array_replace_recursive() 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

927
                $menuConfig = array_replace_recursive(/** @scrutinizer ignore-type */ $menuConfig, $options);
Loading history...
928
            }
929
        }
930
931
        $systemMenu = $this->menuBuilder->build([]);
932
        $menuItems  = [];
933
        foreach ($menuConfig['items'] as $menuIdent => $menuItem) {
934
            $menuItem['menu'] = $systemMenu;
935
            $test = $this->menuItemBuilder->build($menuItem);
936
            if ($test->isAuthorized() === false) {
937
                continue;
938
            }
939
            unset($menuItem['menu']);
940
941
            if (isset($menuItem['active']) && $menuItem['active'] === false) {
942
                continue;
943
            }
944
945
            $menuItem  = $this->parseSystemMenuItem($menuItem, $menuIdent, $currentIdent);
946
            $menuIdent = $menuItem['ident'];
947
948
            $menuItems[$menuIdent] = $menuItem;
949
        }
950
        return $menuItems;
951
    }
952
953
    /**
954
     * As a convenience, all admin templates have a model factory to easily create objects.
955
     *
956
     * @param FactoryInterface $factory The factory used to create models.
957
     * @return void
958
     */
959
    private function setModelFactory(FactoryInterface $factory)
960
    {
961
        $this->modelFactory = $factory;
962
    }
963
964
    /**
965
     * @param FactoryInterface $factory The widget factory, to create the dashboard and secondary menu widgets.
966
     * @return void
967
     */
968
    private function setWidgetFactory(FactoryInterface $factory)
969
    {
970
        $this->widgetFactory = $factory;
971
    }
972
973
    /**
974
     * @param  array       $menuItem     The menu structure.
975
     * @param  string|null $menuIdent    The menu identifier.
976
     * @param  string|null $currentIdent The current menu identifier.
977
     * @return array Finalized menu structure.
978
     */
979
    private function parseMainMenuItem(array $menuItem, $menuIdent = null, $currentIdent = null)
980
    {
981
        $svgUri = $this->baseUrl().'assets/admin/images/svgs.svg#icon-';
982
983
        if (isset($menuItem['ident'])) {
984
            $menuIdent = $menuItem['ident'];
985
        } else {
986
            $menuItem['ident'] = $menuIdent;
987
        }
988
989
        if (!empty($menuItem['url'])) {
990
            $url = $menuItem['url'];
991
            if ($url && strpos($url, ':') === false && !in_array($url[0], [ '/', '#', '?' ])) {
992
                $url = $this->adminUrl().$url;
993
            }
994
        } else {
995
            $url = '';
996
        }
997
998
        $menuItem['url'] = $url;
999
1000
        if (isset($menuItem['icon'])) {
1001
            $icon = $menuItem['icon'];
1002
            if ($icon && strpos($icon, ':') === false && !in_array($icon[0], [ '/', '#', '?' ])) {
1003
                $icon = $svgUri.$icon;
1004
            }
1005
        } else {
1006
            $icon = $svgUri.'contents';
1007
        }
1008
1009
        if (is_string($icon) && strpos($icon, '.svg') > 0) {
1010
            unset($menuItem['icon']);
1011
            $menuItem['svg'] = $icon;
1012
        } else {
1013
            unset($menuItem['svg']);
1014
            $menuItem['icon'] = $icon;
1015
        }
1016
1017
        if (isset($menuItem['label'])) {
1018
            $menuItem['label'] = $this->translator()->translation($menuItem['label']);
1019
        }
1020
1021
        $menuItem['show_label'] = (isset($menuItem['show_label']) ? !!$menuItem['show_label'] : true);
1022
1023
        $menuItem['selected'] = ($menuItem['ident'] === $currentIdent);
1024
1025
        $menuItem['hasSecondaryMenuTab'] = false;
1026
        $secondaryMenu = $this->adminConfig('secondary_menu');
1027
        if (!empty($menuIdent) && isset($secondaryMenu[$menuIdent])) {
1028
            /** Extract the secondary menu widget related to this main menu item. */
1029
            $secondaryMenuWidget = current(
1030
                array_filter(
1031
                    $this->secondaryMenu(),
0 ignored issues
show
Bug introduced by
$this->secondaryMenu() of type Charcoal\Admin\Widget\Se...enuWidgetInterface|null is incompatible with the type array expected by parameter $input of array_filter(). ( Ignorable by Annotation )

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

1031
                    /** @scrutinizer ignore-type */ $this->secondaryMenu(),
Loading history...
1032
                    function ($item) use ($menuIdent) {
1033
                        return $item->ident() === $menuIdent;
1034
                    }
1035
                )
1036
            );
1037
1038
            if (!empty($secondaryMenuWidget)) {
1039
                $menuItem['hasSecondaryMenuTab'] = $secondaryMenuWidget->isTabbed() || (
1040
                    $secondaryMenuWidget->hasSecondaryMenu() && $secondaryMenuWidget->isCurrent()
1041
                );
1042
            }
1043
        }
1044
1045
        return $menuItem;
1046
    }
1047
1048
    /**
1049
     * @param  array       $menuItem     The menu structure.
1050
     * @param  string|null $menuIdent    The menu identifier.
1051
     * @param  string|null $currentIdent The current menu identifier.
1052
     * @return array Finalized menu structure.
1053
     */
1054
    private function parseSystemMenuItem(array $menuItem, $menuIdent = null, $currentIdent = null)
1055
    {
1056
        if (!isset($menuItem['ident'])) {
1057
            $menuItem['ident'] = $menuIdent;
1058
        }
1059
1060
        if (!empty($menuItem['url'])) {
1061
            $url = $menuItem['url'];
1062
            if ($url && strpos($url, ':') === false && !in_array($url[0], [ '/', '#', '?' ])) {
1063
                $url = $this->adminUrl().$url;
1064
            }
1065
        } else {
1066
            $url = '#';
1067
        }
1068
1069
        $menuItem['url'] = $url;
1070
1071
        if ($menuItem['icon_css']) {
1072
            $menuItem['iconCss'] = $menuItem['icon_css'];
1073
        }
1074
1075
        if (isset($menuItem['label'])) {
1076
            $menuItem['label'] = $this->translator()->translation($menuItem['label']);
1077
        }
1078
1079
        $menuItem['selected'] = ($menuItem['ident'] === $currentIdent);
1080
1081
        return $menuItem;
1082
    }
1083
1084
1085
1086
    // Templating
1087
    // =========================================================================
1088
1089
    /**
1090
     * Generate a string containing HTML attributes for the <html> element.
1091
     *
1092
     * @return string
1093
     */
1094
    public function htmlAttr()
1095
    {
1096
        $attributes = [
1097
            'data-template' => $this->templateName(),
1098
            'data-debug'    => $this->debug() ? 'true' : false,
1099
            'lang'          => $this->lang(),
1100
            'locale'        => $this->locale(),
1101
            'class'         => $this->htmlClasses()
1102
        ];
1103
1104
        return html_build_attributes($attributes);
1105
    }
1106
1107
    /**
1108
     * Generate an array containing a list of CSS classes to be used by the <html> tag.
1109
     *
1110
     * @return array
1111
     */
1112
    public function htmlClasses()
1113
    {
1114
        $classes = [
1115
            'has-no-js'
1116
        ];
1117
1118
        if ($this->isFullscreenTemplate()) {
1119
            $classes[] = 'is-fullscreen-template';
1120
        }
1121
1122
        return $classes;
1123
    }
1124
1125
    /**
1126
     * Determine if main & secondary menu should appear as mobile in a desktop resolution.
1127
     *
1128
     * @return boolean
1129
     */
1130
    public function isFullscreenTemplate()
1131
    {
1132
        return false;
1133
    }
1134
1135
    /**
1136
     * Retrieve the template's identifier.
1137
     *
1138
     * @return string
1139
     */
1140
    public function templateName()
1141
    {
1142
        $key = substr(strrchr('\\'.get_class($this), '\\'), 1);
1143
1144
        if (!isset(static::$templateNameCache[$key])) {
1145
            $value = $key;
1146
1147
            if (!ctype_lower($value)) {
1148
                $value = preg_replace('/\s+/u', '', $value);
1149
                $value = mb_strtolower(preg_replace('/(.)(?=[A-Z])/u', '$1-', $value), 'UTF-8');
1150
            }
1151
1152
            $value = str_replace(
1153
                [ 'abstract', 'trait', 'interface', 'template', '\\' ],
1154
                '',
1155
                $value
1156
            );
1157
1158
            static::$templateNameCache[$key] = trim($value, '-');
1159
        }
1160
1161
        return static::$templateNameCache[$key];
1162
    }
1163
}
1164