Completed
Push — master ( 843223...7f5174 )
by
unknown
08:52
created

AdminTemplate::parseMainMenuItem()   F

Complexity

Conditions 19
Paths 720

Size

Total Lines 64
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 19
eloc 38
nc 720
nop 3
dl 0
loc 64
rs 3.125
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);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->translator()->translation($label) can also be of type Charcoal\Translator\Translation. However, the property $label is declared as type Charcoal\Admin\Translation|null|string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
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);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->translator()->translation($title) can also be of type Charcoal\Translator\Translation. However, the property $title is declared as type Charcoal\Admin\Translation|null|string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
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);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->translator()->translation($subtitle) can also be of type Charcoal\Translator\Translation. However, the property $subtitle is declared as type Charcoal\Admin\Translation|null|string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
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 array|Charcoal\Admin\Generator.
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 Charcoal\Admin\Generator or 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...
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 null|Charcoal\Admin\Widg...daryMenuWidgetInterface.
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 mixed|Charcoal\Config\AbstractConfig 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);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->translator()->translation($name) can also be of type Charcoal\Translator\Translation. However, the property $siteName is declared as type Charcoal\Admin\Translation|null|string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
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
            if ($test->isAuthorized() === false) {
774
                continue;
775
            }
776
            unset($menuItem['menu']);
777
778
            if (isset($menuItem['active']) && $menuItem['active'] === false) {
779
                continue;
780
            }
781
782
            $menuItems[] = $this->parseMainMenuItem($menuItem, $menuIdent, $mainMenuIdent);
783
        }
784
785
        return $menuItems;
786
    }
787
788
    /**
789
     * Determine and retrieve the active main menu item's identifier.
790
     *
791
     * @param  mixed $options The secondary menu widget ID or config.
792
     * @return string|null
793
     */
794
    private function mainMenuIdent($options = null)
795
    {
796
        if ($this->mainMenuIdentLoaded === false) {
797
            $mainMenuIdent = null;
798
799
            if (isset($this['main_menu_item'])) {
800
                $mainMenuIdent = $this['main_menu_item'];
801
            }
802
803
            if (!(empty($options) && !is_numeric($options))) {
804
                if (is_string($options)) {
805
                    $mainMenuIdent = $options;
806
                } elseif (is_array($options)) {
807
                    if (isset($options['widget_options']['ident'])) {
808
                        $mainMenuIdent = $options['widget_options']['ident'];
809
                    }
810
                }
811
            }
812
813
            $mainMenuFromRequest = filter_input(INPUT_GET, 'main_menu', FILTER_SANITIZE_STRING);
814
            if ($mainMenuFromRequest) {
815
                $mainMenuIdent = $mainMenuFromRequest;
816
            }
817
818
            $this->mainMenuIdent = $mainMenuIdent;
819
            $this->mainMenuIdentLoaded = true;
820
        }
821
822
        return $this->mainMenuIdent;
823
    }
824
825
    /**
826
     * @throws InvalidArgumentException If the secondary menu widget is invalid.
827
     * @return \Charcoal\Admin\Widget\SecondaryMenuWidgetInterface[]|
828
     */
829
    protected function createSecondaryMenu()
830
    {
831
        $secondaryMenu = [];
832
        $secondaryMenuItems = $this->adminConfig('secondary_menu');
833
834
        // Get the ident of the active main menu item
835
        $mainMenuIdent = $this->mainMenuIdent();
836
837
        foreach ($secondaryMenuItems as $ident => $options) {
838
            $options['ident'] = $ident;
839
840
            if (isset($this['secondary_menu_item'])) {
841
                $options['current_item'] = $this['secondary_menu_item'];
842
            }
843
844
            if (isset($this['main_menu_item'])) {
845
                $mainMenuIdent = $this['main_menu_item'];
846
            }
847
848
            if (is_string($options['ident'])) {
849
                $options['is_current'] = $options['ident'] === $mainMenuIdent;
850
851
                $widget = $this->widgetFactory()
852
                                ->create('charcoal/admin/widget/secondary-menu')
853
                                ->setData($options);
854
855
                $secondaryMenu[] = $widget;
856
            }
857
        }
858
859
        return $secondaryMenu;
860
    }
861
862
    /**
863
     * @param  mixed $options The secondary menu widget ID or config.
864
     * @throws InvalidArgumentException If the menu is missing, invalid, or malformed.
865
     * @return array|Generator
866
     */
