Issues (1686)

sources/ElkArte/Menu/Menu.php (1 issue)

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
The property is_guest does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
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