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) |
|
|
|
|
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
|
|
|
|