867
    protected function createSystemMenu($options = null)
868
    {
869
        $menuConfig = $this->adminConfig('system_menu');
870
871
        if (!isset($menuConfig['items'])) {
872
            return [];
873
        }
874
875
        $currentIdent = null;
876
        if (isset($this['system_menu_item'])) {
877
            $currentIdent = $this['system_menu_item'];
878
        }
879
880
        if (!(empty($options) && !is_numeric($options))) {
881
            if (is_string($options)) {
882
                $currentIdent = $options;
883
            } elseif (is_array($options)) {
884
                $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

884
                $menuConfig = array_replace_recursive(/** @scrutinizer ignore-type */ $menuConfig, $options);
Loading history...
885
            }
886
        }
887
888
        $systemMenu = $this->menuBuilder->build([]);
889
        $menuItems  = [];
890
        foreach ($menuConfig['items'] as $menuIdent => $menuItem) {
891
            $menuItem['menu'] = $systemMenu;
892
            $test = $this->menuItemBuilder->build($menuItem);
893
            if ($test->isAuthorized() === false) {
894
                continue;
895
            }
896
            unset($menuItem['menu']);
897
898
            if (isset($menuItem['active']) && $menuItem['active'] === false) {
899
                continue;
900
            }
901
902
            $menuItem  = $this->parseSystemMenuItem($menuItem, $menuIdent, $currentIdent);
903
            $menuIdent = $menuItem['ident'];
904
905
            $menuItems[$menuIdent] = $menuItem;
906
        }
907
        return $menuItems;
908
    }
909
910
    /**
911
     * As a convenience, all admin templates have a model factory to easily create objects.
912
     *
913
     * @param FactoryInterface $factory The factory used to create models.
914
     * @return void
915
     */
916
    private function setModelFactory(FactoryInterface $factory)
917
    {
918
        $this->modelFactory = $factory;
919
    }
920
921
    /**
922
     * @param FactoryInterface $factory The widget factory, to create the dashboard and secondary menu widgets.
923
     * @return void
924
     */
925
    private function setWidgetFactory(FactoryInterface $factory)
926
    {
927
        $this->widgetFactory = $factory;
928
    }
929
930
    /**
931
     * @param  array       $menuItem     The menu structure.
932
     * @param  string|null $menuIdent    The menu identifier.
933
     * @param  string|null $currentIdent The current menu identifier.
934
     * @return array Finalized menu structure.
935
     */
936
    private function parseMainMenuItem(array $menuItem, $menuIdent = null, $currentIdent = null)
937
    {
938
        $svgUri = $this->baseUrl().'assets/admin/images/svgs.svg#icon-';
939
940
        if (isset($menuItem['ident'])) {
941
            $menuIdent = $menuItem['ident'];
942
        } else {
943
            $menuItem['ident'] = $menuIdent;
944
        }
945
946
        if (!empty($menuItem['url'])) {
947
            $url = $menuItem['url'];
948
            if ($url && strpos($url, ':') === false && !in_array($url[0], [ '/', '#', '?' ])) {
949
                $url = $this->adminUrl().$url;
950
            }
951
        } else {
952
            $url = '';
953
        }
954
955
        $menuItem['url'] = $url;
956
957
        if (isset($menuItem['icon'])) {
958
            $icon = $menuItem['icon'];
959
            if ($icon && strpos($icon, ':') === false && !in_array($icon[0], [ '/', '#', '?' ])) {
960
                $icon = $svgUri.$icon;
961
            }
962
        } else {
963
            $icon = $svgUri.'contents';
964
        }
965
966
        if (is_string($icon) && strpos($icon, '.svg') > 0) {
967
            unset($menuItem['icon']);
968
            $menuItem['svg'] = $icon;
969
        } else {
970
            unset($menuItem['svg']);
971
            $menuItem['icon'] = $icon;
972
        }
973
974
        if (isset($menuItem['label'])) {
975
            $menuItem['label'] = $this->translator()->translation($menuItem['label']);
976
        }
977
978
        $menuItem['show_label'] = (isset($menuItem['show_label']) ? !!$menuItem['show_label'] : true);
979
980
        $menuItem['selected'] = ($menuItem['ident'] === $currentIdent);
981
982
        $menuItem['hasSecondaryMenuTab'] = false;
983
        $secondaryMenu = $this->adminConfig('secondary_menu');
984
        if (!empty($menuIdent) && isset($secondaryMenu[$menuIdent])) {
985
            /** Extract the secondary menu widget related to this main menu item. */
986
            $secondaryMenuWidget = current(
987
                array_filter(
988
                    $this->secondaryMenu(), function($item) use ($menuIdent) {
989
                        return $item->ident() === $menuIdent;
990
                    }
991
                )
992
            );
993
994
            if (!empty($secondaryMenuWidget)) {
995
                $menuItem['hasSecondaryMenuTab'] = $secondaryMenuWidget->isTabbed() || ($secondaryMenuWidget->hasSecondaryMenu() && $secondaryMenuWidget->isCurrent());
996
            }
997
        }
998
999
        return $menuItem;
1000
    }
