Completed
Pull Request — development (#3050)
by John
09:17
created

Menu::setSectionContext()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 2
dl 0
loc 10
ccs 7
cts 7
cp 1
crap 2
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * This class contains a standard way of displaying side/drop down menus.
5
 *
6
 * @name      ElkArte Forum
7
 * @copyright ElkArte Forum contributors
8
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause
9
 *
10
 * This file contains code covered by:
11
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
12
 * license:   BSD, See included LICENSE.TXT for terms and conditions.
13
 *
14
 * @version   2.0 dev
15
 */
16
17
declare(strict_types=1);
18
19
namespace ElkArte\Menu;
20
21
use Elk_Exception;
22
use HttpReq;
23
24
/**
25
 * Class Menu
26
 *
27
 * This class implements a standard way of creating menus
28
 *
29
 * @package ElkArte\Menu
30
 */
31
class Menu
32
{
33
	/** @var HttpReq */
34
	protected $req;
35
36
	/** @var array Will hold the created $context */
37
	protected $menuContext = [];
38
39
	/** @var string Used for profile menu for own / any */
40
	protected $permissionSet;
41
42
	/** @var bool If we found the menu item selected */
43
	protected $foundSection = false;
44
45
	/** @var string Current area */
46
	protected $currentArea = '';
47
48
	/** @var null|string The current subaction of the system */
49
	protected $currentSubaction = '';
50
51
	/** @var array Will hold the selected menu data that is returned to the caller */
52
	private $includeData = [];
53
54
	/** @var int Unique menu number */
55
	private $maxMenuId = 0;
56
57
	/** @var MenuOptions  Holds menu options */
58
	private $menuOptions;
59
60
	/** @var array  Holds menu definition structure set by addSection */
61
	private $menuData = [];
62
63
	/**
64
	 * Initial processing for the menu
65
	 *
66
	 * @param HttpReq|null $req
67
	 */
68 41
	public function __construct(HttpReq $req = null)
69
	{
70 41
		global $context;
71
72
		// Access to post/get data
73 41
		$this->req = $req ?: HttpReq::instance();
74
75
		// Every menu gets a unique ID, these are shown in first in, first out order.
76 41
		$this->maxMenuId = ($context['max_menu_id'] ?? 0) + 1;
77
78
		// This will be all the data for this menu
79 41
		$this->menuContext = [];
80
81
		// This is necessary only in profile (at least for the core), but we do it always because it's easier
82 41
		$this->permissionSet = !empty($context['user']['is_owner']) ? 'own' : 'any';
83
84
		// We may have a current subaction
85 41
		$this->currentSubaction = $context['current_subaction'] ?? null;
86
87
		// Would you like fries with that?
88 41
		$this->menuOptions = new MenuOptions;
89 41
	}
90
91
	/**
92
	 * Create a menu
93
	 *
94
	 * @return array
95
	 * @throws Elk_Exception
96
	 */
97 40
	public function prepareMenu(): array
98
	{
99
		// Build URLs first.
100 40
		$this->menuContext['base_url'] = $this->menuOptions->getBaseUrl();
101 40
		$this->menuContext['current_action'] = $this->menuOptions->getAction();
102 40
		$this->currentArea = $this->req->getQuery('area', 'trim|strval', $this->menuOptions->getArea());
103 40
		$this->menuContext['extra_parameters'] = $this->menuOptions->buildAdditionalParams();
104
105
		// Process the loopy menu data.
106 40
		$this->processMenuData();
107
108
		// Here is some activity.
109 40
		$this->setActiveButtons();
110
111
		// Make sure we created some awesome sauce.
112 40
		if (empty($this->includeData))
113
		{
114
			// No valid areas -- reject!
115 2
			throw new Elk_Exception('no_access');
116
		}
117
118
		// Finally - return information on the selected item.
119 38
		return $this->includeData + [
120 38
				'current_action' => $this->menuContext['current_action'],
121 38
				'current_area' => $this->currentArea,
122 38
				'current_section' => !empty($this->menuContext['current_section']) ? $this->menuContext['current_section'] : '',
123 38
				'current_subsection' => $this->currentSubaction,
124
			];
125
	}
126
127
	/**
128
	 * Prepares tabs for the template.
129
	 *
130
	 * This should be called after the area is dispatched, because areas
131
	 * are usually in their own file. Those files, once dispatched to, hold
132
	 * some data for the tabs which must be specially combined with subaction
133
	 * data for everything to work properly.
134
	 *
135
	 * Seems complicated, yes.
136
	 */
137 1
	public function prepareTabData(): void
138
	{
139 1
		global $context;
140
141
		// Handy shortcut.
142 1
		$tabContext = &$context['menu_data_' . $this->maxMenuId]['tab_data'];
143
144
		// Tabs are really just subactions.
145 1
		if (isset($tabContext['tabs'], $this->menuContext['sections'][$this->menuContext['current_section']]['areas'][$this->currentArea]['subsections']))
146
		{
147
			$tabContext['tabs'] = array_replace_recursive(
148
				$tabContext['tabs'],
149
				$this->menuContext['sections'][$this->menuContext['current_section']]['areas'][$this->currentArea]['subsections']
150
			);
151
152
			// Has it been deemed selected?
153
			$tabContext = array_merge($tabContext, $tabContext['tabs'][$this->currentSubaction]);
154
		}
155 1
	}
156
157
	/**
158
	 * Process the menuData array passed to the class
159
	 *
160
	 *   - Only processes areas that are enabled and that the user has permissions
161
	 */
162 40
	protected function processMenuData(): void
163
	{
164
		// Now setup the context correctly.
165 40
		foreach ($this->menuData as $sectionId => $section)
166
		{
167
			// Is this section enabled? and do they have permissions?
168 39
			if ($section->isEnabled() && $this->checkPermissions($section))
169
			{
170 38
				$this->setSectionContext($sectionId, $section);
171
172
				// Process this menu section
173 39
				$this->processSectionAreas($sectionId, $section);
174
			}
175
		}
176 40
	}
177
178
	/**
179
	 * Determines if the user has the permissions to access the section/area
180
	 *
181
	 * If said item did not provide any permission to check, fullly
182
	 * unfettered access is assumed.
183
	 *
184
	 * The profile areas are a bit different in that each permission is
185
	 * divided into two sets: "own" for owner and "any" for everyone else.
186
	 *
187
	 * @param MenuItem $obj area or section being checked
188
	 *
189
	 * @return bool
190
	 */
191 39
	private function checkPermissions(MenuItem $obj): bool
192
	{
193 39
		if (!empty($obj->getPermission()))
194
		{
195
			// The profile menu has slightly different permissions
196 39
			if (isset($obj->getPermission()['own'], $obj->getPermission()['any']))
197
			{
198 1
				return allowedTo($obj->getPermission()[$this->permissionSet]);
199
			}
200
201 39
			return allowedTo($obj->getPermission());
202
		}
203
204 38
		return true;
205
	}
206
207
	/**
208
	 * Checks if the area has a label or not
209
	 *
210
	 * @param string   $areaId
211
	 * @param MenuArea $area
212
	 *
213
	 * @return bool
214
	 */
215 38
	private function areaHasLabel(string $areaId, MenuArea $area): bool
216
	{
217 38
		global $txt;
218
219 38
		return !empty($area->getLabel()) || isset($txt[$areaId]);
220
	}
221
222
	/**
223
	 * Main processing for creating the menu items for all sections
224
	 *
225
	 * @param string      $sectionId
226
	 * @param MenuSection $section
227
	 */
228 38
	protected function processSectionAreas(string $sectionId, MenuSection $section): void
229
	{
230
		// Now we cycle through the sections to pick the right area.
231 38
		foreach ($section->getAreas() as $areaId => $area)
232
		{
233
			// Is the area enabled, Does the user have permission and it has some form of a name
234 38
			if ($area->isEnabled() && $this->checkPermissions($area) && $this->areaHasLabel($areaId, $area))
235
			{
236
				// Make sure we have a valid current area
237 38
				$this->setFirstAreaCurrent($sectionId, $areaId, $area);
238
239
				// If this is hidden from view don't do the rest.
240 38
				if (!$area->isHidden())
241
				{
242
					// First time this section?
243 38
					$this->setAreaContext($sectionId, $areaId, $area);
244
245
					// Maybe a custom url
246 38
					$this->setAreaUrl($sectionId, $areaId, $area);
247
248
					// Even a little icon
249 38
					$this->setAreaIcon($sectionId, $areaId, $area);
250
251
					// Did it have subsections?
252 38
					$this->processAreaSubsections($sectionId, $areaId, $area);
253
				}
254
255
				// Is this the current section?
256 38
				$this->checkCurrentSection($sectionId, $areaId, $area);
257
			}
258
		}
259 38
	}
260
261
	/**
262
	 * Checks the menu item to see if it is the currently selected one
263
	 *
264
	 * @param string   $sectionId
265
	 * @param string   $areaId
266
	 * @param MenuArea $area
267
	 */
268 38
	private function checkCurrentSection(string $sectionId, string $areaId, MenuArea $area): void
269
	{
270
		// Is this the current section?
271 38
		if ($this->currentArea == $areaId && !$this->foundSection)
272
		{
273 38
			$this->setAreaCurrent($sectionId, $areaId, $area);
274
275
			// Only do this once, m'kay?
276 38
			$this->foundSection = true;
277
		}
278 38
	}
279
280
	/**
281
	 * @param string   $sectionId
282
	 * @param string   $areaId
283
	 * @param MenuArea $area
284
	 */
285 38
	private function setFirstAreaCurrent(string $sectionId, string $areaId, MenuArea $area): void
286
	{
287
		// If we don't have an area then the first valid one is our choice.
288 38
		if (empty($this->currentArea))
289
		{
290 34
			$this->setAreaCurrent($sectionId, $areaId, $area);
291
		}
292 38
	}
293
294
	/**
295
	 * Simply sets the current area
296
	 *
297
	 * @param string   $sectionId
298
	 * @param string   $areaId
299
	 * @param MenuArea $area
300
	 */
301 38
	private function setAreaCurrent(string $sectionId, string $areaId, MenuArea $area): void
302
	{
303
		// Update the context if required - as we can have areas pretending to be others. ;)
304 38
		$this->menuContext['current_section'] = $sectionId;
305 38
		$this->currentArea = $area->getSelect() ?: $areaId;
306 38
		$this->includeData = $area->toArray();
307 38
	}
308
309
	/**
310
	 * @param MenuItem $obj
311
	 * @param integer  $idx
312
	 *
313
	 * @return string
314
	 */
315 38
	private function parseCounter(MenuItem $obj, int $idx): string
316
	{
317 38
		global $settings;
318
319 38
		$counter = '';
320 38
		if (!empty($this->menuOptions->getCounters()[$obj->getCounter()]))
321
		{
322 2
			$counter = sprintf(
323 2
				$settings['menu_numeric_notice'][$idx],
324 2
				$this->menuOptions->getCounters()[$obj->getCounter()]
325
			);
326
		}
327
328 38
		return $counter;
329
	}
330
331
	/**
332
	 * Sets the various section ID items
333
	 *
334
	 * What it does:
335
	 *   - If the ID is not set, sets it and sets the section title
336
	 *   - Sets the section title
337
	 *
338
	 * @param string      $sectionId
339
	 * @param MenuSection $section
340
	 */
341 38
	private function setSectionContext(string $sectionId, MenuSection $section): void
342
	{
343 38
		global $txt;
344
345 38
		$this->menuContext['sections'][$sectionId] = [
346 38
			'id' => $sectionId,
347 38
			'label' => ($section->getLabel() ?: $txt[$sectionId]) . $this->parseCounter($section, 0),
348 38
			'url' => $this->menuContext['base_url'] . $this->menuContext['extra_parameters'],
349
		];
350 38
	}
351
352
	/**
353
	 * Sets the various area items
354
	 *
355
	 * What it does:
356
	 *   - If the ID is not set, sets it and sets the area title
357
	 *   - Sets the area title
358
	 *
359
	 * @param string   $sectionId
360
	 * @param string   $areaId
361
	 * @param MenuArea $area
362
	 */
363 38
	private function setAreaContext(string $sectionId, string $areaId, MenuArea $area): void
364
	{
365 38
		global $txt;
366
367 38
		$this->menuContext['sections'][$sectionId]['areas'][$areaId] = [
368 38
			'label' => ($area->getLabel() ?: $txt[$areaId]) . $this->parseCounter($area, 1),
369
		];
370 38
	}
371
372
	/**
373
	 * Set the URL for the menu item
374
	 *
375
	 * @param string   $sectionId
376
	 * @param string   $areaId
377
	 * @param MenuArea $area
378
	 */
379 38
	private function setAreaUrl(string $sectionId, string $areaId, MenuArea $area): void
380
	{
381 38
		$area->setUrl(
382 38
			$this->menuContext['sections'][$sectionId]['areas'][$areaId]['url'] =
383 38
				$area->getUrl(
384 38
				) ?: $this->menuContext['base_url'] . ';area=' . $areaId . $this->menuContext['extra_parameters']
385
		);
386 38
	}
387
388
	/**
389
	 * Set the menu icon
390
	 *
391
	 * @param string   $sectionId
392
	 * @param string   $areaId
393
	 * @param MenuArea $area
394
	 */
395 38
	private function setAreaIcon(string $sectionId, string $areaId, MenuArea $area): void
396
	{
397 38
		global $settings;
398
399
		// Work out where we should get our menu images from.
400 38
		$imagePath = file_exists($settings['theme_dir'] . '/images/admin/change_menu.png')
401 38
			? $settings['images_url'] . '/admin'
402 38
			: $settings['default_images_url'] . '/admin';
403
404
		// Does this area have its own icon?
405 38
		if (!empty($area->getIcon()))
406
		{
407 1
			$this->menuContext['sections'][$sectionId]['areas'][$areaId]['icon'] =
408 1
				'<img ' . (!empty($area->getClass()) ? 'class="' . $area->getClass(
409 1
					) . '" ' : 'style="background: none"') . ' src="' . $imagePath . '/' . $area->getIcon(
410 1
				) . '" alt="" />&nbsp;&nbsp;';
411
		}
412
		else
413
		{
414 38
			$this->menuContext['sections'][$sectionId]['areas'][$areaId]['icon'] = '';
415
		}
416 38
	}
417
418
	/**
419
	 * Processes all of the subsections for a menu item
420
	 *
421
	 * @param string   $sectionId
422
	 * @param string   $areaId
423
	 * @param MenuArea $area
424
	 */
425 38
	protected function processAreaSubsections(string $sectionId, string $areaId, MenuArea $area): void
426
	{
427 38
		$this->menuContext['sections'][$sectionId]['areas'][$areaId]['subsections'] = [];
428
429
		// For each subsection process the options
430 38
		$subSections = array_filter(
431 38
			$area->getSubsections(),
432 38
			function ($sub) {
433 38
				return $this->checkPermissions($sub) && $sub->isEnabled();
434 38
			}
435
		);
436 38
		foreach ($subSections as $subId => $sub)
437
		{
438 38
			$this->menuContext['sections'][$sectionId]['areas'][$areaId]['subsections'][$subId] = [
439 38
				'label' => $sub->getLabel() . $this->parseCounter($sub, 2),
440
			];
441
442 38
			$this->setSubsSectionUrl($sectionId, $areaId, $subId, $sub);
443
444 38
			if ($this->currentArea == $areaId)
445
			{
446 38
				$this->setCurrentSubSection($subId, $sub);
447
			}
448
		}
449 38
		$this->setDefaultSubSection($areaId, $subSections);
450 38
	}
451
452
	/**
453
	 * @param string         $sectionId
454
	 * @param string         $areaId
455
	 * @param string         $subId
456
	 * @param MenuSubsection $sub
457
	 */
458 38
	private function setSubsSectionUrl(string $sectionId, string $areaId, string $subId, MenuSubsection $sub): void
459
	{
460 38
		$sub->setUrl(
461 38
			$this->menuContext['sections'][$sectionId]['areas'][$areaId]['subsections'][$subId]['url'] =
462 38
				$sub->getUrl(
463 38
				) ?: $this->menuContext['base_url'] . ';area=' . $areaId . ';sa=' . $subId . $this->menuContext['extra_parameters']
464
		);
465 38
	}
466
467
	/**
468
	 * Set the current subsection
469
	 *
470
	 * @param string         $subId
471
	 * @param MenuSubsection $sub
472
	 */
473 6
	private function setCurrentSubSection(string $subId, MenuSubsection $sub): void
474
	{
475
		// Is this the current subsection?
476 6
		$subIdCheck = $this->req->getQuery('sa', 'trim', null);
477
		if (
478 6
			$subIdCheck == $subId
479 6
			|| in_array($subIdCheck, $sub->getActive(), true)
480 6
			|| empty($this->currentSubaction) && $sub->isDefault()
481
		)
482
		{
483 6
			$this->currentSubaction = $subId;
484
		}
485 6
	}
486
487
	/**
488
	 * Ensures that the current subsection is set.
489
	 *
490
	 * @param string $areaId
491
	 * @param array  $subSections
492
	 */
493 38
	private function setDefaultSubSection(string $areaId, array $subSections): void
494
	{
495 38
		if ($this->currentArea == $areaId && empty($this->currentSubaction))
496
		{
497 33
			$this->currentSubaction = key($subSections) ?? '';
498
		}
499 38
	}
500
501
	/**
502
	 * Checks and updates base and section urls
503
	 */
504 40
	private function setActiveButtons(): void
505
	{
506
		// If there are sections quickly goes through all the sections to check if the base menu has an url
507 40
		if (!empty($this->menuContext['current_section']))
508
		{
509 38
			$this->menuContext['sections'][$this->menuContext['current_section']]['selected'] = true;
510 38
			$this->menuContext['sections'][$this->menuContext['current_section']]['areas'][$this->currentArea]['selected'] =
511
				true;
512
513 38
			if (!empty($this->menuContext['sections'][$this->menuContext['current_section']]['areas'][$this->currentArea]['subsections'][$this->currentSubaction]))
514
			{
515 6
				$this->menuContext['sections'][$this->menuContext['current_section']]['areas'][$this->currentArea]['subsections'][$this->currentSubaction]['selected'] =
516
					true;
517
			}
518
		}
519 40
	}
520
521
	/**
522
	 * Add the base menu options for this menu
523
	 *
524
	 * @param array $menuOptions an array of options that can be used to override some default
525
	 *                           behaviours. See MenuOptions for details.
526
	 */
527 41
	public function addOptions(array $menuOptions): void
528
	{
529 41
		$this->menuOptions = MenuOptions::buildFromArray($menuOptions);
530 41
	}
531
532
	/**
533
	 * @param string      $id
534
	 * @param MenuSection $section
535
	 *
536
	 * @return $this
537
	 */
538 41
	public function addSection(string $id, MenuSection $section): Menu
539
	{
540 41
		$this->menuData[$id] = $section;
541
542 41
		return $this;
543
	}
544
545
	/**
546
	 * Finalizes items so the computed menu can be used
547
	 *
548
	 * What it does:
549
	 *   - Sets the menu layer in the template stack
550
	 *   - Loads context with the computed menu context
551
	 *   - Sets current subaction and current max menu id
552
	 */
553 38
	public function setContext(): void
554
	{
555 38
		global $context;
556
557
		// Almost there - load the template and add to the template layers.
558 38
		theme()->getTemplates()->load($this->menuOptions->getTemplateName());
559 38
		theme()->getLayers()->add($this->menuOptions->getLayerName());
560
561
		// Set it all to context for template consumption
562 38
		$this->menuContext['layer_name'] = $this->menuOptions->getLayerName();
563 38
		$this->menuContext['can_toggle_drop_down'] = $this->menuOptions->isDropDownToggleable();
564 38
		$context['max_menu_id'] = $this->maxMenuId;
565 38
		$context['current_subaction'] = $this->currentSubaction;
566 38
		$this->menuContext['current_subsection'] = $this->currentSubaction;
567 38
		$this->menuContext['current_area'] = $this->currentArea;
568 38
		$context['menu_data_' . $this->maxMenuId] = $this->menuContext;
569 38
	}
570
571
	/**
572
	 * Delete a menu.
573
	 *
574
	 * Checks to see if this menu been loaded into context
575
	 * and, if so, resets $context['max_menu_id'] back to the
576
	 * last known menu (if any) and remove the template layer
577
	 * if there aren't any other known menus.
578
	 */
579 21
	public function destroy(): void
580
	{
581 21
		global $context;
582
583
		// Has this menu been loaded into context?
584 21
		if (isset($context[$menuName = 'menu_data_' . $this->maxMenuId]))
585
		{
586
			// Decrement the pointer if this is the final menu in the series.
587 18
			if ($this->maxMenuId == $context['max_menu_id'])
588
			{
589 18
				$context['max_menu_id'] = max($context['max_menu_id'] - 1, 0);
590
			}
591
592
			// Remove the template layer if this was the only menu left.
593 18
			if ($context['max_menu_id'] == 0)
594
			{
595 18
				theme()->getLayers()->remove($context[$menuName]['layer_name']);
596
			}
597
598 18
			unset($context[$menuName]);
599
		}
600 21
	}
601
}
602