1 | <?php |
||
2 | |||
3 | /** |
||
4 | * This class contains a standard way of displaying side/drop down menus. |
||
5 | * |
||
6 | * @package ElkArte Forum |
||
7 | * @copyright ElkArte Forum contributors |
||
8 | * @license BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file) |
||
9 | * |
||
10 | * This file contains code covered by: |
||
11 | * copyright: 2011 Simple Machines (http://www.simplemachines.org) |
||
12 | * |
||
13 | * @version 2.0 dev |
||
14 | * |
||
15 | */ |
||
16 | |||
17 | namespace ElkArte\Menu; |
||
18 | |||
19 | use ElkArte\Exceptions\Exception; |
||
20 | use ElkArte\Helper\HttpReq; |
||
21 | use ElkArte\User; |
||
22 | |||
23 | /** |
||
24 | * Class Menu |
||
25 | * |
||
26 | * This class implements a standard way of creating menus |
||
27 | * |
||
28 | * @package ElkArte\Menu |
||
29 | */ |
||
30 | class Menu |
||
31 | { |
||
32 | /** @var HttpReq */ |
||
33 | protected $req; |
||
34 | |||
35 | /** @var array Will hold the created $context */ |
||
36 | public $menuContext = []; |
||
37 | |||
38 | /** @var string Used for profile menu for own / any */ |
||
39 | public $permissionSet; |
||
40 | |||
41 | /** @var bool If we found the menu item selected */ |
||
42 | public $foundSection = false; |
||
43 | |||
44 | /** @var string Current area */ |
||
45 | public $currentArea = ''; |
||
46 | |||
47 | /** @var null|string The current subaction of the system */ |
||
48 | public $currentSubaction = ''; |
||
49 | |||
50 | /** @var array Will hold the selected menu data that is returned to the caller */ |
||
51 | private $includeData = []; |
||
52 | |||
53 | /** @var int Unique menu number */ |
||
54 | private $maxMenuId; |
||
55 | |||
56 | /** @var MenuOptions Holds menu options */ |
||
57 | private $menuOptions; |
||
58 | |||
59 | /** @var array Holds menu definition structure set by addSection */ |
||
60 | private $menuData = []; |
||
61 | |||
62 | /** @var array Holds the first accessible menu section/area if any */ |
||
63 | private $firstAreaCurrent = []; |
||
64 | |||
65 | /** |
||
66 | * Initial processing for the menu |
||
67 | * |
||
68 | * @param HttpReq|null $req |
||
69 | */ |
||
70 | public function __construct($req = null) |
||
71 | { |
||
72 | global $context; |
||
73 | 62 | ||
74 | // Access to post/get data |
||
75 | 62 | $this->req = $req ?: HttpReq::instance(); |
|
76 | |||
77 | // Every menu gets a unique ID, these are shown in first in, first out order. |
||
78 | 62 | $this->maxMenuId = ($context['max_menu_id'] ?? 0) + 1; |
|
79 | |||
80 | // This will be all the data for this menu |
||
81 | 62 | $this->menuContext = []; |
|
82 | |||
83 | // This is necessary only in profile (at least for the core), but we do it always because it's easier |
||
84 | 62 | $this->permissionSet = empty($context['user']['is_owner']) ? 'any' : 'own'; |
|
85 | |||
86 | // We may have a current subaction |
||
87 | 62 | $this->currentSubaction = $context['current_subaction'] ?? null; |
|
88 | |||
89 | // Would you like fries with that? |
||
90 | 62 | $this->menuOptions = new MenuOptions(); |
|
91 | } |
||
92 | |||
93 | 62 | /** |
|
94 | 62 | * Add the base menu options for this menu |
|
95 | * |
||
96 | * @param array $menuOptions an array of options that can be used to override some default |
||
97 | * behaviours. See MenuOptions for details. |
||
98 | */ |
||
99 | public function addOptions(array $menuOptions) |
||
100 | { |
||
101 | $this->menuOptions = MenuOptions::buildFromArray($menuOptions); |
||
102 | 62 | ||
103 | return $this; |
||
104 | 62 | } |
|
105 | 62 | ||
106 | /** |
||
107 | * Parses the supplied menu data in to the relevant menu array structure |
||
108 | * |
||
109 | * @param array $menuData the menu array |
||
110 | */ |
||
111 | public function addMenuData($menuData) |
||
112 | 20 | { |
|
113 | // Process each menu area's section/subsections |
||
114 | foreach ($menuData as $section_id => $section) |
||
115 | 20 | { |
|
116 | // $section['areas'] are the items under a menu button |
||
117 | $newAreas = ['areas' => []]; |
||
118 | 20 | foreach ($section['areas'] as $area_id => $area) |
|
119 | 20 | { |
|
120 | // subsections are deeper menus inside of a area (3rd level menu) |
||
121 | $newSubsections = ['subsections' => []]; |
||
122 | 20 | if (!empty($area['subsections'])) |
|
123 | 20 | { |
|
124 | foreach ($area['subsections'] as $sa => $sub) |
||
125 | 18 | { |
|
126 | $newSubsections['subsections'][$sa] = MenuSubsection::buildFromArray($sub, $sa); |
||
127 | 18 | unset($area['subsections']); |
|
128 | 18 | } |
|
129 | } |
||
130 | |||
131 | $newAreas['areas'][$area_id] = MenuArea::buildFromArray($area + $newSubsections); |
||
132 | 20 | } |
|
133 | |||
134 | // Finally, the menu button |
||
135 | unset($section['areas']); |
||
136 | 20 | $this->addSection($section_id, MenuSection::buildFromArray($section + $newAreas)); |
|
137 | 20 | } |
|
138 | |||
139 | 20 | return $this; |
|
140 | } |
||
141 | |||
142 | /** |
||
143 | * Adds the built out menu sections/subsections to the menu |
||
144 | * |
||
145 | * @param string $id |
||
146 | * @param MenuItem $section |
||
147 | * |
||
148 | * @return $this |
||
149 | 62 | */ |
|
150 | public function addSection($id, $section) |
||
151 | 62 | { |
|
152 | $this->menuData[$id] = $section; |
||
153 | 62 | ||
154 | return $this; |
||
155 | } |
||
156 | |||
157 | /** |
||
158 | * Adds sections/subsections to the existing menu. Generally used by addons via hook |
||
159 | * |
||
160 | * @param array $section_data |
||
161 | * @param string $location optional menu item after which you want to add the section |
||
162 | 60 | * |
|
163 | * @return $this |
||
164 | */ |
||
165 | 60 | public function insertSection($section_data, $location = '') |
|
166 | { |
||
167 | foreach ($section_data as $section_id => $section) |
||
168 | 60 | { |
|
169 | 60 | foreach ($section as $area_id => $area) |
|
170 | 60 | { |
|
171 | 60 | $newSubsections = ['subsections' => []]; |
|
172 | if (!empty($area['subsections'])) |
||
173 | { |
||
174 | 60 | foreach ($area['subsections'] as $sa => $sub) |
|
175 | { |
||
176 | $newSubsections['subsections'][$sa] = MenuSubsection::buildFromArray($sub, $sa); |
||
177 | 60 | } |
|
178 | } |
||
179 | |||
180 | 60 | /** @var MenuSection $section */ |
|
181 | $section = $this->menuData[$section_id]; |
||
182 | $section->insertArea($area_id, $location, MenuArea::buildFromArray($area + $newSubsections)); |
||
183 | 4 | } |
|
184 | } |
||
185 | |||
186 | return $this; |
||
187 | } |
||
188 | |||
189 | 4 | /** |
|
190 | * Create a menu. Expects that addOptions and addMenuData (or equivalent) have been called |
||
191 | * |
||
192 | * @throws Exception |
||
193 | */ |
||
194 | 56 | public function prepareMenu() |
|
195 | 56 | { |
|
196 | // If options set a hook, give it call |
||
197 | $this->callHook(); |
||
198 | 56 | ||
199 | 56 | // Build URLs first. |
|
200 | 56 | $this->menuContext['base_url'] = $this->menuOptions->getBaseUrl(); |
|
201 | 56 | $this->menuContext['current_action'] = $this->menuOptions->getAction(); |
|
202 | 56 | $this->currentArea = empty($this->menuOptions->getCurrentArea()) ? $this->req->getQuery('area', 'trim|strval', '') : $this->menuOptions->getCurrentArea(); |
|
203 | $this->menuContext['extra_parameters'] = $this->menuOptions->buildAdditionalParams(); |
||
204 | |||
205 | // Process the loopy menu data. |
||
206 | $this->processMenuData(); |
||
207 | |||
208 | // Here is some activity. |
||
209 | 60 | $this->setActiveButtons(); |
|
210 | |||
211 | // Make sure we created some awesome sauce. |
||
212 | 60 | if (empty($this->includeData)) |
|
213 | { |
||
214 | 20 | // Give a guest a boot to the login screen |
|
215 | if (User::$info->is_guest) |
||
0 ignored issues
–
show
Bug
Best Practice
introduced
by
![]() |
|||
216 | 60 | { |
|
217 | is_not_guest(); |
||
218 | } |
||
219 | |||
220 | // Users get a slap in the face, No valid areas -- reject! |
||
221 | throw new Exception('no_access', false); |
||
222 | } |
||
223 | 60 | ||
224 | // For consistency with the past, clean up the returned array |
||
225 | $this->includeData = array_filter($this->includeData, static fn($value) => !is_null($value) && $value !== ''); |
||
226 | 60 | ||
227 | // Set information on the selected item. |
||
228 | $this->includeData += [ |
||
229 | 58 | 'current_action' => $this->menuContext['current_action'], |
|
230 | 'current_area' => $this->currentArea, |
||
231 | 56 | 'current_section' => empty($this->menuContext['current_section']) ? '' : $this->menuContext['current_section'], |
|
232 | 'current_subsection' => $this->currentSubaction, |
||
233 | ]; |
||
234 | 57 | ||
235 | return $this; |
||
236 | } |
||
237 | |||
238 | /** |
||
239 | 60 | * Return the computed include data array |
|
240 | * |
||
241 | 34 | * @return array |
|
242 | */ |
||
243 | 60 | public function getIncludeData() |
|
244 | { |
||
245 | return $this->includeData; |
||
246 | } |
||
247 | |||
248 | /** |
||
249 | * Allow extending *any* menu with a single hook |
||
250 | * |
||
251 | * - Call hook name defined in options as integrate_supplied name_areas |
||
252 | * - example, integrate_profile_areas, integrate_admin_areas |
||
253 | * - Hooks are passed $this |
||
254 | */ |
||
255 | public function callHook() |
||
256 | { |
||
257 | // Allow to extend *any* menu with a single hook |
||
258 | if ($this->menuOptions->getHook()) |
||
259 | 58 | { |
|
260 | call_integration_hook($this->menuOptions->getHook(), [$this]); |
||
261 | 58 | } |
|
262 | } |
||
263 | |||
264 | 52 | /** |
|
265 | * Process the menuData array passed to the class |
||
266 | * |
||
267 | * - Only processes areas that are enabled and that the user has permissions |
||
268 | */ |
||
269 | 52 | protected function processMenuData() |
|
270 | { |
||
271 | // Now setup the context correctly. |
||
272 | 56 | foreach ($this->menuData as $sectionId => $section) |
|
273 | { |
||
274 | // Is this section enabled? and do they have permissions? |
||
275 | if ($section->isEnabled() && $this->checkPermissions($section)) |
||
276 | { |
||
277 | $this->setSectionContext($sectionId, $section); |
||
278 | |||
279 | // Process this menu section |
||
280 | $this->processSectionAreas($sectionId, $section); |
||
281 | |||
282 | // Validate is created *something* |
||
283 | $this->validateSection($sectionId); |
||
284 | } |
||
285 | 56 | } |
|
286 | |||
287 | 56 | // If we did not find a current area, then use the first valid one found. |
|
288 | if ($this->foundSection) |
||
289 | 56 | { |
|
290 | 56 | return; |
|
291 | 56 | } |
|
292 | 56 | ||
293 | if (empty($this->firstAreaCurrent)) |
||
294 | 56 | { |
|
295 | return; |
||
296 | } |
||
297 | |||
298 | $this->setAreaCurrent($this->firstAreaCurrent[0], $this->firstAreaCurrent[1], $this->firstAreaCurrent[2]); |
||
299 | } |
||
300 | |||
301 | /** |
||
302 | * Removes a generated section that has no areas and no URL, aka empty. This can happen |
||
303 | * due to conflicting permissions. |
||
304 | 56 | * |
|
305 | * @param string $sectionId |
||
306 | 56 | */ |
|
307 | private function validateSection($sectionId) |
||
308 | 56 | { |
|
309 | 56 | if (!empty($this->menuContext['sections'][$sectionId]['areas'])) |
|
310 | { |
||
311 | 2 | return; |
|
312 | 2 | } |
|
313 | 2 | ||
314 | if (!empty($this->menuContext['sections'][$sectionId]['url'])) |
||
315 | { |
||
316 | return; |
||
317 | 56 | } |
|
318 | |||
319 | unset($this->menuContext['sections'][$sectionId]); |
||
320 | } |
||
321 | |||
322 | /** |
||
323 | * Determines if the user has the permissions to access the section/area |
||
324 | * |
||
325 | * If said item did not provide any permission to check, fully |
||
326 | * unfettered access is assumed. |
||
327 | 56 | * |
|
328 | * The profile areas are a bit different in that each permission is |
||
329 | * divided into two sets: "own" for owner and "any" for everyone else. |
||
330 | 56 | * |
|
331 | * @param MenuItem $obj area or section being checked |
||
332 | * |
||
333 | 56 | * @return bool |
|
334 | */ |
||
335 | private function checkPermissions($obj) |
||
336 | 56 | { |
|
337 | if (!empty($obj->getPermission())) |
||
338 | { |
||
339 | 56 | // The profile menu has slightly different permissions |
|
340 | if (isset($obj->getPermission()['own'], $obj->getPermission()['any'])) |
||
341 | { |
||
342 | 56 | return !empty($obj->getPermission()[$this->permissionSet]) && allowedTo($obj->getPermission()[$this->permissionSet]); |
|
343 | } |
||
344 | |||
345 | 56 | return allowedTo($obj->getPermission()); |
|
346 | } |
||
347 | |||
348 | 56 | return true; |
|
349 | } |
||
350 | |||
351 | 56 | /** |
|
352 | * Sets the various section ID items |
||
353 | * |
||
354 | * What it does: |
||
355 | 56 | * - If the ID is not set, sets it and sets the section title |
|
356 | * - Sets the section title |
||
357 | * |
||
358 | * @param string $sectionId |
||
359 | * @param MenuSection $section |
||
360 | 56 | */ |
|
361 | 56 | private function setSectionContext($sectionId, $section) |
|
362 | { |
||
363 | global $txt; |
||
364 | |||
365 | $this->menuContext['sections'][$sectionId] = [ |
||
366 | 'id' => $sectionId, |
||
367 | 'label' => ($section->getLabel() ?: $txt[$sectionId]) . $this->parseCounter($section, 0), |
||
368 | 'url' => '', |
||
369 | ]; |
||
370 | } |
||
371 | 56 | ||
372 | /** |
||
373 | 56 | * If the menu has that little notification count, that's right this sets its value |
|
374 | * |
||
375 | 56 | * @param MenuItem $obj |
|
376 | * @param int $idx |
||
377 | * |
||
378 | * @return string |
||
379 | */ |
||
380 | private function parseCounter($obj, $idx) |
||
381 | { |
||
382 | global $settings; |
||
383 | |||
384 | if (!empty($this->menuOptions->getCounters()[$obj->getCounter()])) |
||
385 | 56 | { |
|
386 | return sprintf( |
||
387 | $settings['menu_numeric_notice'][$idx], |
||
388 | 56 | $this->menuOptions->getCounters()[$obj->getCounter()] |
|
389 | ); |
||
390 | 56 | } |
|
391 | |||
392 | 56 | return ''; |
|
393 | } |
||
394 | |||
395 | /** |
||
396 | * Main processing for creating the menu items for all sections |
||
397 | * |
||
398 | * @param string $sectionId |
||
399 | * @param MenuSection $section |
||
400 | */ |
||
401 | 56 | protected function processSectionAreas($sectionId, $section) |
|
402 | { |
||
403 | // Now we cycle through the sections to pick the right area. |
||
404 | 56 | foreach ($section->getAreas() as $areaId => $area) |
|
405 | 56 | { |
|
406 | 56 | // Is the area enabled, Does the user have permission and it has some form of a name |
|
407 | 56 | if ($area->isEnabled() && $this->checkPermissions($area) && $this->areaHasLabel($areaId, $area)) |
|
408 | { |
||
409 | // Make sure we have a valid current area |
||
410 | $this->setFirstAreaCurrent($sectionId, $areaId, $area); |
||
411 | |||
412 | // If this is hidden from view don't do the rest. |
||
413 | if (!$area->isHidden()) |
||
414 | { |
||
415 | // First time this section? |
||
416 | $this->setAreaContext($sectionId, $areaId, $area); |
||
417 | |||
418 | // Maybe a custom url |
||
419 | $this->setAreaUrl($sectionId, $areaId, $area); |
||
420 | 56 | ||
421 | // Even a little icon |
||
422 | 56 | $this->setAreaIcon($sectionId, $areaId, $area); |
|
423 | |||
424 | 56 | // Did it have subsections? |
|
425 | 56 | $this->processAreaSubsections($sectionId, $areaId, $area); |
|
426 | } |
||
427 | 56 | ||
428 | // Is this the current section? |
||
429 | $this->checkCurrentSection($sectionId, $areaId, $area); |
||
430 | } |
||
431 | } |
||
432 | |||
433 | // Now that we have valid section areas, set the section url |
||
434 | $this->setSectionUrl($sectionId); |
||
435 | } |
||
436 | 56 | ||
437 | /** |
||
438 | 56 | * Checks if the area has a label or not |
|
439 | 56 | * |
|
440 | 56 | * @param string $areaId |
|
441 | * @param MenuArea $area |
||
442 | 56 | * |
|
443 | * @return bool |
||
444 | */ |
||
445 | private function areaHasLabel($areaId, $area) |
||
446 | { |
||
447 | global $txt; |
||
448 | |||
449 | return !empty($area->getLabel()) || isset($txt[$areaId]); |
||
450 | } |
||
451 | 56 | ||
452 | /** |
||
453 | 56 | * Set the current area, or pick it for them |
|
454 | * |
||
455 | * @param string $sectionId |
||
456 | 56 | * @param string $areaId |
|
457 | 56 | * @param MenuArea $area |
|
458 | 56 | */ |
|
459 | private function setFirstAreaCurrent($sectionId, $areaId, $area) |
||
460 | { |
||
461 | 56 | // If an area was not directly specified, or wrongly specified, this first valid one is our choice. |
|
462 | if (empty($this->firstAreaCurrent)) |
||
463 | 18 | { |
|
464 | 18 | $this->firstAreaCurrent = [$sectionId, $areaId, $area]; |
|
465 | } |
||
466 | } |
||
467 | |||
468 | 46 | /** |
|
469 | * Simply sets the current area |
||
470 | 56 | * |
|
471 | * @param string $sectionId |
||
472 | * @param string $areaId |
||
473 | * @param MenuArea $area |
||
474 | */ |
||
475 | private function setAreaCurrent($sectionId, $areaId, $area) |
||
476 | { |
||
477 | // Update the context if required - as we can have areas pretending to be others. ;) |
||
478 | $this->menuContext['current_section'] = $sectionId; |
||
479 | 56 | $this->currentArea = $area->getSelect() ?: $areaId; |
|
480 | $this->includeData = $area->toArray($area); |
||
481 | 56 | } |
|
482 | |||
483 | /** |
||
484 | 56 | * Sets the various area items |
|
485 | 56 | * |
|
486 | * What it does: |
||
487 | 54 | * - If the ID is not set, sets it and sets the area title |
|
488 | 56 | * - Sets the area title |
|
489 | * |
||
490 | * @param string $sectionId |
||
491 | * @param string $areaId |
||
492 | 56 | * @param MenuArea $area |
|
493 | */ |
||
494 | 54 | private function setAreaContext($sectionId, $areaId, $area) |
|
495 | 54 | { |
|
496 | global $txt; |
||
497 | |||
498 | 54 | $this->menuContext['sections'][$sectionId]['areas'][$areaId] = [ |
|
499 | 'label' => ($area->getLabel() ?: $txt[$areaId]) . $this->parseCounter($area, 1), |
||
500 | 54 | ]; |
|
501 | } |
||
502 | 33 | ||
503 | /** |
||
504 | * Set the URL for the menu item |
||
505 | * |
||
506 | 56 | * @param string $sectionId |
|
507 | 56 | * @param string $areaId |
|
508 | * @param MenuArea $area |
||
509 | */ |
||
510 | private function setAreaUrl($sectionId, $areaId, $area) |
||
511 | { |
||
512 | $area->setUrl( |
||
513 | $this->menuContext['sections'][$sectionId]['areas'][$areaId]['url'] = |
||
514 | ($area->getUrl() ?: $this->menuContext['base_url'] . ';area=' . $areaId) . $this->menuContext['extra_parameters'] |
||
515 | 54 | ); |
|
516 | } |
||
517 | 54 | ||
518 | 54 | /** |
|
519 | 54 | * Set the menu icon from a class name if using pseudo elements |
|
520 | * of class and icon if using that method |
||
521 | 54 | * |
|
522 | * @param string $sectionId |
||
523 | * @param string $areaId |
||
524 | * @param MenuArea $area |
||
525 | */ |
||
526 | private function setAreaIcon($sectionId, $areaId, $area) |
||
527 | { |
||
528 | global $settings; |
||
529 | 12 | ||
530 | // Work out where we should get our menu images from. |
||
531 | $imagePath = file_exists($settings['theme_dir'] . '/images/admin/transparent.png') |
||
532 | 12 | ? $settings['images_url'] . '/admin' |
|
533 | 12 | : $settings['default_images_url'] . '/admin'; |
|
534 | 12 | ||
535 | 12 | // Does this area even have an icon? |
|
536 | if (empty($area->getIcon()) && empty($area->getClass())) |
||
537 | { |
||
538 | 6 | $this->menuContext['sections'][$sectionId]['areas'][$areaId]['icon'] = ''; |
|
539 | return; |
||
540 | 12 | } |
|
541 | |||
542 | // Perhaps a png |
||
543 | if (!empty($area->getIcon())) |
||
544 | { |
||
545 | $this->menuContext['sections'][$sectionId]['areas'][$areaId]['icon'] = |
||
546 | '<img ' . (empty($area->getClass()) ? 'style="background: none"' : 'class="' . $area->getClass() . '"') . ' src="' . $imagePath . '/' . $area->getIcon() . '" alt="" />'; |
||
547 | return; |
||
548 | 56 | } |
|
549 | |||
550 | 56 | $this->menuContext['sections'][$sectionId]['areas'][$areaId]['icon'] = |
|
551 | '<i class="' . (empty($area->getClass()) ? '' : 'icon ' . $area->getClass() . '"') . '></i>'; |
||
552 | 18 | } |
|
553 | |||
554 | 56 | /** |
|
555 | * Processes all subsections for a menu item |
||
556 | * |
||
557 | * @param string $sectionId |
||
558 | * @param string $areaId |
||
559 | * @param MenuArea $area |
||
560 | */ |
||
561 | protected function processAreaSubsections($sectionId, $areaId, $area) |
||
562 | { |
||
563 | 56 | $this->menuContext['sections'][$sectionId]['areas'][$areaId]['subsections'] = []; |
|
564 | |||
565 | // Clear out ones not enabled or accessible |
||
566 | 56 | $subSections = array_filter( |
|
567 | $area->getSubsections(), |
||
568 | 24 | fn($sub) => $sub->isEnabled() && $this->checkPermissions($sub) |
|
569 | ); |
||
570 | |||
571 | 24 | // For each subsection process the options |
|
572 | foreach ($subSections as $subId => $sub) |
||
573 | 56 | { |
|
574 | $this->menuContext['sections'][$sectionId]['areas'][$areaId]['subsections'][$subId] = [ |
||
575 | 'label' => $sub->getLabel() . $this->parseCounter($sub, 2), |
||
576 | ]; |
||
577 | |||
578 | $this->setSubsSectionUrl($sectionId, $areaId, $subId, $sub); |
||
579 | |||
580 | if ($this->currentArea === $areaId) |
||
581 | 56 | { |
|
582 | $this->setCurrentSubSection($subId, $sub); |
||
583 | 56 | } |
|
584 | } |
||
585 | 56 | ||
586 | $this->setDefaultSubSection($areaId, $subSections); |
||
587 | 56 | } |
|
588 | 56 | ||
589 | /** |
||
590 | 56 | * Set subsection url/click location |
|
591 | * |
||
592 | * @param string $sectionId |
||
593 | * @param string $areaId |
||
594 | * @param string $subId |
||
595 | 60 | * @param MenuSubsection $sub |
|
596 | */ |
||
597 | private function setSubsSectionUrl($sectionId, $areaId, $subId, $sub) |
||
598 | 60 | { |
|
599 | $sub->setUrl( |
||
600 | 56 | $this->menuContext['sections'][$sectionId]['areas'][$areaId]['subsections'][$subId]['url'] = |
|
601 | 56 | $sub->getUrl() ?: $this->menuContext['base_url'] . ';area=' . $areaId . ';sa=' . $subId . $this->menuContext['extra_parameters'] |
|
602 | ); |
||
603 | 56 | } |
|
604 | |||
605 | 12 | /** |
|
606 | * Set the current subsection |
||
607 | * |
||
608 | 60 | * @param string $subId |
|
609 | * @param MenuSubsection $sub |
||
610 | */ |
||
611 | private function setCurrentSubSection($subId, $sub) |
||
612 | { |
||
613 | // Is this the current subsection? |
||
614 | $subIdCheck = $this->req->getQuery('sa', 'trim', null); |
||
615 | if ($subIdCheck === $subId |
||
616 | || (empty($this->currentSubaction) && $sub->isDefault()) |
||
617 | || in_array($subIdCheck, $sub->getActive(), true) |
||
618 | 56 | ) |
|
619 | { |
||
620 | 56 | $this->currentSubaction = $subId; |
|
621 | } |
||
622 | } |
||
623 | 56 | ||
624 | 56 | /** |
|
625 | * Ensures that the current subsection is set. |
||
626 | * |
||
627 | 56 | * @param string $areaId |
|
628 | 56 | * @param array $subSections |
|
629 | 56 | */ |
|
630 | 56 | private function setDefaultSubSection($areaId, $subSections) |
|
631 | 56 | { |
|
632 | 56 | if ($this->currentArea !== $areaId) |
|
633 | 56 | { |
|
634 | 56 | return; |
|
635 | 56 | } |
|
636 | |||
637 | if (!empty($this->currentSubaction)) |
||
638 | { |
||
639 | return; |
||
640 | } |
||
641 | |||
642 | $this->currentSubaction = key($subSections) ?? ''; |
||
643 | } |
||
644 | |||
645 | /** |
||
646 | * Checks the menu item to see if it is the one specified |
||
647 | 6 | * |
|
648 | * @param string $sectionId |
||
649 | 6 | * @param string $areaId |
|
650 | * @param MenuArea $area |
||
651 | */ |
||
652 | 6 | private function checkCurrentSection($sectionId, $areaId, $area) |
|
653 | 6 | { |
|
654 | // Is this the current selection? |
||
655 | if ($this->currentArea === $areaId && !$this->foundSection) |
||
656 | 6 | { |
|
657 | $this->setAreaCurrent($sectionId, $areaId, $area); |
||
658 | 6 | ||
659 | // Only do this once, m'kay? |
||
660 | $this->foundSection = true; |
||
661 | } |
||
662 | 6 | } |
|
663 | |||
664 | 6 | /** |
|
665 | 6 | * The top level section gets its url from the first valid area under it. Its |
|
666 | 6 | * done here to avoid setting it to an invalid area. |
|
667 | * |
||
668 | * @param string $sectionId |
||
669 | */ |
||
670 | 6 | private function setSectionUrl($sectionId) |
|
671 | { |
||
672 | 4 | if (!empty($this->menuContext['sections'][$sectionId]['areas'])) |
|
673 | { |
||
674 | $firstAreaId = key($this->menuContext['sections'][$sectionId]['areas']); |
||
675 | 6 | ||
676 | $this->menuContext['sections'][$sectionId]['url'] = |
||
677 | $this->menuContext['sections'][$sectionId]['areas'][$firstAreaId]['url']; |
||
678 | } |
||
679 | } |
||
680 | |||
681 | /** |
||
682 | * Checks and updates base and section urls |
||
683 | */ |
||
684 | private function setActiveButtons() |
||
685 | 42 | { |
|
686 | // If there are sections quickly goes through all the sections to check if the base menu has an url |
||
687 | 42 | if (!empty($this->menuContext['current_section'])) |
|
688 | { |
||
689 | $this->menuContext['sections'][$this->menuContext['current_section']]['selected'] = true; |
||
690 | 42 | $this->menuContext['sections'][$this->menuContext['current_section']]['areas'][$this->currentArea]['selected'] = true; |
|
691 | |||
692 | if (!empty($this->menuContext['sections'][$this->menuContext['current_section']]['areas'][$this->currentArea]['subsections'][$this->currentSubaction])) |
||
693 | 36 | { |
|
694 | $this->menuContext['sections'][$this->menuContext['current_section']]['areas'][$this->currentArea]['subsections'][$this->currentSubaction]['selected'] = true; |
||
695 | 36 | } |
|
696 | } |
||
697 | } |
||
698 | |||
699 | 36 | /** |
|
700 | * Finalizes items so the computed menu can be used |
||
701 | 36 | * |
|
702 | * What it does: |
||
703 | * - Sets the menu layer in the template stack |
||
704 | 36 | * - Loads context with the computed menu context |
|
705 | * - Sets current subaction and current max menu id |
||
706 | 42 | */ |
|
707 | public function setContext() |
||
708 | { |
||
709 | global $context; |
||
710 | |||
711 | // Almost there - load the template and add to the template layers. |
||
712 | theme()->getTemplates()->load($this->menuOptions->getTemplateName()); |
||
713 | theme()->getLayers()->add($this->menuOptions->getLayerName()); |
||
714 | |||
715 | // Set it all to context for template consumption |
||
716 | $this->menuContext['layer_name'] = $this->menuOptions->getLayerName(); |
||
717 | $this->menuContext['can_toggle_drop_down'] = $this->menuOptions->isDropDownToggleable(); |
||
718 | |||
719 | // Keep track of where we are |
||
720 | $this->menuContext['current_area'] = $this->currentArea; |
||
721 | $context['current_subaction'] = $this->currentSubaction; |
||
722 | $this->menuContext['current_subsection'] = $this->currentSubaction; |
||
723 | |||
724 | // Make a note of the Unique ID for this menu. |
||
725 | $context['max_menu_id'] = $this->maxMenuId; |
||
726 | $context['menu_data_' . $this->maxMenuId] = $this->menuContext; |
||
727 | $context['menu_data_' . $this->maxMenuId]['object'] = $this; |
||
728 | |||
729 | return $this; |
||
730 | } |
||
731 | |||
732 | /** |
||
733 | * Prepares tabs for the template, specifically for template_generic_menu_tabs |
||
734 | * |
||
735 | * This should be called after the menu area is dispatched, because areas are usually in their |
||
736 | * own controller file. Those files, once dispatched to, hold data for the tabs (descriptions, |
||
737 | * disabled, extra tabs, etc), which must be combined with subaction data for everything to work properly. |
||
738 | * |
||
739 | * Seems complicated, yes. |
||
740 | * |
||
741 | * @param array $tabArray named key array holding details on how to build a tab area |
||
742 | */ |
||
743 | public function prepareTabData($tabArray = []) |
||
744 | { |
||
745 | global $context; |
||
746 | |||
747 | $tabBuilder = new MenuTabs($this->menuContext['current_area'] ?? '', $this->currentSubaction ?? ''); |
||
748 | |||
749 | $currentArea = ''; |
||
750 | if (!empty($this->menuContext['sections'])) |
||
751 | { |
||
752 | $currentArea = $this->menuContext['sections'][$this->menuContext['current_section']]['areas'][$this->currentArea]; |
||
753 | } |
||
754 | |||
755 | // Build out the tab title/description area |
||
756 | $tabBuilder |
||
757 | ->setDescription($tabArray['description'] ?? '') |
||
758 | ->setTitle($tabArray['title'] ?? '') |
||
759 | ->setPrefix($tabArray['prefix'] ?? '') |
||
760 | ->setClass($tabArray['class'] ?? null) |
||
761 | ->setHelp($tabArray['help'] ?? null); |
||
762 | $tabContext = $tabBuilder->setHeader(); |
||
763 | |||
764 | // Nothing supplied, then subsections of the current area are used as tabs. |
||
765 | if (!isset($tabArray['tabs']) && isset($currentArea['subsections'])) |
||
766 | { |
||
767 | $tabContext['tabs'] = $tabBuilder->getTabs($currentArea); |
||
768 | } |
||
769 | // Tabs specified with area subsections, combine them |
||
770 | elseif (isset($tabArray['tabs'], $currentArea['subsections'])) |
||
771 | { |
||
772 | // Tabs are really just subactions. |
||
773 | $tabContext['tabs'] = array_replace_recursive( |
||
774 | $tabBuilder->getTabs($currentArea), |
||
775 | $tabArray['tabs'] |
||
776 | ); |
||
777 | } |
||
778 | // Custom loading tabs |
||
779 | else |
||
780 | { |
||
781 | $tabContext['tabs'] = $tabArray['tabs'] ?? []; |
||
782 | } |
||
783 | |||
784 | // Drop any non-enabled ones |
||
785 | $tabContext['tabs'] = array_filter($tabContext['tabs'], static fn($tab) => !isset($tab['disabled']) || $tab['disabled'] === false); |
||
786 | |||
787 | // Has it been deemed selected? |
||
788 | if (isset($tabContext['tabs'][$this->currentSubaction])) |
||
789 | { |
||
790 | $tabContext = array_merge($tabContext, $tabContext['tabs'][$this->currentSubaction]); |
||
791 | } |
||
792 | |||
793 | $context['menu_data_' . $this->maxMenuId]['tab_data'] = $tabContext; |
||
794 | } |
||
795 | |||
796 | /** |
||
797 | * Delete a menu. |
||
798 | * |
||
799 | * Checks to see if this menu been loaded into context |
||
800 | * and, if so, resets $context['max_menu_id'] back to the |
||
801 | * last known menu (if any) and remove the template layer |
||
802 | * if there aren't any other known menus. |
||
803 | */ |
||
804 | public function destroy() |
||
805 | { |
||
806 | global $context; |
||
807 | |||
808 | // Has this menu been loaded into context? |
||
809 | if (isset($context[$menuName = 'menu_data_' . $this->maxMenuId])) |
||
810 | { |
||
811 | // Decrement the pointer if this is the final menu in the series. |
||
812 | if ($this->maxMenuId === $context['max_menu_id']) |
||
813 | { |
||
814 | $context['max_menu_id'] = max($context['max_menu_id'] - 1, 0); |
||
815 | } |
||
816 | |||
817 | // Remove the template layer if this was the only menu left. |
||
818 | if ($context['max_menu_id'] === 0) |
||
819 | { |
||
820 | theme()->getLayers()->remove($context[$menuName]['layer_name']); |
||
821 | } |
||
822 | |||
823 | unset($context[$menuName]); |
||
824 | } |
||
825 | } |
||
826 | } |
||
827 |