1001
1002
    /**
1003
     * @param  array       $menuItem     The menu structure.
1004
     * @param  string|null $menuIdent    The menu identifier.
1005
     * @param  string|null $currentIdent The current menu identifier.
1006
     * @return array Finalized menu structure.
1007
     */
1008
    private function parseSystemMenuItem(array $menuItem, $menuIdent = null, $currentIdent = null)
1009
    {
1010
        if (!isset($menuItem['ident'])) {
1011
            $menuItem['ident'] = $menuIdent;
1012
        }
1013
1014
        if (!empty($menuItem['url'])) {
1015
            $url = $menuItem['url'];
1016
            if ($url && strpos($url, ':') === false && !in_array($url[0], [ '/', '#', '?' ])) {
1017
                $url = $this->adminUrl().$url;
1018
            }
1019
        } else {
1020
            $url = '#';
1021
        }
1022
1023
        $menuItem['url'] = $url;
1024
1025
        if ($menuItem['icon_css']) {
1026
            $menuItem['iconCss'] = $menuItem['icon_css'];
1027
        }
1028
1029
        if (isset($menuItem['label'])) {
1030
            $menuItem['label'] = $this->translator()->translation($menuItem['label']);
1031
        }
1032
1033
        $menuItem['selected'] = ($menuItem['ident'] === $currentIdent);
1034
1035
        return $menuItem;
1036
    }
1037
1038
1039
1040
    // Templating
1041
    // =========================================================================
1042
1043
    /**
1044
     * Generate a string containing HTML attributes for the <html> element.
1045
     *
1046
     * @return string
1047
     */
1048
    public function htmlAttr()
1049
    {
1050
        $attributes = [
1051
            'data-template' => $this->templateName(),
1052
            'data-debug'    => $this->debug() ? 'true' : false,
1053
            'lang'          => $this->lang(),
1054
            'locale'        => $this->locale(),
1055
            'class'         => $this->htmlClasses()
1056
        ];
1057
1058
        return html_build_attributes($attributes);
1059
    }
1060
1061
    /**
1062
     * Generate an array containing a list of CSS classes to be used by the <html> tag.
1063
     *
1064
     * @return array
1065
     */
1066
    public function htmlClasses()
1067
    {
1068
        $classes = [
1069
            'has-no-js'
1070
        ];
1071
1072
        if ($this->isFullscreenTemplate()) {
1073
            $classes[] = 'is-fullscreen-template';
1074
        }
1075
1076
        return $classes;
1077
    }
1078
1079
    /**
1080
     * Determine if main & secondary menu should appear as mobile in a desktop resolution.
1081
     *
1082
     * @return boolean
1083
     */
1084
    public function isFullscreenTemplate()
1085
    {
1086
        return false;
1087
    }
1088
1089
    /**
1090
     * Retrieve the template's identifier.
1091
     *
1092
     * @return string
1093
     */
1094
    public function templateName()
1095
    {
1096
        $key = substr(strrchr('\\'.get_class($this), '\\'), 1);
1097
1098
        if (!isset(static::$templateNameCache[$key])) {
1099
            $value = $key;
1100
1101
            if (!ctype_lower($value)) {
1102
                $value = preg_replace('/\s+/u', '', $value);
1103
                $value = mb_strtolower(preg_replace('/(.)(?=[A-Z])/u', '$1-', $value), 'UTF-8');
1104
            }
1105
1106
            $value = str_replace(
1107
                [ 'abstract', 'trait', 'interface', 'template', '\\' ],
1108
                '',
1109
                $value
1110
            );
1111
1112
            static::$templateNameCache[$key] = trim($value, '-');
1113
        }
1114
1115
        return static::$templateNameCache[$key];
1116
    }
1117
}
1118