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
|
|||||
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
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. filter:
dependency_paths: ["lib/*"]
For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths ![]() |
|||||
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
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
![]() |
|||||
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
|
|||||
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
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
![]() |
|||||
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
|
|||||
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
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.. ![]() |
|||||
348 | } |
||||
349 | |||||
350 | return $this->mainMenu; |
||||
0 ignored issues
–
show
|
|||||
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
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.. ![]() |
|||||
378 | } |
||||
379 | |||||
380 | return new \ArrayIterator($this->systemMenu); |
||||
0 ignored issues
–
show
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
![]() |
|||||
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
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.. ![]() |
|||||
410 | } |
||||
411 | |||||
412 | return $this->secondaryMenu; |
||||
0 ignored issues
–
show
|
|||||
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
|
|||||
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 — Charcoal', $pageTitle); |
||||
497 | } else { |
||||
498 | return sprintf('%1$s ‹ %2$s — 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
|
|||||
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
|
|||||
706 | $this->menuItemBuilder = $container['menu/item/builder']; |
||||
0 ignored issues
–
show
|
|||||
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
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
![]() |
|||||
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
$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
![]() |
|||||
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 |
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:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths