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 1.1 |
15
|
|
|
* |
16
|
|
|
*/ |
17
|
|
|
|
18
|
|
|
declare(strict_types = 1); |
19
|
|
|
|
20
|
|
|
namespace ElkArte\Menu; |
21
|
|
|
|
22
|
|
|
use HttpReq; |
23
|
|
|
|
24
|
|
|
/** |
25
|
|
|
* This class implements a standard way of creating menus |
26
|
|
|
*/ |
27
|
|
|
class Menu |
28
|
|
|
{ |
29
|
|
|
/** |
30
|
|
|
* Instance of HttpReq |
31
|
|
|
* @var HttpReq |
32
|
|
|
*/ |
33
|
|
|
protected $req; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* Will hold the created $context |
37
|
|
|
* @var array |
38
|
|
|
*/ |
39
|
|
|
protected $menu_context = []; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* Used for profile menu for own / any |
43
|
|
|
* @var string |
44
|
|
|
*/ |
45
|
|
|
protected $permission_set; |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* If we found the menu item selected |
49
|
|
|
* @var bool |
50
|
|
|
*/ |
51
|
|
|
protected $found_section = false; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* If we can't find the selection, we pick for them |
55
|
|
|
* @var string |
56
|
|
|
*/ |
57
|
|
|
protected $current_area = ''; |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* The current subaction of the system |
61
|
|
|
* @var string |
62
|
|
|
*/ |
63
|
|
|
protected $current_subaction = ''; |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* Will hold the selected menu data that is returned to the caller |
67
|
|
|
* @var array |
68
|
|
|
*/ |
69
|
|
|
private $include_data = []; |
70
|
|
|
|
71
|
|
|
/** |
72
|
|
|
* Unique menu number |
73
|
|
|
* @var int |
74
|
|
|
*/ |
75
|
|
|
private $max_menu_id; |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* Holds menu options set by AddOptions |
79
|
|
|
* @var array |
80
|
|
|
*/ |
81
|
|
|
public $menuOptions = []; |
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* Holds menu definition structure set by AddAreas |
85
|
|
|
* @var array |
86
|
|
|
*/ |
87
|
|
|
public $menuData = []; |
88
|
|
|
|
89
|
|
|
/** |
90
|
|
|
* Initial processing for the menu |
91
|
|
|
*/ |
92
|
|
|
public function __construct(HttpReq $req = null) |
93
|
|
|
{ |
94
|
|
|
global $context; |
95
|
|
|
|
96
|
|
|
// Access to post/get data |
97
|
|
|
$this->req = $req ?: HttpReq::instance(); |
98
|
|
|
|
99
|
|
|
// Every menu gets a unique ID, these are shown in first in, first out order. |
100
|
|
|
$this->max_menu_id = ($context['max_menu_id'] ?? 0) + 1; |
101
|
|
|
|
102
|
|
|
// This will be all the data for this menu |
103
|
|
|
$this->menu_context = []; |
104
|
|
|
|
105
|
|
|
// This is necessary only in profile (at least for the core), but we do it always because it's easier |
106
|
|
|
$this->permission_set = !empty($context['user']['is_owner']) ? 'own' : 'any'; |
107
|
|
|
|
108
|
|
|
// We may have a current subaction |
109
|
|
|
$this->current_subaction = $context['current_subaction'] ?? null; |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
/** |
113
|
|
|
* Create a menu |
114
|
|
|
* |
115
|
|
|
* @return array |
116
|
|
|
* @throws \Elk_Exception |
117
|
|
|
*/ |
118
|
|
|
public function prepareMenu(): array |
119
|
|
|
{ |
120
|
|
|
// Process the menu Options |
121
|
|
|
$this->processMenuOptions(); |
122
|
|
|
|
123
|
|
|
// Check the menus urls |
124
|
|
|
$this->setBaseUrl(); |
125
|
|
|
|
126
|
|
|
// Process the menu Data |
127
|
|
|
$this->processMenuData(); |
128
|
|
|
|
129
|
|
|
// Check the menus urls |
130
|
|
|
$this->setActiveButtons(); |
131
|
|
|
|
132
|
|
|
// Make sure we created some awesome sauce |
133
|
|
|
if (!$this->validateData()) |
134
|
|
|
{ |
135
|
|
|
throw new \Elk_Exception('no_access', false); |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
// Finally - return information on the selected item. |
139
|
|
|
return $this->include_data + [ |
140
|
|
|
'current_action' => $this->menu_context['current_action'], |
141
|
|
|
'current_area' => $this->current_area, |
142
|
|
|
'current_section' => !empty($this->menu_context['current_section']) ? $this->menu_context['current_section'] : '', |
143
|
|
|
'current_subsection' => $this->current_subaction, |
144
|
|
|
]; |
145
|
|
|
|
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
/** |
149
|
|
|
* Prepares tabs for the template. |
150
|
|
|
* |
151
|
|
|
* This should be called after the area is dispatched, because areas |
152
|
|
|
* are usually in their own file. Those files, once dispatched to, hold |
153
|
|
|
* some data for the tabs which must be specially combined with subaction |
154
|
|
|
* data for everything to work properly. |
155
|
|
|
* |
156
|
|
|
* Seems complicated, yes. |
157
|
|
|
*/ |
158
|
|
|
public function prepareTabData(): void |
159
|
|
|
{ |
160
|
|
|
global $context; |
161
|
|
|
|
162
|
|
|
// Handy shortcut. |
163
|
|
|
$tab_context = &$context['menu_data_' . $this->max_menu_id]['tab_data']; |
164
|
|
|
|
165
|
|
|
// Tabs are really just subactions. |
166
|
|
|
if (isset($tab_context['tabs'], $this->menu_context['sections'][$this->menu_context['current_section']]['areas'][$this->current_area]['subsections'])) |
167
|
|
|
{ |
168
|
|
|
$tab_context['tabs'] = array_replace_recursive( |
169
|
|
|
$tab_context['tabs'], |
170
|
|
|
$this->menu_context['sections'][$this->menu_context['current_section']]['areas'][$this->current_area]['subsections'] |
171
|
|
|
); |
172
|
|
|
|
173
|
|
|
// Has it been deemed selected? |
174
|
|
|
$tab_context = array_merge($tab_context, $tab_context['tabs'][$this->current_subaction]); |
175
|
|
|
} |
176
|
|
|
} |
177
|
|
|
|
178
|
|
|
/** |
179
|
|
|
* Performs a sanity check that a menu was created successfully |
180
|
|
|
* |
181
|
|
|
* - If it fails to find valid data, will reset max_menu_id and any menu context created |
182
|
|
|
* |
183
|
|
|
* @return bool |
184
|
|
|
*/ |
185
|
|
|
private function validateData(): bool |
186
|
|
|
{ |
187
|
|
|
if (empty($this->menu_context['sections'])) |
188
|
|
|
{ |
189
|
|
|
return false; |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
// Check we had something - for sanity sake. |
193
|
|
|
return !empty($this->include_data); |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
/** |
197
|
|
|
* Process the array of MenuOptions passed to the class |
198
|
|
|
*/ |
199
|
|
|
protected function processMenuOptions(): void |
200
|
|
|
{ |
201
|
|
|
global $context; |
202
|
|
|
|
203
|
|
|
// What is the general action of this menu i.e. $scripturl?action=XYZ. |
204
|
|
|
$this->menu_context['current_action'] = $this->menuOptions['action'] ?? $context['current_action']; |
205
|
|
|
|
206
|
|
|
// What is the current area selected? |
207
|
|
|
$this->current_area = $this->req->getQuery('area', 'trim|strval', $this->menuOptions['area'] ?? ''); |
208
|
|
|
|
209
|
|
|
$this->buildAdditionalParams(); |
210
|
|
|
$this->buildTemplateVars(); |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
/** |
214
|
|
|
* Build the menuOption additional parameters for use in the url |
215
|
|
|
*/ |
216
|
|
|
private function buildAdditionalParams(): void |
217
|
|
|
{ |
218
|
|
|
global $context; |
219
|
|
|
|
220
|
|
|
$this->menu_context['extra_parameters'] = ''; |
221
|
|
|
|
222
|
|
|
if (!empty($this->menuOptions['extra_url_parameters'])) |
223
|
|
|
{ |
224
|
|
|
foreach ($this->menuOptions['extra_url_parameters'] as $key => $value) |
225
|
|
|
{ |
226
|
|
|
$this->menu_context['extra_parameters'] .= ';' . $key . '=' . $value; |
227
|
|
|
} |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
// Only include the session ID in the URL if it's strictly necessary. |
231
|
|
|
if (empty($this->menuOptions['disable_url_session_check'])) |
232
|
|
|
{ |
233
|
|
|
$this->menu_context['extra_parameters'] .= ';' . $context['session_var'] . '=' . $context['session_id']; |
234
|
|
|
} |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
/** |
238
|
|
|
* Process the menuData array passed to the class |
239
|
|
|
* |
240
|
|
|
* - Only processes areas that are enabled and that the user has permissions |
241
|
|
|
*/ |
242
|
|
|
protected function processMenuData(): void |
243
|
|
|
{ |
244
|
|
|
// Now setup the context correctly. |
245
|
|
|
foreach ($this->menuData as $section_id => $section) |
246
|
|
|
{ |
247
|
|
|
// Is this section enabled? and do they have permissions? |
248
|
|
|
if ($section->isEnabled() && $this->checkPermissions($section)) |
249
|
|
|
{ |
250
|
|
|
$this->setSectionContext($section_id, $section); |
251
|
|
|
|
252
|
|
|
// Process this menu section |
253
|
|
|
$this->processSectionAreas($section_id, $section); |
254
|
|
|
} |
255
|
|
|
} |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
/** |
259
|
|
|
* Determines if the user has the permissions to access the section/area |
260
|
|
|
* |
261
|
|
|
* If said item did not provide any permission to check, full |
262
|
|
|
* unfettered access is assumed. |
263
|
|
|
* |
264
|
|
|
* The profile areas are a bit different in that each permission is |
265
|
|
|
* divided into two sets: "own" for owner and "any" for everyone else. |
266
|
|
|
* |
267
|
|
|
* @param MenuItem $obj area or section being checked |
268
|
|
|
* |
269
|
|
|
* @return bool |
270
|
|
|
*/ |
271
|
|
|
private function checkPermissions(MenuItem $obj): bool |
272
|
|
|
{ |
273
|
|
|
if (!empty($obj->getPermission())) |
274
|
|
|
{ |
275
|
|
|
// The profile menu has slightly different permissions |
276
|
|
|
if (is_array($obj->getPermission()) && isset($obj->getPermission()['own'], $obj->getPermission()['any'])) |
277
|
|
|
{ |
278
|
|
|
return allowedTo($obj->getPermission()[$this->permission_set]); |
279
|
|
|
} |
280
|
|
|
|
281
|
|
|
return allowedTo($obj->getPermission()); |
282
|
|
|
} |
283
|
|
|
|
284
|
|
|
return true; |
285
|
|
|
} |
286
|
|
|
|
287
|
|
|
/** |
288
|
|
|
* Checks if the area has a label or not |
289
|
|
|
* |
290
|
|
|
* @return bool |
291
|
|
|
*/ |
292
|
|
|
private function areaHasLabel(string $area_id, MenuArea $area): bool |
293
|
|
|
{ |
294
|
|
|
global $txt; |
295
|
|
|
|
296
|
|
|
return !empty($area->getLabel()) || isset($txt[$area_id]); |
297
|
|
|
} |
298
|
|
|
|
299
|
|
|
/** |
300
|
|
|
* Main processing for creating the menu items for all sections |
301
|
|
|
* |
302
|
|
|
* @param string $section_id |
303
|
|
|
* @param MenuSection $section |
304
|
|
|
*/ |
305
|
|
|
protected function processSectionAreas(string $section_id, MenuSection $section): void |
306
|
|
|
{ |
307
|
|
|
// Now we cycle through the sections to pick the right area. |
308
|
|
|
foreach ($section->getAreas() as $area_id => $area) |
309
|
|
|
{ |
310
|
|
|
// Is the area enabled, Does the user have permission and it has some form of a name |
311
|
|
|
if ($area->isEnabled() && $this->checkPermissions($area) && $this->areaHasLabel($area_id, $area)) |
312
|
|
|
{ |
313
|
|
|
// Make sure we have a valid current area |
314
|
|
|
$this->setFirstAreaCurrent($section_id, $area_id, $area); |
315
|
|
|
|
316
|
|
|
// If this is hidden from view don't do the rest. |
317
|
|
|
if (!$area->isHidden()) |
318
|
|
|
{ |
319
|
|
|
// First time this section? |
320
|
|
|
$this->setAreaContext($section_id, $area_id, $area); |
321
|
|
|
|
322
|
|
|
// Maybe a custom url |
323
|
|
|
$this->setAreaUrl($section_id, $area_id, $area); |
324
|
|
|
|
325
|
|
|
// Even a little icon |
326
|
|
|
$this->setAreaIcon($section_id, $area_id, $area); |
327
|
|
|
|
328
|
|
|
// Did it have subsections? |
329
|
|
|
$this->processAreaSubsections($section_id, $area_id, $area); |
330
|
|
|
} |
331
|
|
|
|
332
|
|
|
// Is this the current section? |
333
|
|
|
$this->checkCurrentSection($section_id, $area_id, $area); |
334
|
|
|
} |
335
|
|
|
} |
336
|
|
|
} |
337
|
|
|
|
338
|
|
|
/** |
339
|
|
|
* Checks the menu item to see if it is the currently selected one |
340
|
|
|
* |
341
|
|
|
* @param string $section_id |
342
|
|
|
* @param string $area_id |
343
|
|
|
* @param MenuArea $area |
344
|
|
|
*/ |
345
|
|
|
private function checkCurrentSection(string $section_id, string $area_id, MenuArea $area): void |
346
|
|
|
{ |
347
|
|
|
// Is this the current section? |
348
|
|
|
if ($this->current_area == $area_id && !$this->found_section) |
349
|
|
|
{ |
350
|
|
|
$this->setAreaCurrent($section_id, $area_id, $area); |
351
|
|
|
|
352
|
|
|
// Only do this once, m'kay? |
353
|
|
|
$this->found_section = true; |
354
|
|
|
} |
355
|
|
|
} |
356
|
|
|
|
357
|
|
|
/** |
358
|
|
|
* @param string $section_id |
359
|
|
|
* @param string $area_id |
360
|
|
|
* @param MenuArea $area |
361
|
|
|
*/ |
362
|
|
|
private function setFirstAreaCurrent(string $section_id, string $area_id, MenuArea $area): void |
363
|
|
|
{ |
364
|
|
|
// If we don't have an area then the first valid one is our choice. |
365
|
|
|
if (empty($this->current_area)) |
366
|
|
|
{ |
367
|
|
|
$this->setAreaCurrent($section_id, $area_id, $area); |
368
|
|
|
} |
369
|
|
|
} |
370
|
|
|
|
371
|
|
|
/** |
372
|
|
|
* Simply sets the current area |
373
|
|
|
* |
374
|
|
|
* @param string $section_id |
375
|
|
|
* @param string $area_id |
376
|
|
|
* @param MenuArea $area |
377
|
|
|
*/ |
378
|
|
|
private function setAreaCurrent(string $section_id, string $area_id, MenuArea $area): void |
379
|
|
|
{ |
380
|
|
|
// Update the context if required - as we can have areas pretending to be others. ;) |
381
|
|
|
$this->menu_context['current_section'] = $section_id; |
382
|
|
|
$this->current_area = $area->getSelect() ?: $area_id; |
383
|
|
|
$this->include_data = $area->toArray(); |
384
|
|
|
} |
385
|
|
|
|
386
|
|
|
/** |
387
|
|
|
* @param MenuItem $obj |
388
|
|
|
* @param integer $idx |
389
|
|
|
* |
390
|
|
|
* @return string |
391
|
|
|
*/ |
392
|
|
|
private function parseCounter(MenuItem $obj, int $idx): string |
393
|
|
|
{ |
394
|
|
|
global $settings; |
395
|
|
|
|
396
|
|
|
$counter = ''; |
397
|
|
|
if (isset($this->menuOptions['counters']) && !empty($this->menuOptions['counters'][$obj->getCounter()])) |
398
|
|
|
{ |
399
|
|
|
$counter = |
400
|
|
|
sprintf($settings['menu_numeric_notice'][$idx], $this->menuOptions['counters'][$obj->getCounter()]); |
401
|
|
|
} |
402
|
|
|
|
403
|
|
|
return $counter; |
404
|
|
|
} |
405
|
|
|
|
406
|
|
|
/** |
407
|
|
|
* Sets the various section ID items |
408
|
|
|
* |
409
|
|
|
* What it does: |
410
|
|
|
* - If the ID is not set, sets it and sets the section title |
411
|
|
|
* - Sets the section title |
412
|
|
|
* |
413
|
|
|
* @param string $section_id |
414
|
|
|
* @param MenuSection $section |
415
|
|
|
*/ |
416
|
|
|
private function setSectionContext(string $section_id, MenuSection $section): void |
417
|
|
|
{ |
418
|
|
|
global $txt; |
419
|
|
|
|
420
|
|
|
$this->menu_context['sections'][$section_id] = [ |
421
|
|
|
'id' => $section_id, |
422
|
|
|
'label' => ($section->getLabel() ?: $txt[$section_id]) . $this->parseCounter($section, 0), |
423
|
|
|
'url' => $this->menu_context['base_url'] . $this->menu_context['extra_parameters'], |
424
|
|
|
]; |
425
|
|
|
} |
426
|
|
|
|
427
|
|
|
/** |
428
|
|
|
* Sets the various area items |
429
|
|
|
* |
430
|
|
|
* What it does: |
431
|
|
|
* - If the ID is not set, sets it and sets the area title |
432
|
|
|
* - Sets the area title |
433
|
|
|
* |
434
|
|
|
* @param string $section_id |
435
|
|
|
* @param string $area_id |
436
|
|
|
* @param MenuArea $area |
437
|
|
|
*/ |
438
|
|
|
private function setAreaContext(string $section_id, string $area_id, MenuArea $area): void |
439
|
|
|
{ |
440
|
|
|
global $txt; |
441
|
|
|
|
442
|
|
|
$this->menu_context['sections'][$section_id]['areas'][$area_id] = [ |
443
|
|
|
'label' => ($area->getLabel() ?: $txt[$area_id]) . $this->parseCounter($area, 1), |
444
|
|
|
]; |
445
|
|
|
} |
446
|
|
|
|
447
|
|
|
/** |
448
|
|
|
* Set the URL for the menu item |
449
|
|
|
* |
450
|
|
|
* @param string $section_id |
451
|
|
|
* @param string $area_id |
452
|
|
|
* @param MenuArea $area |
453
|
|
|
*/ |
454
|
|
|
private function setAreaUrl(string $section_id, string $area_id, MenuArea $area): void |
455
|
|
|
{ |
456
|
|
|
$area->setUrl( |
457
|
|
|
$this->menu_context['sections'][$section_id]['areas'][$area_id]['url'] = |
458
|
|
|
$area->getUrl( |
459
|
|
|
) ?: $this->menu_context['base_url'] . ';area=' . $area_id . $this->menu_context['extra_parameters'] |
460
|
|
|
); |
461
|
|
|
} |
462
|
|
|
|
463
|
|
|
/** |
464
|
|
|
* Set the menu icon |
465
|
|
|
* |
466
|
|
|
* @param string $section_id |
467
|
|
|
* @param string $area_id |
468
|
|
|
* @param MenuArea $area |
469
|
|
|
*/ |
470
|
|
|
private function setAreaIcon(string $section_id, string $area_id, MenuArea $area): void |
471
|
|
|
{ |
472
|
|
|
global $settings; |
473
|
|
|
|
474
|
|
|
// Work out where we should get our menu images from. |
475
|
|
|
$image_path = file_exists($settings['theme_dir'] . '/images/admin/change_menu.png') |
476
|
|
|
? $settings['images_url'] . '/admin' |
477
|
|
|
: $settings['default_images_url'] . '/admin'; |
478
|
|
|
|
479
|
|
|
// Does this area have its own icon? |
480
|
|
|
if (!empty($area->getIcon())) |
481
|
|
|
{ |
482
|
|
|
$this->menu_context['sections'][$section_id]['areas'][$area_id]['icon'] = |
483
|
|
|
'<img ' . (!empty($area->getClass()) ? 'class="' . $area->getClass( |
484
|
|
|
) . '" ' : 'style="background: none"') . ' src="' . $image_path . '/' . $area->getIcon( |
485
|
|
|
) . '" alt="" /> '; |
486
|
|
|
} |
487
|
|
|
else |
488
|
|
|
{ |
489
|
|
|
$this->menu_context['sections'][$section_id]['areas'][$area_id]['icon'] = ''; |
490
|
|
|
} |
491
|
|
|
} |
492
|
|
|
|
493
|
|
|
/** |
494
|
|
|
* Processes all of the subsections for a menu item |
495
|
|
|
* |
496
|
|
|
* @param string $section_id |
497
|
|
|
* @param string $area_id |
498
|
|
|
* @param MenuArea $area |
499
|
|
|
*/ |
500
|
|
|
protected function processAreaSubsections(string $section_id, string $area_id, MenuArea $area): void |
501
|
|
|
{ |
502
|
|
|
// If there are subsections for this menu item |
503
|
|
|
if (!empty($area->getSubsections())) |
504
|
|
|
{ |
505
|
|
|
$this->menu_context['sections'][$section_id]['areas'][$area_id]['subsections'] = []; |
506
|
|
|
$first_sub_id = ''; |
507
|
|
|
|
508
|
|
|
// For each subsection process the options |
509
|
|
|
foreach ($area->getSubsections() as $sub_id => $sub) |
510
|
|
|
{ |
511
|
|
|
if ($this->checkPermissions($sub) && $sub->isEnabled()) |
512
|
|
|
{ |
513
|
|
|
$this->menu_context['sections'][$section_id]['areas'][$area_id]['subsections'][$sub_id] = [ |
514
|
|
|
'label' => $sub->getLabel() . $this->parseCounter($sub, 2), |
515
|
|
|
]; |
516
|
|
|
|
517
|
|
|
$this->setSubsSectionUrl($section_id, $area_id, $sub_id, $sub); |
518
|
|
|
|
519
|
|
|
if ($this->current_area == $area_id) |
520
|
|
|
{ |
521
|
|
|
if (empty($first_sub_id)) |
522
|
|
|
{ |
523
|
|
|
$first_sub_id = $sub_id; |
524
|
|
|
} |
525
|
|
|
$this->setCurrentSubSection($sub_id, $sub); |
526
|
|
|
} |
527
|
|
|
} |
528
|
|
|
else |
529
|
|
|
{ |
530
|
|
|
// Mark it as disabled... |
531
|
|
|
$this->menu_context['sections'][$section_id]['areas'][$area_id]['subsections'][$sub_id]['disabled'] = |
532
|
|
|
true; |
533
|
|
|
} |
534
|
|
|
} |
535
|
|
|
|
536
|
|
|
// Is this the current subsection? |
537
|
|
|
if (empty($this->current_subaction)) |
538
|
|
|
{ |
539
|
|
|
$this->current_subaction = $first_sub_id; |
|
|
|
|
540
|
|
|
} |
541
|
|
|
} |
542
|
|
|
} |
543
|
|
|
|
544
|
|
|
/** |
545
|
|
|
* @param string $section_id |
546
|
|
|
* @param string $area_id |
547
|
|
|
* @param string $sub_id |
548
|
|
|
* @param MenuSubsection $sub |
549
|
|
|
*/ |
550
|
|
|
private function setSubsSectionUrl(string $section_id, string $area_id, string $sub_id, MenuSubsection $sub): void |
551
|
|
|
{ |
552
|
|
|
$sub->setUrl( |
553
|
|
|
$this->menu_context['sections'][$section_id]['areas'][$area_id]['subsections'][$sub_id]['url'] = |
554
|
|
|
$sub->getUrl( |
555
|
|
|
) ?: $this->menu_context['base_url'] . ';area=' . $area_id . ';sa=' . $sub_id . $this->menu_context['extra_parameters'] |
556
|
|
|
); |
557
|
|
|
} |
558
|
|
|
|
559
|
|
|
/** |
560
|
|
|
* Set the current subsection |
561
|
|
|
* |
562
|
|
|
* @param string $sub_id |
563
|
|
|
* @param MenuSubsection $sub |
564
|
|
|
*/ |
565
|
|
|
private function setCurrentSubSection(string $sub_id, MenuSubsection $sub): void |
566
|
|
|
{ |
567
|
|
|
// Is this the current subsection? |
568
|
|
|
$sub_id_check = $this->req->getQuery('sa', 'trim', null); |
569
|
|
|
if ( |
570
|
|
|
$sub_id_check == $sub_id |
571
|
|
|
|| in_array($sub_id_check, $sub->getActive(), true) |
572
|
|
|
|| empty($this->current_subaction) && $sub->isDefault() |
573
|
|
|
) |
574
|
|
|
{ |
575
|
|
|
$this->current_subaction = $sub_id; |
576
|
|
|
} |
577
|
|
|
} |
578
|
|
|
|
579
|
|
|
/** |
580
|
|
|
* Checks and updates base and section urls |
581
|
|
|
*/ |
582
|
|
|
private function setBaseUrl(): void |
583
|
|
|
{ |
584
|
|
|
global $scripturl; |
585
|
|
|
|
586
|
|
|
// Should we use a custom base url, or use the default? |
587
|
|
|
$this->menu_context['base_url'] = |
588
|
|
|
$this->menuOptions['base_url'] ?? $scripturl . '?action=' . $this->menu_context['current_action']; |
589
|
|
|
} |
590
|
|
|
|
591
|
|
|
/** |
592
|
|
|
* Checks and updates base and section urls |
593
|
|
|
*/ |
594
|
|
|
private function setActiveButtons(): void |
595
|
|
|
{ |
596
|
|
|
// If there are sections quickly goes through all the sections to check if the base menu has an url |
597
|
|
|
if (!empty($this->menu_context['current_section'])) |
598
|
|
|
{ |
599
|
|
|
$this->menu_context['sections'][$this->menu_context['current_section']]['selected'] = true; |
600
|
|
|
$this->menu_context['sections'][$this->menu_context['current_section']]['areas'][$this->current_area]['selected'] = |
601
|
|
|
true; |
602
|
|
|
|
603
|
|
|
if (!empty($this->menu_context['sections'][$this->menu_context['current_section']]['areas'][$this->current_area]['subsections'][$this->current_subaction])) |
604
|
|
|
{ |
605
|
|
|
$this->menu_context['sections'][$this->menu_context['current_section']]['areas'][$this->current_area]['subsections'][$this->current_subaction]['selected'] = |
606
|
|
|
true; |
607
|
|
|
} |
608
|
|
|
} |
609
|
|
|
} |
610
|
|
|
|
611
|
|
|
/** |
612
|
|
|
* Add the base menu options for this menu |
613
|
|
|
* |
614
|
|
|
* @param array $menuOptions an array of options that can be used to override some default behaviours. |
615
|
|
|
* It can accept the following indexes: |
616
|
|
|
* - action => overrides the default action |
617
|
|
|
* - current_area => overrides the current area |
618
|
|
|
* - extra_url_parameters => an array or pairs or parameters to be added to the url |
619
|
|
|
* - disable_url_session_check => (boolean) if true the session var/id are omitted from |
620
|
|
|
* the url |
621
|
|
|
* - base_url => an alternative base url |
622
|
|
|
* - menu_type => alternative menu types? |
623
|
|
|
* - can_toggle_drop_down => (boolean) if the menu can "toggle" |
624
|
|
|
* - template_name => an alternative template to load (instead of Generic) |
625
|
|
|
* - layer_name => alternative layer name for the menu |
626
|
|
|
* - hook => hook name to call integrate_ . 'hook name' . '_areas' |
627
|
|
|
* - default_include_dir => directory to include for function support |
628
|
|
|
*/ |
629
|
|
|
public function addOptions(array $menuOptions): void |
630
|
|
|
{ |
631
|
|
|
$this->menuOptions = array_merge($this->menuOptions, $menuOptions); |
632
|
|
|
} |
633
|
|
|
|
634
|
|
|
/** |
635
|
|
|
* @param string $id |
636
|
|
|
* @param MenuSection $section |
637
|
|
|
* |
638
|
|
|
* @return $this |
639
|
|
|
*/ |
640
|
|
|
public function addSection(string $id, MenuSection $section): Menu |
641
|
|
|
{ |
642
|
|
|
$this->menuData[$id] = $section; |
643
|
|
|
|
644
|
|
|
return $this; |
645
|
|
|
} |
646
|
|
|
|
647
|
|
|
private function buildTemplateVars(): void |
648
|
|
|
{ |
649
|
|
|
global $user_info, $options; |
650
|
|
|
|
651
|
|
|
if (empty($this->menuOptions['menu_type'])) |
652
|
|
|
{ |
653
|
|
|
$this->menuOptions['menu_type'] = empty($options['use_sidebar_menu']) ? 'dropdown' : 'sidebar'; |
654
|
|
|
} |
655
|
|
|
$this->menuOptions['can_toggle_drop_down'] = |
656
|
|
|
!$user_info['is_guest'] || !empty($this->menuOptions['can_toggle_drop_down']); |
657
|
|
|
|
658
|
|
|
$this->menuOptions['template_name'] = $this->menuOptions['template_name'] ?? 'GenericMenu'; |
659
|
|
|
$this->menuOptions['layer_name'] = |
660
|
|
|
($this->menuOptions['layer_name'] ?? 'generic_menu') . '_' . $this->menuOptions['menu_type']; |
661
|
|
|
} |
662
|
|
|
|
663
|
|
|
/** |
664
|
|
|
* Finalizes items so the computed menu can be used |
665
|
|
|
* |
666
|
|
|
* What it does: |
667
|
|
|
* - Sets the menu layer in the template stack |
668
|
|
|
* - Loads context with the computed menu context |
669
|
|
|
* - Sets current subaction and current max menu id |
670
|
|
|
*/ |
671
|
|
|
public function setContext(): void |
672
|
|
|
{ |
673
|
|
|
global $context; |
674
|
|
|
|
675
|
|
|
// Almost there - load the template and add to the template layers. |
676
|
|
|
theme()->getTemplates()->load($this->menuOptions['template_name']); |
677
|
|
|
theme()->getLayers()->add($this->menuOptions['layer_name']); |
678
|
|
|
|
679
|
|
|
// Set it all to context for template consumption |
680
|
|
|
$this->menu_context['layer_name'] = $this->menuOptions['layer_name']; |
681
|
|
|
$this->menu_context['can_toggle_drop_down'] = $this->menuOptions['can_toggle_drop_down']; |
682
|
|
|
$context['max_menu_id'] = $this->max_menu_id; |
683
|
|
|
$context['current_subaction'] = $this->current_subaction; |
684
|
|
|
$this->menu_context['current_subsection'] = $this->current_subaction; |
685
|
|
|
$this->menu_context['current_area'] = $this->current_area; |
686
|
|
|
$context['menu_data_' . $this->max_menu_id] = $this->menu_context; |
687
|
|
|
} |
688
|
|
|
|
689
|
|
|
/** |
690
|
|
|
* Delete a menu. |
691
|
|
|
* |
692
|
|
|
* Checks to see if this menu been loaded into context |
693
|
|
|
* and, if so, resets $context['max_menu_id'] back to the |
694
|
|
|
* last known menu (if any) and remove the template layer |
695
|
|
|
* if there aren't any other known menus. |
696
|
|
|
*/ |
697
|
|
|
public function destroy(): void |
698
|
|
|
{ |
699
|
|
|
global $context; |
700
|
|
|
|
701
|
|
|
// Has this menu been loaded into context? |
702
|
|
|
if (isset($context[$menu_name = 'menu_data_' . $this->max_menu_id])) |
703
|
|
|
{ |
704
|
|
|
// Decrement the pointer if this is the final menu in the series. |
705
|
|
View Code Duplication |
if ($this->max_menu_id == $context['max_menu_id']) |
706
|
|
|
{ |
707
|
|
|
$context['max_menu_id'] = max($context['max_menu_id'] - 1, 0); |
708
|
|
|
} |
709
|
|
|
|
710
|
|
|
// Remove the template layer if this was the only menu left. |
711
|
|
|
if ($context['max_menu_id'] == 0) |
712
|
|
|
{ |
713
|
|
|
theme()->getLayers()->remove($context[$menu_name]['layer_name']); |
714
|
|
|
} |
715
|
|
|
|
716
|
|
|
unset($context[$menu_name]); |
717
|
|
|
} |
718
|
|
|
} |
719
|
|
|
} |
720
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.