1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Elgg Menu Builder |
4
|
|
|
* |
5
|
|
|
* @package Elgg.Core |
6
|
|
|
* @subpackage Navigation |
7
|
|
|
* @since 1.8.0 |
8
|
|
|
*/ |
9
|
|
|
class ElggMenuBuilder { |
10
|
|
|
|
11
|
|
|
/** |
12
|
|
|
* @var \ElggMenuItem[] |
13
|
|
|
*/ |
14
|
|
|
protected $menu = []; |
15
|
|
|
|
16
|
|
|
protected $selected = null; |
17
|
|
|
|
18
|
|
|
/** |
19
|
|
|
* \ElggMenuBuilder constructor |
20
|
|
|
* |
21
|
|
|
* @param \ElggMenuItem[] $menu Array of \ElggMenuItem objects |
22
|
|
|
*/ |
23
|
33 |
|
public function __construct(array $menu) { |
24
|
33 |
|
$this->menu = $menu; |
25
|
33 |
|
} |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* Get a prepared menu array |
29
|
|
|
* |
30
|
|
|
* @param mixed $sort_by Method to sort the menu by. @see \ElggMenuBuilder::sort() |
31
|
|
|
* @return array |
32
|
|
|
*/ |
33
|
33 |
|
public function getMenu($sort_by = 'text') { |
34
|
|
|
|
35
|
33 |
|
$this->selectFromContext(); |
36
|
|
|
|
37
|
33 |
|
$this->selected = $this->findSelected(); |
38
|
|
|
|
39
|
33 |
|
$this->setupSections(); |
40
|
|
|
|
41
|
33 |
|
$this->setupTrees(); |
42
|
|
|
|
43
|
33 |
|
$this->sort($sort_by); |
44
|
|
|
|
45
|
33 |
|
return $this->menu; |
46
|
|
|
} |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* Get the selected menu item |
50
|
|
|
* |
51
|
|
|
* @return \ElggMenuItem |
52
|
|
|
*/ |
53
|
33 |
|
public function getSelected() { |
54
|
33 |
|
return $this->selected; |
55
|
|
|
} |
56
|
|
|
|
57
|
|
|
/** |
58
|
|
|
* Select menu items for the current context |
59
|
|
|
* |
60
|
|
|
* @return void |
61
|
|
|
*/ |
62
|
33 |
|
protected function selectFromContext() { |
63
|
33 |
|
if (!isset($this->menu)) { |
64
|
|
|
$this->menu = []; |
65
|
|
|
return; |
66
|
|
|
} |
67
|
|
|
|
68
|
|
|
// get menu items for this context |
69
|
33 |
|
$selected_menu = []; |
70
|
33 |
|
foreach ($this->menu as $menu_item) { |
71
|
23 |
|
if (!is_object($menu_item)) { |
72
|
|
|
_elgg_services()->logger->error("A non-object was passed to \ElggMenuBuilder"); |
73
|
|
|
continue; |
74
|
|
|
} |
75
|
23 |
|
if ($menu_item->inContext()) { |
76
|
23 |
|
$selected_menu[] = $menu_item; |
77
|
|
|
} |
78
|
|
|
} |
79
|
|
|
|
80
|
33 |
|
$this->menu = $selected_menu; |
81
|
33 |
|
} |
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* Group the menu items into sections |
85
|
|
|
* |
86
|
|
|
* @return void |
87
|
|
|
*/ |
88
|
33 |
|
protected function setupSections() { |
89
|
33 |
|
$sectioned_menu = []; |
90
|
33 |
|
foreach ($this->menu as $menu_item) { |
91
|
21 |
|
if (!isset($sectioned_menu[$menu_item->getSection()])) { |
92
|
21 |
|
$sectioned_menu[$menu_item->getSection()] = []; |
93
|
|
|
} |
94
|
21 |
|
$sectioned_menu[$menu_item->getSection()][] = $menu_item; |
95
|
|
|
} |
96
|
33 |
|
$this->menu = $sectioned_menu; |
97
|
33 |
|
} |
98
|
|
|
|
99
|
|
|
/** |
100
|
|
|
* Create trees for each menu section |
101
|
|
|
* |
102
|
|
|
* @internal The tree is doubly linked (parent and children links) |
103
|
|
|
* @return void |
104
|
|
|
*/ |
105
|
33 |
|
protected function setupTrees() { |
106
|
33 |
|
$menu_tree = []; |
107
|
|
|
|
108
|
33 |
|
foreach ($this->menu as $key => $section) { |
109
|
21 |
|
$parents = []; |
110
|
21 |
|
$children = []; |
111
|
21 |
|
$all_menu_items = []; |
112
|
|
|
|
113
|
|
|
// divide base nodes from children |
114
|
21 |
|
foreach ($section as $menu_item) { |
115
|
|
|
/* @var \ElggMenuItem $menu_item */ |
116
|
21 |
|
$parent_name = $menu_item->getParentName(); |
117
|
21 |
|
$menu_item_name = $menu_item->getName(); |
118
|
|
|
|
119
|
21 |
|
if (!$parent_name) { |
120
|
|
|
// no parents so top level menu items |
121
|
21 |
|
$parents[$menu_item_name] = $menu_item; |
122
|
|
|
} else { |
123
|
2 |
|
$children[$menu_item_name] = $menu_item; |
124
|
|
|
} |
125
|
|
|
|
126
|
21 |
|
$all_menu_items[$menu_item_name] = $menu_item; |
127
|
|
|
} |
128
|
|
|
|
129
|
21 |
|
if (empty($all_menu_items)) { |
130
|
|
|
// empty sections can be skipped |
131
|
|
|
continue; |
132
|
|
|
} |
133
|
|
|
|
134
|
21 |
|
if (empty($parents)) { |
135
|
|
|
// menu items without parents? That is sad.. report to the log |
136
|
|
|
$message = _elgg_services()->translator->translate('ElggMenuBuilder:Trees:NoParents'); |
137
|
|
|
_elgg_services()->logger->notice($message); |
138
|
|
|
|
139
|
|
|
// skip section as without parents menu can not be drawn |
140
|
|
|
continue; |
141
|
|
|
} |
142
|
|
|
|
143
|
21 |
|
foreach ($children as $menu_item_name => $menu_item) { |
144
|
2 |
|
$parent_name = $menu_item->getParentName(); |
145
|
|
|
|
146
|
2 |
|
if (!array_key_exists($parent_name, $all_menu_items)) { |
147
|
|
|
// orphaned child, inform authorities and skip to next item |
148
|
2 |
|
$message = _elgg_services()->translator->translate('ElggMenuBuilder:Trees:OrphanedChild', [$menu_item_name, $parent_name]); |
149
|
2 |
|
_elgg_services()->logger->notice($message); |
150
|
|
|
|
151
|
2 |
|
continue; |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
if (!in_array($menu_item, $all_menu_items[$parent_name]->getData('children'))) { |
155
|
|
|
$all_menu_items[$parent_name]->addChild($menu_item); |
156
|
|
|
$menu_item->setParent($all_menu_items[$parent_name]); |
157
|
|
|
} else { |
158
|
|
|
// menu item already existed in parents children, report the duplicate registration |
159
|
|
|
$message = _elgg_services()->translator->translate('ElggMenuBuilder:Trees:DuplicateChild', [$menu_item_name]); |
160
|
|
|
_elgg_services()->logger->notice($message); |
161
|
|
|
|
162
|
|
|
continue; |
163
|
|
|
} |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
// convert keys to indexes for first level of tree |
167
|
21 |
|
$parents = array_values($parents); |
168
|
|
|
|
169
|
21 |
|
$menu_tree[$key] = $parents; |
170
|
|
|
} |
171
|
|
|
|
172
|
33 |
|
$this->menu = $menu_tree; |
|
|
|
|
173
|
33 |
|
} |
174
|
|
|
|
175
|
|
|
/** |
176
|
|
|
* Find the menu item that is currently selected |
177
|
|
|
* |
178
|
|
|
* @return \ElggMenuItem |
179
|
|
|
*/ |
180
|
33 |
|
protected function findSelected() { |
181
|
|
|
|
182
|
|
|
// do we have a selected menu item already |
183
|
33 |
|
foreach ($this->menu as $menu_item) { |
184
|
21 |
|
if ($menu_item->getSelected()) { |
185
|
21 |
|
return $menu_item; |
186
|
|
|
} |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
// scan looking for a selected item |
190
|
33 |
|
foreach ($this->menu as $menu_item) { |
191
|
21 |
|
if ($menu_item->getHref()) { |
192
|
13 |
|
if (elgg_http_url_is_identical(current_page_url(), $menu_item->getHref())) { |
193
|
|
|
$menu_item->setSelected(true); |
194
|
21 |
|
return $menu_item; |
195
|
|
|
} |
196
|
|
|
} |
197
|
|
|
} |
198
|
|
|
|
199
|
33 |
|
return null; |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
/** |
203
|
|
|
* Sort the menu sections and trees |
204
|
|
|
* |
205
|
|
|
* @param mixed $sort_by Sort type as string or php callback |
206
|
|
|
* @return void |
207
|
|
|
*/ |
208
|
33 |
|
protected function sort($sort_by) { |
209
|
|
|
|
210
|
|
|
// sort sections |
211
|
33 |
|
ksort($this->menu); |
212
|
|
|
|
213
|
33 |
|
switch ($sort_by) { |
214
|
|
|
case 'text': |
215
|
17 |
|
$sort_callback = [\ElggMenuBuilder::class, 'compareByText']; |
216
|
17 |
|
break; |
217
|
|
|
case 'name': |
218
|
2 |
|
$sort_callback = [\ElggMenuBuilder::class, 'compareByName']; |
219
|
2 |
|
break; |
220
|
|
|
case 'priority': |
221
|
16 |
|
$sort_callback = [\ElggMenuBuilder::class, 'compareByPriority']; |
222
|
16 |
|
break; |
223
|
|
|
case 'register': |
224
|
|
|
// use registration order - usort breaks this |
225
|
|
|
return; |
226
|
|
|
break; |
|
|
|
|
227
|
|
|
default: |
228
|
|
|
if (is_callable($sort_by)) { |
229
|
|
|
$sort_callback = $sort_by; |
230
|
|
|
} else { |
231
|
|
|
return; |
232
|
|
|
} |
233
|
|
|
break; |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
// sort each section |
237
|
33 |
|
foreach ($this->menu as $index => $section) { |
238
|
21 |
|
foreach ($section as $key => $node) { |
239
|
21 |
|
$section[$key]->setData('original_order', $key); |
240
|
|
|
} |
241
|
21 |
|
usort($section, $sort_callback); |
|
|
|
|
242
|
21 |
|
$this->menu[$index] = $section; |
243
|
|
|
|
244
|
|
|
// depth first traversal of tree |
245
|
21 |
|
foreach ($section as $root) { |
246
|
21 |
|
$stack = []; |
247
|
21 |
|
array_push($stack, $root); |
248
|
21 |
|
while (!empty($stack)) { |
249
|
21 |
|
$node = array_pop($stack); |
250
|
|
|
/* @var \ElggMenuItem $node */ |
251
|
21 |
|
$node->sortChildren($sort_callback); |
|
|
|
|
252
|
21 |
|
$children = $node->getChildren(); |
253
|
21 |
|
if ($children) { |
|
|
|
|
254
|
|
|
$stack = array_merge($stack, $children); |
255
|
|
|
} |
256
|
|
|
} |
257
|
|
|
} |
258
|
|
|
} |
259
|
33 |
|
} |
260
|
|
|
|
261
|
|
|
/** |
262
|
|
|
* Compare two menu items by their display text |
263
|
|
|
* HTML tags are stripped before comparison |
264
|
|
|
* |
265
|
|
|
* @param \ElggMenuItem $a Menu item |
266
|
|
|
* @param \ElggMenuItem $b Menu item |
267
|
|
|
* @return bool |
268
|
|
|
*/ |
269
|
6 |
|
public static function compareByText($a, $b) { |
270
|
6 |
|
$at = strip_tags($a->getText()); |
271
|
6 |
|
$bt = strip_tags($b->getText()); |
272
|
|
|
|
273
|
6 |
|
$result = strnatcmp($at, $bt); |
274
|
6 |
|
if ($result === 0) { |
275
|
|
|
return $a->getData('original_order') - $b->getData('original_order'); |
|
|
|
|
276
|
|
|
} |
277
|
6 |
|
return $result; |
|
|
|
|
278
|
|
|
} |
279
|
|
|
|
280
|
|
|
/** |
281
|
|
|
* Compare two menu items by their identifiers |
282
|
|
|
* |
283
|
|
|
* @param \ElggMenuItem $a Menu item |
284
|
|
|
* @param \ElggMenuItem $b Menu item |
285
|
|
|
* @return bool |
286
|
|
|
*/ |
287
|
|
|
public static function compareByName($a, $b) { |
288
|
|
|
$an = $a->getName(); |
289
|
|
|
$bn = $b->getName(); |
290
|
|
|
|
291
|
|
|
$result = strnatcmp($an, $bn); |
292
|
|
|
if ($result === 0) { |
293
|
|
|
return $a->getData('original_order') - $b->getData('original_order'); |
|
|
|
|
294
|
|
|
} |
295
|
|
|
return $result; |
|
|
|
|
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
/** |
299
|
|
|
* Compare two menu items by their priority |
300
|
|
|
* |
301
|
|
|
* @param \ElggMenuItem $a Menu item |
302
|
|
|
* @param \ElggMenuItem $b Menu item |
303
|
|
|
* @return bool |
304
|
|
|
* @since 1.9.0 |
305
|
|
|
*/ |
306
|
5 |
|
public static function compareByPriority($a, $b) { |
307
|
5 |
|
$aw = $a->getPriority(); |
308
|
5 |
|
$bw = $b->getPriority(); |
309
|
|
|
|
310
|
5 |
|
if ($aw == $bw) { |
311
|
4 |
|
return $a->getData('original_order') - $b->getData('original_order'); |
|
|
|
|
312
|
|
|
} |
313
|
3 |
|
return $aw - $bw; |
|
|
|
|
314
|
|
|
} |
315
|
|
|
} |
316
|
|
|
|
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 given class or a super-class is assigned to a property that is type hinted more strictly.
Either this assignment is in error or an instanceof check should be added for that assignment.