|
1 <?php |
|
2 |
|
3 /** |
|
4 * @file |
|
5 * API for the Drupal menu system. |
|
6 */ |
|
7 |
|
8 /** |
|
9 * @defgroup menu Menu system |
|
10 * @{ |
|
11 * Define the navigation menus, and route page requests to code based on URLs. |
|
12 * |
|
13 * The Drupal menu system drives both the navigation system from a user |
|
14 * perspective and the callback system that Drupal uses to respond to URLs |
|
15 * passed from the browser. For this reason, a good understanding of the |
|
16 * menu system is fundamental to the creation of complex modules. As a note, |
|
17 * this is related to, but separate from menu.module, which allows menus |
|
18 * (which in this context are hierarchical lists of links) to be customized from |
|
19 * the Drupal administrative interface. |
|
20 * |
|
21 * Drupal's menu system follows a simple hierarchy defined by paths. |
|
22 * Implementations of hook_menu() define menu items and assign them to |
|
23 * paths (which should be unique). The menu system aggregates these items |
|
24 * and determines the menu hierarchy from the paths. For example, if the |
|
25 * paths defined were a, a/b, e, a/b/c/d, f/g, and a/b/h, the menu system |
|
26 * would form the structure: |
|
27 * - a |
|
28 * - a/b |
|
29 * - a/b/c/d |
|
30 * - a/b/h |
|
31 * - e |
|
32 * - f/g |
|
33 * Note that the number of elements in the path does not necessarily |
|
34 * determine the depth of the menu item in the tree. |
|
35 * |
|
36 * When responding to a page request, the menu system looks to see if the |
|
37 * path requested by the browser is registered as a menu item with a |
|
38 * callback. If not, the system searches up the menu tree for the most |
|
39 * complete match with a callback it can find. If the path a/b/i is |
|
40 * requested in the tree above, the callback for a/b would be used. |
|
41 * |
|
42 * The found callback function is called with any arguments specified |
|
43 * in the "page arguments" attribute of its menu item. The |
|
44 * attribute must be an array. After these arguments, any remaining |
|
45 * components of the path are appended as further arguments. In this |
|
46 * way, the callback for a/b above could respond to a request for |
|
47 * a/b/i differently than a request for a/b/j. |
|
48 * |
|
49 * For an illustration of this process, see page_example.module. |
|
50 * |
|
51 * Access to the callback functions is also protected by the menu system. |
|
52 * The "access callback" with an optional "access arguments" of each menu |
|
53 * item is called before the page callback proceeds. If this returns TRUE, |
|
54 * then access is granted; if FALSE, then access is denied. Default local task |
|
55 * menu items (see next paragraph) may omit this attribute to use the value |
|
56 * provided by the parent item. |
|
57 * |
|
58 * In the default Drupal interface, you will notice many links rendered as |
|
59 * tabs. These are known in the menu system as "local tasks", and they are |
|
60 * rendered as tabs by default, though other presentations are possible. |
|
61 * Local tasks function just as other menu items in most respects. It is |
|
62 * convention that the names of these tasks should be short verbs if |
|
63 * possible. In addition, a "default" local task should be provided for |
|
64 * each set. When visiting a local task's parent menu item, the default |
|
65 * local task will be rendered as if it is selected; this provides for a |
|
66 * normal tab user experience. This default task is special in that it |
|
67 * links not to its provided path, but to its parent item's path instead. |
|
68 * The default task's path is only used to place it appropriately in the |
|
69 * menu hierarchy. |
|
70 * |
|
71 * Everything described so far is stored in the menu_router table. The |
|
72 * menu_links table holds the visible menu links. By default these are |
|
73 * derived from the same hook_menu definitions, however you are free to |
|
74 * add more with menu_link_save(). |
|
75 */ |
|
76 |
|
77 /** |
|
78 * @defgroup menu_flags Menu flags |
|
79 * @{ |
|
80 * Flags for use in the "type" attribute of menu items. |
|
81 */ |
|
82 |
|
83 /** |
|
84 * Internal menu flag -- menu item is the root of the menu tree. |
|
85 */ |
|
86 define('MENU_IS_ROOT', 0x0001); |
|
87 |
|
88 /** |
|
89 * Internal menu flag -- menu item is visible in the menu tree. |
|
90 */ |
|
91 define('MENU_VISIBLE_IN_TREE', 0x0002); |
|
92 |
|
93 /** |
|
94 * Internal menu flag -- menu item is visible in the breadcrumb. |
|
95 */ |
|
96 define('MENU_VISIBLE_IN_BREADCRUMB', 0x0004); |
|
97 |
|
98 /** |
|
99 * Internal menu flag -- menu item links back to its parent. |
|
100 */ |
|
101 define('MENU_LINKS_TO_PARENT', 0x0008); |
|
102 |
|
103 /** |
|
104 * Internal menu flag -- menu item can be modified by administrator. |
|
105 */ |
|
106 define('MENU_MODIFIED_BY_ADMIN', 0x0020); |
|
107 |
|
108 /** |
|
109 * Internal menu flag -- menu item was created by administrator. |
|
110 */ |
|
111 define('MENU_CREATED_BY_ADMIN', 0x0040); |
|
112 |
|
113 /** |
|
114 * Internal menu flag -- menu item is a local task. |
|
115 */ |
|
116 define('MENU_IS_LOCAL_TASK', 0x0080); |
|
117 |
|
118 /** |
|
119 * Internal menu flag -- menu item is a local action. |
|
120 */ |
|
121 define('MENU_IS_LOCAL_ACTION', 0x0100); |
|
122 |
|
123 /** |
|
124 * @} End of "Menu flags". |
|
125 */ |
|
126 |
|
127 /** |
|
128 * @defgroup menu_item_types Menu item types |
|
129 * @{ |
|
130 * Definitions for various menu item types. |
|
131 * |
|
132 * Menu item definitions provide one of these constants, which are shortcuts for |
|
133 * combinations of @link menu_flags Menu flags @endlink. |
|
134 */ |
|
135 |
|
136 /** |
|
137 * Menu type -- A "normal" menu item that's shown in menu and breadcrumbs. |
|
138 * |
|
139 * Normal menu items show up in the menu tree and can be moved/hidden by |
|
140 * the administrator. Use this for most menu items. It is the default value if |
|
141 * no menu item type is specified. |
|
142 */ |
|
143 define('MENU_NORMAL_ITEM', MENU_VISIBLE_IN_TREE | MENU_VISIBLE_IN_BREADCRUMB); |
|
144 |
|
145 /** |
|
146 * Menu type -- A hidden, internal callback, typically used for API calls. |
|
147 * |
|
148 * Callbacks simply register a path so that the correct function is fired |
|
149 * when the URL is accessed. They do not appear in menus or breadcrumbs. |
|
150 */ |
|
151 define('MENU_CALLBACK', 0x0000); |
|
152 |
|
153 /** |
|
154 * Menu type -- A normal menu item, hidden until enabled by an administrator. |
|
155 * |
|
156 * Modules may "suggest" menu items that the administrator may enable. They act |
|
157 * just as callbacks do until enabled, at which time they act like normal items. |
|
158 * Note for the value: 0x0010 was a flag which is no longer used, but this way |
|
159 * the values of MENU_CALLBACK and MENU_SUGGESTED_ITEM are separate. |
|
160 */ |
|
161 define('MENU_SUGGESTED_ITEM', MENU_VISIBLE_IN_BREADCRUMB | 0x0010); |
|
162 |
|
163 /** |
|
164 * Menu type -- A task specific to the parent item, usually rendered as a tab. |
|
165 * |
|
166 * Local tasks are menu items that describe actions to be performed on their |
|
167 * parent item. An example is the path "node/52/edit", which performs the |
|
168 * "edit" task on "node/52". |
|
169 */ |
|
170 define('MENU_LOCAL_TASK', MENU_IS_LOCAL_TASK | MENU_VISIBLE_IN_BREADCRUMB); |
|
171 |
|
172 /** |
|
173 * Menu type -- The "default" local task, which is initially active. |
|
174 * |
|
175 * Every set of local tasks should provide one "default" task, that links to the |
|
176 * same path as its parent when clicked. |
|
177 */ |
|
178 define('MENU_DEFAULT_LOCAL_TASK', MENU_IS_LOCAL_TASK | MENU_LINKS_TO_PARENT | MENU_VISIBLE_IN_BREADCRUMB); |
|
179 |
|
180 /** |
|
181 * Menu type -- An action specific to the parent, usually rendered as a link. |
|
182 * |
|
183 * Local actions are menu items that describe actions on the parent item such |
|
184 * as adding a new user, taxonomy term, etc. |
|
185 */ |
|
186 define('MENU_LOCAL_ACTION', MENU_IS_LOCAL_TASK | MENU_IS_LOCAL_ACTION | MENU_VISIBLE_IN_BREADCRUMB); |
|
187 |
|
188 /** |
|
189 * @} End of "Menu item types". |
|
190 */ |
|
191 |
|
192 /** |
|
193 * @defgroup menu_context_types Menu context types |
|
194 * @{ |
|
195 * Flags for use in the "context" attribute of menu router items. |
|
196 */ |
|
197 |
|
198 /** |
|
199 * Internal menu flag: Invisible local task. |
|
200 * |
|
201 * This flag may be used for local tasks like "Delete", so custom modules and |
|
202 * themes can alter the default context and expose the task by altering menu. |
|
203 */ |
|
204 define('MENU_CONTEXT_NONE', 0x0000); |
|
205 |
|
206 /** |
|
207 * Internal menu flag: Local task should be displayed in page context. |
|
208 */ |
|
209 define('MENU_CONTEXT_PAGE', 0x0001); |
|
210 |
|
211 /** |
|
212 * Internal menu flag: Local task should be displayed inline. |
|
213 */ |
|
214 define('MENU_CONTEXT_INLINE', 0x0002); |
|
215 |
|
216 /** |
|
217 * @} End of "Menu context types". |
|
218 */ |
|
219 |
|
220 /** |
|
221 * @defgroup menu_status_codes Menu status codes |
|
222 * @{ |
|
223 * Status codes for menu callbacks. |
|
224 */ |
|
225 |
|
226 /** |
|
227 * Internal menu status code -- Menu item was found. |
|
228 */ |
|
229 define('MENU_FOUND', 1); |
|
230 |
|
231 /** |
|
232 * Menu status code -- Not found. |
|
233 * |
|
234 * This can be used as the return value from a page callback, although it is |
|
235 * preferable to use a load function to accomplish this; see the hook_menu() |
|
236 * documentation for details. |
|
237 */ |
|
238 define('MENU_NOT_FOUND', 2); |
|
239 |
|
240 /** |
|
241 * Menu status code -- Access denied. |
|
242 * |
|
243 * This can be used as the return value from a page callback, although it is |
|
244 * preferable to use an access callback to accomplish this; see the hook_menu() |
|
245 * documentation for details. |
|
246 */ |
|
247 define('MENU_ACCESS_DENIED', 3); |
|
248 |
|
249 /** |
|
250 * Internal menu status code -- Menu item inaccessible because site is offline. |
|
251 */ |
|
252 define('MENU_SITE_OFFLINE', 4); |
|
253 |
|
254 /** |
|
255 * Internal menu status code -- Everything is working fine. |
|
256 */ |
|
257 define('MENU_SITE_ONLINE', 5); |
|
258 |
|
259 /** |
|
260 * @} End of "Menu status codes". |
|
261 */ |
|
262 |
|
263 /** |
|
264 * @defgroup menu_tree_parameters Menu tree parameters |
|
265 * @{ |
|
266 * Parameters for a menu tree. |
|
267 */ |
|
268 |
|
269 /** |
|
270 * The maximum number of path elements for a menu callback |
|
271 */ |
|
272 define('MENU_MAX_PARTS', 9); |
|
273 |
|
274 |
|
275 /** |
|
276 * The maximum depth of a menu links tree - matches the number of p columns. |
|
277 */ |
|
278 define('MENU_MAX_DEPTH', 9); |
|
279 |
|
280 |
|
281 /** |
|
282 * @} End of "Menu tree parameters". |
|
283 */ |
|
284 |
|
285 /** |
|
286 * Reserved key to identify the most specific menu link for a given path. |
|
287 * |
|
288 * The value of this constant is a hash of the constant name. We use the hash |
|
289 * so that the reserved key is over 32 characters in length and will not |
|
290 * collide with allowed menu names: |
|
291 * @code |
|
292 * sha1('MENU_PREFERRED_LINK') = 1cf698d64d1aa4b83907cf6ed55db3a7f8e92c91 |
|
293 * @endcode |
|
294 * |
|
295 * @see menu_link_get_preferred() |
|
296 */ |
|
297 define('MENU_PREFERRED_LINK', '1cf698d64d1aa4b83907cf6ed55db3a7f8e92c91'); |
|
298 |
|
299 /** |
|
300 * Returns the ancestors (and relevant placeholders) for any given path. |
|
301 * |
|
302 * For example, the ancestors of node/12345/edit are: |
|
303 * - node/12345/edit |
|
304 * - node/12345/% |
|
305 * - node/%/edit |
|
306 * - node/%/% |
|
307 * - node/12345 |
|
308 * - node/% |
|
309 * - node |
|
310 * |
|
311 * To generate these, we will use binary numbers. Each bit represents a |
|
312 * part of the path. If the bit is 1, then it represents the original |
|
313 * value while 0 means wildcard. If the path is node/12/edit/foo |
|
314 * then the 1011 bitstring represents node/%/edit/foo where % means that |
|
315 * any argument matches that part. We limit ourselves to using binary |
|
316 * numbers that correspond the patterns of wildcards of router items that |
|
317 * actually exists. This list of 'masks' is built in menu_rebuild(). |
|
318 * |
|
319 * @param $parts |
|
320 * An array of path parts; for the above example, |
|
321 * array('node', '12345', 'edit'). |
|
322 * |
|
323 * @return |
|
324 * An array which contains the ancestors and placeholders. Placeholders |
|
325 * simply contain as many '%s' as the ancestors. |
|
326 */ |
|
327 function menu_get_ancestors($parts) { |
|
328 $number_parts = count($parts); |
|
329 $ancestors = array(); |
|
330 $length = $number_parts - 1; |
|
331 $end = (1 << $number_parts) - 1; |
|
332 $masks = variable_get('menu_masks'); |
|
333 // If the optimized menu_masks array is not available use brute force to get |
|
334 // the correct $ancestors and $placeholders returned. Do not use this as the |
|
335 // default value of the menu_masks variable to avoid building such a big |
|
336 // array. |
|
337 if (!$masks) { |
|
338 $masks = range(511, 1); |
|
339 } |
|
340 // Only examine patterns that actually exist as router items (the masks). |
|
341 foreach ($masks as $i) { |
|
342 if ($i > $end) { |
|
343 // Only look at masks that are not longer than the path of interest. |
|
344 continue; |
|
345 } |
|
346 elseif ($i < (1 << $length)) { |
|
347 // We have exhausted the masks of a given length, so decrease the length. |
|
348 --$length; |
|
349 } |
|
350 $current = ''; |
|
351 for ($j = $length; $j >= 0; $j--) { |
|
352 // Check the bit on the $j offset. |
|
353 if ($i & (1 << $j)) { |
|
354 // Bit one means the original value. |
|
355 $current .= $parts[$length - $j]; |
|
356 } |
|
357 else { |
|
358 // Bit zero means means wildcard. |
|
359 $current .= '%'; |
|
360 } |
|
361 // Unless we are at offset 0, add a slash. |
|
362 if ($j) { |
|
363 $current .= '/'; |
|
364 } |
|
365 } |
|
366 $ancestors[] = $current; |
|
367 } |
|
368 return $ancestors; |
|
369 } |
|
370 |
|
371 /** |
|
372 * Unserializes menu data, using a map to replace path elements. |
|
373 * |
|
374 * The menu system stores various path-related information (such as the 'page |
|
375 * arguments' and 'access arguments' components of a menu item) in the database |
|
376 * using serialized arrays, where integer values in the arrays represent |
|
377 * arguments to be replaced by values from the path. This function first |
|
378 * unserializes such menu information arrays, and then does the path |
|
379 * replacement. |
|
380 * |
|
381 * The path replacement acts on each integer-valued element of the unserialized |
|
382 * menu data array ($data) using a map array ($map, which is typically an array |
|
383 * of path arguments) as a list of replacements. For instance, if there is an |
|
384 * element of $data whose value is the number 2, then it is replaced in $data |
|
385 * with $map[2]; non-integer values in $data are left alone. |
|
386 * |
|
387 * As an example, an unserialized $data array with elements ('node_load', 1) |
|
388 * represents instructions for calling the node_load() function. Specifically, |
|
389 * this instruction says to use the path component at index 1 as the input |
|
390 * parameter to node_load(). If the path is 'node/123', then $map will be the |
|
391 * array ('node', 123), and the returned array from this function will have |
|
392 * elements ('node_load', 123), since $map[1] is 123. This return value will |
|
393 * indicate specifically that node_load(123) is to be called to load the node |
|
394 * whose ID is 123 for this menu item. |
|
395 * |
|
396 * @param $data |
|
397 * A serialized array of menu data, as read from the database. |
|
398 * @param $map |
|
399 * A path argument array, used to replace integer values in $data; an integer |
|
400 * value N in $data will be replaced by value $map[N]. Typically, the $map |
|
401 * array is generated from a call to the arg() function. |
|
402 * |
|
403 * @return |
|
404 * The unserialized $data array, with path arguments replaced. |
|
405 */ |
|
406 function menu_unserialize($data, $map) { |
|
407 if ($data = unserialize($data)) { |
|
408 foreach ($data as $k => $v) { |
|
409 if (is_int($v)) { |
|
410 $data[$k] = isset($map[$v]) ? $map[$v] : ''; |
|
411 } |
|
412 } |
|
413 return $data; |
|
414 } |
|
415 else { |
|
416 return array(); |
|
417 } |
|
418 } |
|
419 |
|
420 |
|
421 |
|
422 /** |
|
423 * Replaces the statically cached item for a given path. |
|
424 * |
|
425 * @param $path |
|
426 * The path. |
|
427 * @param $router_item |
|
428 * The router item. Usually a router entry from menu_get_item() is either |
|
429 * modified or set to a different path. This allows the navigation block, |
|
430 * the page title, the breadcrumb, and the page help to be modified in one |
|
431 * call. |
|
432 */ |
|
433 function menu_set_item($path, $router_item) { |
|
434 menu_get_item($path, $router_item); |
|
435 } |
|
436 |
|
437 /** |
|
438 * Gets a router item. |
|
439 * |
|
440 * @param $path |
|
441 * The path; for example, 'node/5'. The function will find the corresponding |
|
442 * node/% item and return that. Defaults to the current path. |
|
443 * @param $router_item |
|
444 * Internal use only. |
|
445 * |
|
446 * @return |
|
447 * The router item or, if an error occurs in _menu_translate(), FALSE. A |
|
448 * router item is an associative array corresponding to one row in the |
|
449 * menu_router table. The value corresponding to the key 'map' holds the |
|
450 * loaded objects. The value corresponding to the key 'access' is TRUE if the |
|
451 * current user can access this page. The values corresponding to the keys |
|
452 * 'title', 'page_arguments', 'access_arguments', and 'theme_arguments' will |
|
453 * be filled in based on the database values and the objects loaded. |
|
454 */ |
|
455 function menu_get_item($path = NULL, $router_item = NULL) { |
|
456 $router_items = &drupal_static(__FUNCTION__); |
|
457 if (!isset($path)) { |
|
458 $path = $_GET['q']; |
|
459 } |
|
460 if (isset($router_item)) { |
|
461 $router_items[$path] = $router_item; |
|
462 } |
|
463 if (!isset($router_items[$path])) { |
|
464 // Rebuild if we know it's needed, or if the menu masks are missing which |
|
465 // occurs rarely, likely due to a race condition of multiple rebuilds. |
|
466 if (variable_get('menu_rebuild_needed', FALSE) || !variable_get('menu_masks', array())) { |
|
467 if (_menu_check_rebuild()) { |
|
468 menu_rebuild(); |
|
469 } |
|
470 } |
|
471 $original_map = arg(NULL, $path); |
|
472 |
|
473 $parts = array_slice($original_map, 0, MENU_MAX_PARTS); |
|
474 $ancestors = menu_get_ancestors($parts); |
|
475 $router_item = db_query_range('SELECT * FROM {menu_router} WHERE path IN (:ancestors) ORDER BY fit DESC', 0, 1, array(':ancestors' => $ancestors))->fetchAssoc(); |
|
476 |
|
477 if ($router_item) { |
|
478 // Allow modules to alter the router item before it is translated and |
|
479 // checked for access. |
|
480 drupal_alter('menu_get_item', $router_item, $path, $original_map); |
|
481 |
|
482 $map = _menu_translate($router_item, $original_map); |
|
483 $router_item['original_map'] = $original_map; |
|
484 if ($map === FALSE) { |
|
485 $router_items[$path] = FALSE; |
|
486 return FALSE; |
|
487 } |
|
488 if ($router_item['access']) { |
|
489 $router_item['map'] = $map; |
|
490 $router_item['page_arguments'] = array_merge(menu_unserialize($router_item['page_arguments'], $map), array_slice($map, $router_item['number_parts'])); |
|
491 $router_item['theme_arguments'] = array_merge(menu_unserialize($router_item['theme_arguments'], $map), array_slice($map, $router_item['number_parts'])); |
|
492 } |
|
493 } |
|
494 $router_items[$path] = $router_item; |
|
495 } |
|
496 return $router_items[$path]; |
|
497 } |
|
498 |
|
499 /** |
|
500 * Execute the page callback associated with the current path. |
|
501 * |
|
502 * @param $path |
|
503 * The drupal path whose handler is to be be executed. If set to NULL, then |
|
504 * the current path is used. |
|
505 * @param $deliver |
|
506 * (optional) A boolean to indicate whether the content should be sent to the |
|
507 * browser using the appropriate delivery callback (TRUE) or whether to return |
|
508 * the result to the caller (FALSE). |
|
509 */ |
|
510 function menu_execute_active_handler($path = NULL, $deliver = TRUE) { |
|
511 // Check if site is offline. |
|
512 $page_callback_result = _menu_site_is_offline() ? MENU_SITE_OFFLINE : MENU_SITE_ONLINE; |
|
513 |
|
514 // Allow other modules to change the site status but not the path because that |
|
515 // would not change the global variable. hook_url_inbound_alter() can be used |
|
516 // to change the path. Code later will not use the $read_only_path variable. |
|
517 $read_only_path = !empty($path) ? $path : $_GET['q']; |
|
518 drupal_alter('menu_site_status', $page_callback_result, $read_only_path); |
|
519 |
|
520 // Only continue if the site status is not set. |
|
521 if ($page_callback_result == MENU_SITE_ONLINE) { |
|
522 if ($router_item = menu_get_item($path)) { |
|
523 if ($router_item['access']) { |
|
524 if ($router_item['include_file']) { |
|
525 require_once DRUPAL_ROOT . '/' . $router_item['include_file']; |
|
526 } |
|
527 $page_callback_result = call_user_func_array($router_item['page_callback'], $router_item['page_arguments']); |
|
528 } |
|
529 else { |
|
530 $page_callback_result = MENU_ACCESS_DENIED; |
|
531 } |
|
532 } |
|
533 else { |
|
534 $page_callback_result = MENU_NOT_FOUND; |
|
535 } |
|
536 } |
|
537 |
|
538 // Deliver the result of the page callback to the browser, or if requested, |
|
539 // return it raw, so calling code can do more processing. |
|
540 if ($deliver) { |
|
541 $default_delivery_callback = (isset($router_item) && $router_item) ? $router_item['delivery_callback'] : NULL; |
|
542 drupal_deliver_page($page_callback_result, $default_delivery_callback); |
|
543 } |
|
544 else { |
|
545 return $page_callback_result; |
|
546 } |
|
547 } |
|
548 |
|
549 /** |
|
550 * Loads objects into the map as defined in the $item['load_functions']. |
|
551 * |
|
552 * @param $item |
|
553 * A menu router or menu link item |
|
554 * @param $map |
|
555 * An array of path arguments; for example, array('node', '5'). |
|
556 * |
|
557 * @return |
|
558 * Returns TRUE for success, FALSE if an object cannot be loaded. |
|
559 * Names of object loading functions are placed in $item['load_functions']. |
|
560 * Loaded objects are placed in $map[]; keys are the same as keys in the |
|
561 * $item['load_functions'] array. |
|
562 * $item['access'] is set to FALSE if an object cannot be loaded. |
|
563 */ |
|
564 function _menu_load_objects(&$item, &$map) { |
|
565 if ($load_functions = $item['load_functions']) { |
|
566 // If someone calls this function twice, then unserialize will fail. |
|
567 if (!is_array($load_functions)) { |
|
568 $load_functions = unserialize($load_functions); |
|
569 } |
|
570 $path_map = $map; |
|
571 foreach ($load_functions as $index => $function) { |
|
572 if ($function) { |
|
573 $value = isset($path_map[$index]) ? $path_map[$index] : ''; |
|
574 if (is_array($function)) { |
|
575 // Set up arguments for the load function. These were pulled from |
|
576 // 'load arguments' in the hook_menu() entry, but they need |
|
577 // some processing. In this case the $function is the key to the |
|
578 // load_function array, and the value is the list of arguments. |
|
579 list($function, $args) = each($function); |
|
580 $load_functions[$index] = $function; |
|
581 |
|
582 // Some arguments are placeholders for dynamic items to process. |
|
583 foreach ($args as $i => $arg) { |
|
584 if ($arg === '%index') { |
|
585 // Pass on argument index to the load function, so multiple |
|
586 // occurrences of the same placeholder can be identified. |
|
587 $args[$i] = $index; |
|
588 } |
|
589 if ($arg === '%map') { |
|
590 // Pass on menu map by reference. The accepting function must |
|
591 // also declare this as a reference if it wants to modify |
|
592 // the map. |
|
593 $args[$i] = &$map; |
|
594 } |
|
595 if (is_int($arg)) { |
|
596 $args[$i] = isset($path_map[$arg]) ? $path_map[$arg] : ''; |
|
597 } |
|
598 } |
|
599 array_unshift($args, $value); |
|
600 $return = call_user_func_array($function, $args); |
|
601 } |
|
602 else { |
|
603 $return = $function($value); |
|
604 } |
|
605 // If callback returned an error or there is no callback, trigger 404. |
|
606 if ($return === FALSE) { |
|
607 $item['access'] = FALSE; |
|
608 $map = FALSE; |
|
609 return FALSE; |
|
610 } |
|
611 $map[$index] = $return; |
|
612 } |
|
613 } |
|
614 $item['load_functions'] = $load_functions; |
|
615 } |
|
616 return TRUE; |
|
617 } |
|
618 |
|
619 /** |
|
620 * Checks access to a menu item using the access callback. |
|
621 * |
|
622 * @param $item |
|
623 * A menu router or menu link item |
|
624 * @param $map |
|
625 * An array of path arguments; for example, array('node', '5'). |
|
626 * |
|
627 * @return |
|
628 * $item['access'] becomes TRUE if the item is accessible, FALSE otherwise. |
|
629 */ |
|
630 function _menu_check_access(&$item, $map) { |
|
631 $item['access'] = FALSE; |
|
632 // Determine access callback, which will decide whether or not the current |
|
633 // user has access to this path. |
|
634 $callback = empty($item['access_callback']) ? 0 : trim($item['access_callback']); |
|
635 // Check for a TRUE or FALSE value. |
|
636 if (is_numeric($callback)) { |
|
637 $item['access'] = (bool) $callback; |
|
638 } |
|
639 else { |
|
640 $arguments = menu_unserialize($item['access_arguments'], $map); |
|
641 // As call_user_func_array is quite slow and user_access is a very common |
|
642 // callback, it is worth making a special case for it. |
|
643 if ($callback == 'user_access') { |
|
644 $item['access'] = (count($arguments) == 1) ? user_access($arguments[0]) : user_access($arguments[0], $arguments[1]); |
|
645 } |
|
646 elseif (function_exists($callback)) { |
|
647 $item['access'] = call_user_func_array($callback, $arguments); |
|
648 } |
|
649 } |
|
650 } |
|
651 |
|
652 /** |
|
653 * Localizes the router item title using t() or another callback. |
|
654 * |
|
655 * Translate the title and description to allow storage of English title |
|
656 * strings in the database, yet display of them in the language required |
|
657 * by the current user. |
|
658 * |
|
659 * @param $item |
|
660 * A menu router item or a menu link item. |
|
661 * @param $map |
|
662 * The path as an array with objects already replaced. E.g., for path |
|
663 * node/123 $map would be array('node', $node) where $node is the node |
|
664 * object for node 123. |
|
665 * @param $link_translate |
|
666 * TRUE if we are translating a menu link item; FALSE if we are |
|
667 * translating a menu router item. |
|
668 * |
|
669 * @return |
|
670 * No return value. |
|
671 * $item['title'] is localized according to $item['title_callback']. |
|
672 * If an item's callback is check_plain(), $item['options']['html'] becomes |
|
673 * TRUE. |
|
674 * $item['description'] is translated using t(). |
|
675 * When doing link translation and the $item['options']['attributes']['title'] |
|
676 * (link title attribute) matches the description, it is translated as well. |
|
677 */ |
|
678 function _menu_item_localize(&$item, $map, $link_translate = FALSE) { |
|
679 $callback = $item['title_callback']; |
|
680 $item['localized_options'] = $item['options']; |
|
681 // All 'class' attributes are assumed to be an array during rendering, but |
|
682 // links stored in the database may use an old string value. |
|
683 // @todo In order to remove this code we need to implement a database update |
|
684 // including unserializing all existing link options and running this code |
|
685 // on them, as well as adding validation to menu_link_save(). |
|
686 if (isset($item['options']['attributes']['class']) && is_string($item['options']['attributes']['class'])) { |
|
687 $item['localized_options']['attributes']['class'] = explode(' ', $item['options']['attributes']['class']); |
|
688 } |
|
689 // If we are translating the title of a menu link, and its title is the same |
|
690 // as the corresponding router item, then we can use the title information |
|
691 // from the router. If it's customized, then we need to use the link title |
|
692 // itself; can't localize. |
|
693 // If we are translating a router item (tabs, page, breadcrumb), then we |
|
694 // can always use the information from the router item. |
|
695 if (!$link_translate || ($item['title'] == $item['link_title'])) { |
|
696 // t() is a special case. Since it is used very close to all the time, |
|
697 // we handle it directly instead of using indirect, slower methods. |
|
698 if ($callback == 't') { |
|
699 if (empty($item['title_arguments'])) { |
|
700 $item['title'] = t($item['title']); |
|
701 } |
|
702 else { |
|
703 $item['title'] = t($item['title'], menu_unserialize($item['title_arguments'], $map)); |
|
704 } |
|
705 } |
|
706 elseif ($callback && function_exists($callback)) { |
|
707 if (empty($item['title_arguments'])) { |
|
708 $item['title'] = $callback($item['title']); |
|
709 } |
|
710 else { |
|
711 $item['title'] = call_user_func_array($callback, menu_unserialize($item['title_arguments'], $map)); |
|
712 } |
|
713 // Avoid calling check_plain again on l() function. |
|
714 if ($callback == 'check_plain') { |
|
715 $item['localized_options']['html'] = TRUE; |
|
716 } |
|
717 } |
|
718 } |
|
719 elseif ($link_translate) { |
|
720 $item['title'] = $item['link_title']; |
|
721 } |
|
722 |
|
723 // Translate description, see the motivation above. |
|
724 if (!empty($item['description'])) { |
|
725 $original_description = $item['description']; |
|
726 $item['description'] = t($item['description']); |
|
727 if ($link_translate && isset($item['options']['attributes']['title']) && $item['options']['attributes']['title'] == $original_description) { |
|
728 $item['localized_options']['attributes']['title'] = $item['description']; |
|
729 } |
|
730 } |
|
731 } |
|
732 |
|
733 /** |
|
734 * Handles dynamic path translation and menu access control. |
|
735 * |
|
736 * When a user arrives on a page such as node/5, this function determines |
|
737 * what "5" corresponds to, by inspecting the page's menu path definition, |
|
738 * node/%node. This will call node_load(5) to load the corresponding node |
|
739 * object. |
|
740 * |
|
741 * It also works in reverse, to allow the display of tabs and menu items which |
|
742 * contain these dynamic arguments, translating node/%node to node/5. |
|
743 * |
|
744 * Translation of menu item titles and descriptions are done here to |
|
745 * allow for storage of English strings in the database, and translation |
|
746 * to the language required to generate the current page. |
|
747 * |
|
748 * @param $router_item |
|
749 * A menu router item |
|
750 * @param $map |
|
751 * An array of path arguments; for example, array('node', '5'). |
|
752 * @param $to_arg |
|
753 * Execute $item['to_arg_functions'] or not. Use only if you want to render a |
|
754 * path from the menu table, for example tabs. |
|
755 * |
|
756 * @return |
|
757 * Returns the map with objects loaded as defined in the |
|
758 * $item['load_functions']. $item['access'] becomes TRUE if the item is |
|
759 * accessible, FALSE otherwise. $item['href'] is set according to the map. |
|
760 * If an error occurs during calling the load_functions (like trying to load |
|
761 * a non-existent node) then this function returns FALSE. |
|
762 */ |
|
763 function _menu_translate(&$router_item, $map, $to_arg = FALSE) { |
|
764 if ($to_arg && !empty($router_item['to_arg_functions'])) { |
|
765 // Fill in missing path elements, such as the current uid. |
|
766 _menu_link_map_translate($map, $router_item['to_arg_functions']); |
|
767 } |
|
768 // The $path_map saves the pieces of the path as strings, while elements in |
|
769 // $map may be replaced with loaded objects. |
|
770 $path_map = $map; |
|
771 if (!empty($router_item['load_functions']) && !_menu_load_objects($router_item, $map)) { |
|
772 // An error occurred loading an object. |
|
773 $router_item['access'] = FALSE; |
|
774 return FALSE; |
|
775 } |
|
776 |
|
777 // Generate the link path for the page request or local tasks. |
|
778 $link_map = explode('/', $router_item['path']); |
|
779 if (isset($router_item['tab_root'])) { |
|
780 $tab_root_map = explode('/', $router_item['tab_root']); |
|
781 } |
|
782 if (isset($router_item['tab_parent'])) { |
|
783 $tab_parent_map = explode('/', $router_item['tab_parent']); |
|
784 } |
|
785 for ($i = 0; $i < $router_item['number_parts']; $i++) { |
|
786 if ($link_map[$i] == '%') { |
|
787 $link_map[$i] = $path_map[$i]; |
|
788 } |
|
789 if (isset($tab_root_map[$i]) && $tab_root_map[$i] == '%') { |
|
790 $tab_root_map[$i] = $path_map[$i]; |
|
791 } |
|
792 if (isset($tab_parent_map[$i]) && $tab_parent_map[$i] == '%') { |
|
793 $tab_parent_map[$i] = $path_map[$i]; |
|
794 } |
|
795 } |
|
796 $router_item['href'] = implode('/', $link_map); |
|
797 $router_item['tab_root_href'] = implode('/', $tab_root_map); |
|
798 $router_item['tab_parent_href'] = implode('/', $tab_parent_map); |
|
799 $router_item['options'] = array(); |
|
800 _menu_check_access($router_item, $map); |
|
801 |
|
802 // For performance, don't localize an item the user can't access. |
|
803 if ($router_item['access']) { |
|
804 _menu_item_localize($router_item, $map); |
|
805 } |
|
806 |
|
807 return $map; |
|
808 } |
|
809 |
|
810 /** |
|
811 * Translates the path elements in the map using any to_arg helper function. |
|
812 * |
|
813 * @param $map |
|
814 * An array of path arguments; for example, array('node', '5'). |
|
815 * @param $to_arg_functions |
|
816 * An array of helper functions; for example, array(2 => 'menu_tail_to_arg'). |
|
817 * |
|
818 * @see hook_menu() |
|
819 */ |
|
820 function _menu_link_map_translate(&$map, $to_arg_functions) { |
|
821 $to_arg_functions = unserialize($to_arg_functions); |
|
822 foreach ($to_arg_functions as $index => $function) { |
|
823 // Translate place-holders into real values. |
|
824 $arg = $function(!empty($map[$index]) ? $map[$index] : '', $map, $index); |
|
825 if (!empty($map[$index]) || isset($arg)) { |
|
826 $map[$index] = $arg; |
|
827 } |
|
828 else { |
|
829 unset($map[$index]); |
|
830 } |
|
831 } |
|
832 } |
|
833 |
|
834 /** |
|
835 * Returns a string containing the path relative to the current index. |
|
836 */ |
|
837 function menu_tail_to_arg($arg, $map, $index) { |
|
838 return implode('/', array_slice($map, $index)); |
|
839 } |
|
840 |
|
841 /** |
|
842 * Loads the path as one string relative to the current index. |
|
843 * |
|
844 * To use this load function, you must specify the load arguments |
|
845 * in the router item as: |
|
846 * @code |
|
847 * $item['load arguments'] = array('%map', '%index'); |
|
848 * @endcode |
|
849 * |
|
850 * @see search_menu(). |
|
851 */ |
|
852 function menu_tail_load($arg, &$map, $index) { |
|
853 $arg = implode('/', array_slice($map, $index)); |
|
854 $map = array_slice($map, 0, $index); |
|
855 return $arg; |
|
856 } |
|
857 |
|
858 /** |
|
859 * Provides menu link access control, translation, and argument handling. |
|
860 * |
|
861 * This function is similar to _menu_translate(), but it also does |
|
862 * link-specific preparation (such as always calling to_arg() functions). |
|
863 * |
|
864 * @param $item |
|
865 * A menu link. |
|
866 * @param $translate |
|
867 * (optional) Whether to try to translate a link containing dynamic path |
|
868 * argument placeholders (%) based on the menu router item of the current |
|
869 * path. Defaults to FALSE. Internally used for breadcrumbs. |
|
870 * |
|
871 * @return |
|
872 * Returns the map of path arguments with objects loaded as defined in the |
|
873 * $item['load_functions']. |
|
874 * $item['access'] becomes TRUE if the item is accessible, FALSE otherwise. |
|
875 * $item['href'] is generated from link_path, possibly by to_arg functions. |
|
876 * $item['title'] is generated from link_title, and may be localized. |
|
877 * $item['options'] is unserialized; it is also changed within the call here |
|
878 * to $item['localized_options'] by _menu_item_localize(). |
|
879 */ |
|
880 function _menu_link_translate(&$item, $translate = FALSE) { |
|
881 if (!is_array($item['options'])) { |
|
882 $item['options'] = unserialize($item['options']); |
|
883 } |
|
884 if ($item['external']) { |
|
885 $item['access'] = 1; |
|
886 $map = array(); |
|
887 $item['href'] = $item['link_path']; |
|
888 $item['title'] = $item['link_title']; |
|
889 $item['localized_options'] = $item['options']; |
|
890 } |
|
891 else { |
|
892 // Complete the path of the menu link with elements from the current path, |
|
893 // if it contains dynamic placeholders (%). |
|
894 $map = explode('/', $item['link_path']); |
|
895 if (strpos($item['link_path'], '%') !== FALSE) { |
|
896 // Invoke registered to_arg callbacks. |
|
897 if (!empty($item['to_arg_functions'])) { |
|
898 _menu_link_map_translate($map, $item['to_arg_functions']); |
|
899 } |
|
900 // Or try to derive the path argument map from the current router item, |
|
901 // if this $item's path is within the router item's path. This means |
|
902 // that if we are on the current path 'foo/%/bar/%/baz', then |
|
903 // menu_get_item() will have translated the menu router item for the |
|
904 // current path, and we can take over the argument map for a link like |
|
905 // 'foo/%/bar'. This inheritance is only valid for breadcrumb links. |
|
906 // @see _menu_tree_check_access() |
|
907 // @see menu_get_active_breadcrumb() |
|
908 elseif ($translate && ($current_router_item = menu_get_item())) { |
|
909 // If $translate is TRUE, then this link is in the active trail. |
|
910 // Only translate paths within the current path. |
|
911 if (strpos($current_router_item['path'], $item['link_path']) === 0) { |
|
912 $count = count($map); |
|
913 $map = array_slice($current_router_item['original_map'], 0, $count); |
|
914 $item['original_map'] = $map; |
|
915 if (isset($current_router_item['map'])) { |
|
916 $item['map'] = array_slice($current_router_item['map'], 0, $count); |
|
917 } |
|
918 // Reset access to check it (for the first time). |
|
919 unset($item['access']); |
|
920 } |
|
921 } |
|
922 } |
|
923 $item['href'] = implode('/', $map); |
|
924 |
|
925 // Skip links containing untranslated arguments. |
|
926 if (strpos($item['href'], '%') !== FALSE) { |
|
927 $item['access'] = FALSE; |
|
928 return FALSE; |
|
929 } |
|
930 // menu_tree_check_access() may set this ahead of time for links to nodes. |
|
931 if (!isset($item['access'])) { |
|
932 if (!empty($item['load_functions']) && !_menu_load_objects($item, $map)) { |
|
933 // An error occurred loading an object. |
|
934 $item['access'] = FALSE; |
|
935 return FALSE; |
|
936 } |
|
937 _menu_check_access($item, $map); |
|
938 } |
|
939 // For performance, don't localize a link the user can't access. |
|
940 if ($item['access']) { |
|
941 _menu_item_localize($item, $map, TRUE); |
|
942 } |
|
943 } |
|
944 |
|
945 // Allow other customizations - e.g. adding a page-specific query string to the |
|
946 // options array. For performance reasons we only invoke this hook if the link |
|
947 // has the 'alter' flag set in the options array. |
|
948 if (!empty($item['options']['alter'])) { |
|
949 drupal_alter('translated_menu_link', $item, $map); |
|
950 } |
|
951 |
|
952 return $map; |
|
953 } |
|
954 |
|
955 /** |
|
956 * Gets a loaded object from a router item. |
|
957 * |
|
958 * menu_get_object() provides access to objects loaded by the current router |
|
959 * item. For example, on the page node/%node, the router loads the %node object, |
|
960 * and calling menu_get_object() will return that. Normally, it is necessary to |
|
961 * specify the type of object referenced, however node is the default. |
|
962 * The following example tests to see whether the node being displayed is of the |
|
963 * "story" content type: |
|
964 * @code |
|
965 * $node = menu_get_object(); |
|
966 * $story = $node->type == 'story'; |
|
967 * @endcode |
|
968 * |
|
969 * @param $type |
|
970 * Type of the object. These appear in hook_menu definitions as %type. Core |
|
971 * provides aggregator_feed, aggregator_category, contact, filter_format, |
|
972 * forum_term, menu, menu_link, node, taxonomy_vocabulary, user. See the |
|
973 * relevant {$type}_load function for more on each. Defaults to node. |
|
974 * @param $position |
|
975 * The position of the object in the path, where the first path segment is 0. |
|
976 * For node/%node, the position of %node is 1, but for comment/reply/%node, |
|
977 * it's 2. Defaults to 1. |
|
978 * @param $path |
|
979 * See menu_get_item() for more on this. Defaults to the current path. |
|
980 */ |
|
981 function menu_get_object($type = 'node', $position = 1, $path = NULL) { |
|
982 $router_item = menu_get_item($path); |
|
983 if (isset($router_item['load_functions'][$position]) && !empty($router_item['map'][$position]) && $router_item['load_functions'][$position] == $type . '_load') { |
|
984 return $router_item['map'][$position]; |
|
985 } |
|
986 } |
|
987 |
|
988 /** |
|
989 * Renders a menu tree based on the current path. |
|
990 * |
|
991 * The tree is expanded based on the current path and dynamic paths are also |
|
992 * changed according to the defined to_arg functions (for example the 'My |
|
993 * account' link is changed from user/% to a link with the current user's uid). |
|
994 * |
|
995 * @param $menu_name |
|
996 * The name of the menu. |
|
997 * |
|
998 * @return |
|
999 * A structured array representing the specified menu on the current page, to |
|
1000 * be rendered by drupal_render(). |
|
1001 */ |
|
1002 function menu_tree($menu_name) { |
|
1003 $menu_output = &drupal_static(__FUNCTION__, array()); |
|
1004 |
|
1005 if (!isset($menu_output[$menu_name])) { |
|
1006 $tree = menu_tree_page_data($menu_name); |
|
1007 $menu_output[$menu_name] = menu_tree_output($tree); |
|
1008 } |
|
1009 return $menu_output[$menu_name]; |
|
1010 } |
|
1011 |
|
1012 /** |
|
1013 * Returns an output structure for rendering a menu tree. |
|
1014 * |
|
1015 * The menu item's LI element is given one of the following classes: |
|
1016 * - expanded: The menu item is showing its submenu. |
|
1017 * - collapsed: The menu item has a submenu which is not shown. |
|
1018 * - leaf: The menu item has no submenu. |
|
1019 * |
|
1020 * @param $tree |
|
1021 * A data structure representing the tree as returned from menu_tree_data. |
|
1022 * |
|
1023 * @return |
|
1024 * A structured array to be rendered by drupal_render(). |
|
1025 */ |
|
1026 function menu_tree_output($tree) { |
|
1027 $build = array(); |
|
1028 $items = array(); |
|
1029 |
|
1030 // Pull out just the menu links we are going to render so that we |
|
1031 // get an accurate count for the first/last classes. |
|
1032 foreach ($tree as $data) { |
|
1033 if ($data['link']['access'] && !$data['link']['hidden']) { |
|
1034 $items[] = $data; |
|
1035 } |
|
1036 } |
|
1037 |
|
1038 $router_item = menu_get_item(); |
|
1039 $num_items = count($items); |
|
1040 foreach ($items as $i => $data) { |
|
1041 $class = array(); |
|
1042 if ($i == 0) { |
|
1043 $class[] = 'first'; |
|
1044 } |
|
1045 if ($i == $num_items - 1) { |
|
1046 $class[] = 'last'; |
|
1047 } |
|
1048 // Set a class for the <li>-tag. Since $data['below'] may contain local |
|
1049 // tasks, only set 'expanded' class if the link also has children within |
|
1050 // the current menu. |
|
1051 if ($data['link']['has_children'] && $data['below']) { |
|
1052 $class[] = 'expanded'; |
|
1053 } |
|
1054 elseif ($data['link']['has_children']) { |
|
1055 $class[] = 'collapsed'; |
|
1056 } |
|
1057 else { |
|
1058 $class[] = 'leaf'; |
|
1059 } |
|
1060 // Set a class if the link is in the active trail. |
|
1061 if ($data['link']['in_active_trail']) { |
|
1062 $class[] = 'active-trail'; |
|
1063 $data['link']['localized_options']['attributes']['class'][] = 'active-trail'; |
|
1064 } |
|
1065 // Normally, l() compares the href of every link with $_GET['q'] and sets |
|
1066 // the active class accordingly. But local tasks do not appear in menu |
|
1067 // trees, so if the current path is a local task, and this link is its |
|
1068 // tab root, then we have to set the class manually. |
|
1069 if ($data['link']['href'] == $router_item['tab_root_href'] && $data['link']['href'] != $_GET['q']) { |
|
1070 $data['link']['localized_options']['attributes']['class'][] = 'active'; |
|
1071 } |
|
1072 |
|
1073 // Allow menu-specific theme overrides. |
|
1074 $element['#theme'] = 'menu_link__' . strtr($data['link']['menu_name'], '-', '_'); |
|
1075 $element['#attributes']['class'] = $class; |
|
1076 $element['#title'] = $data['link']['title']; |
|
1077 $element['#href'] = $data['link']['href']; |
|
1078 $element['#localized_options'] = !empty($data['link']['localized_options']) ? $data['link']['localized_options'] : array(); |
|
1079 $element['#below'] = $data['below'] ? menu_tree_output($data['below']) : $data['below']; |
|
1080 $element['#original_link'] = $data['link']; |
|
1081 // Index using the link's unique mlid. |
|
1082 $build[$data['link']['mlid']] = $element; |
|
1083 } |
|
1084 if ($build) { |
|
1085 // Make sure drupal_render() does not re-order the links. |
|
1086 $build['#sorted'] = TRUE; |
|
1087 // Add the theme wrapper for outer markup. |
|
1088 // Allow menu-specific theme overrides. |
|
1089 $build['#theme_wrappers'][] = 'menu_tree__' . strtr($data['link']['menu_name'], '-', '_'); |
|
1090 } |
|
1091 |
|
1092 return $build; |
|
1093 } |
|
1094 |
|
1095 /** |
|
1096 * Gets the data structure representing a named menu tree. |
|
1097 * |
|
1098 * Since this can be the full tree including hidden items, the data returned |
|
1099 * may be used for generating an an admin interface or a select. |
|
1100 * |
|
1101 * @param $menu_name |
|
1102 * The named menu links to return |
|
1103 * @param $link |
|
1104 * A fully loaded menu link, or NULL. If a link is supplied, only the |
|
1105 * path to root will be included in the returned tree - as if this link |
|
1106 * represented the current page in a visible menu. |
|
1107 * @param $max_depth |
|
1108 * Optional maximum depth of links to retrieve. Typically useful if only one |
|
1109 * or two levels of a sub tree are needed in conjunction with a non-NULL |
|
1110 * $link, in which case $max_depth should be greater than $link['depth']. |
|
1111 * |
|
1112 * @return |
|
1113 * An tree of menu links in an array, in the order they should be rendered. |
|
1114 */ |
|
1115 function menu_tree_all_data($menu_name, $link = NULL, $max_depth = NULL) { |
|
1116 $tree = &drupal_static(__FUNCTION__, array()); |
|
1117 |
|
1118 // Use $mlid as a flag for whether the data being loaded is for the whole tree. |
|
1119 $mlid = isset($link['mlid']) ? $link['mlid'] : 0; |
|
1120 // Generate a cache ID (cid) specific for this $menu_name, $link, $language, and depth. |
|
1121 $cid = 'links:' . $menu_name . ':all:' . $mlid . ':' . $GLOBALS['language']->language . ':' . (int) $max_depth; |
|
1122 |
|
1123 if (!isset($tree[$cid])) { |
|
1124 // If the static variable doesn't have the data, check {cache_menu}. |
|
1125 $cache = cache_get($cid, 'cache_menu'); |
|
1126 if ($cache && isset($cache->data)) { |
|
1127 // If the cache entry exists, it contains the parameters for |
|
1128 // menu_build_tree(). |
|
1129 $tree_parameters = $cache->data; |
|
1130 } |
|
1131 // If the tree data was not in the cache, build $tree_parameters. |
|
1132 if (!isset($tree_parameters)) { |
|
1133 $tree_parameters = array( |
|
1134 'min_depth' => 1, |
|
1135 'max_depth' => $max_depth, |
|
1136 ); |
|
1137 if ($mlid) { |
|
1138 // The tree is for a single item, so we need to match the values in its |
|
1139 // p columns and 0 (the top level) with the plid values of other links. |
|
1140 $parents = array(0); |
|
1141 for ($i = 1; $i < MENU_MAX_DEPTH; $i++) { |
|
1142 if (!empty($link["p$i"])) { |
|
1143 $parents[] = $link["p$i"]; |
|
1144 } |
|
1145 } |
|
1146 $tree_parameters['expanded'] = $parents; |
|
1147 $tree_parameters['active_trail'] = $parents; |
|
1148 $tree_parameters['active_trail'][] = $mlid; |
|
1149 } |
|
1150 |
|
1151 // Cache the tree building parameters using the page-specific cid. |
|
1152 cache_set($cid, $tree_parameters, 'cache_menu'); |
|
1153 } |
|
1154 |
|
1155 // Build the tree using the parameters; the resulting tree will be cached |
|
1156 // by _menu_build_tree()). |
|
1157 $tree[$cid] = menu_build_tree($menu_name, $tree_parameters); |
|
1158 } |
|
1159 |
|
1160 return $tree[$cid]; |
|
1161 } |
|
1162 |
|
1163 /** |
|
1164 * Sets the path for determining the active trail of the specified menu tree. |
|
1165 * |
|
1166 * This path will also affect the breadcrumbs under some circumstances. |
|
1167 * Breadcrumbs are built using the preferred link returned by |
|
1168 * menu_link_get_preferred(). If the preferred link is inside one of the menus |
|
1169 * specified in calls to menu_tree_set_path(), the preferred link will be |
|
1170 * overridden by the corresponding path returned by menu_tree_get_path(). |
|
1171 * |
|
1172 * Setting this path does not affect the main content; for that use |
|
1173 * menu_set_active_item() instead. |
|
1174 * |
|
1175 * @param $menu_name |
|
1176 * The name of the affected menu tree. |
|
1177 * @param $path |
|
1178 * The path to use when finding the active trail. |
|
1179 */ |
|
1180 function menu_tree_set_path($menu_name, $path = NULL) { |
|
1181 $paths = &drupal_static(__FUNCTION__); |
|
1182 if (isset($path)) { |
|
1183 $paths[$menu_name] = $path; |
|
1184 } |
|
1185 return isset($paths[$menu_name]) ? $paths[$menu_name] : NULL; |
|
1186 } |
|
1187 |
|
1188 /** |
|
1189 * Gets the path for determining the active trail of the specified menu tree. |
|
1190 * |
|
1191 * @param $menu_name |
|
1192 * The menu name of the requested tree. |
|
1193 * |
|
1194 * @return |
|
1195 * A string containing the path. If no path has been specified with |
|
1196 * menu_tree_set_path(), NULL is returned. |
|
1197 */ |
|
1198 function menu_tree_get_path($menu_name) { |
|
1199 return menu_tree_set_path($menu_name); |
|
1200 } |
|
1201 |
|
1202 /** |
|
1203 * Gets the data structure for a named menu tree, based on the current page. |
|
1204 * |
|
1205 * The tree order is maintained by storing each parent in an individual |
|
1206 * field, see http://drupal.org/node/141866 for more. |
|
1207 * |
|
1208 * @param $menu_name |
|
1209 * The named menu links to return. |
|
1210 * @param $max_depth |
|
1211 * (optional) The maximum depth of links to retrieve. |
|
1212 * @param $only_active_trail |
|
1213 * (optional) Whether to only return the links in the active trail (TRUE) |
|
1214 * instead of all links on every level of the menu link tree (FALSE). Defaults |
|
1215 * to FALSE. Internally used for breadcrumbs only. |
|
1216 * |
|
1217 * @return |
|
1218 * An array of menu links, in the order they should be rendered. The array |
|
1219 * is a list of associative arrays -- these have two keys, link and below. |
|
1220 * link is a menu item, ready for theming as a link. Below represents the |
|
1221 * submenu below the link if there is one, and it is a subtree that has the |
|
1222 * same structure described for the top-level array. |
|
1223 */ |
|
1224 function menu_tree_page_data($menu_name, $max_depth = NULL, $only_active_trail = FALSE) { |
|
1225 $tree = &drupal_static(__FUNCTION__, array()); |
|
1226 |
|
1227 // Check if the active trail has been overridden for this menu tree. |
|
1228 $active_path = menu_tree_get_path($menu_name); |
|
1229 // Load the menu item corresponding to the current page. |
|
1230 if ($item = menu_get_item($active_path)) { |
|
1231 if (isset($max_depth)) { |
|
1232 $max_depth = min($max_depth, MENU_MAX_DEPTH); |
|
1233 } |
|
1234 // Generate a cache ID (cid) specific for this page. |
|
1235 $cid = 'links:' . $menu_name . ':page:' . $item['href'] . ':' . $GLOBALS['language']->language . ':' . (int) $item['access'] . ':' . (int) $max_depth; |
|
1236 // If we are asked for the active trail only, and $menu_name has not been |
|
1237 // built and cached for this page yet, then this likely means that it |
|
1238 // won't be built anymore, as this function is invoked from |
|
1239 // template_process_page(). So in order to not build a giant menu tree |
|
1240 // that needs to be checked for access on all levels, we simply check |
|
1241 // whether we have the menu already in cache, or otherwise, build a minimum |
|
1242 // tree containing the breadcrumb/active trail only. |
|
1243 // @see menu_set_active_trail() |
|
1244 if (!isset($tree[$cid]) && $only_active_trail) { |
|
1245 $cid .= ':trail'; |
|
1246 } |
|
1247 |
|
1248 if (!isset($tree[$cid])) { |
|
1249 // If the static variable doesn't have the data, check {cache_menu}. |
|
1250 $cache = cache_get($cid, 'cache_menu'); |
|
1251 if ($cache && isset($cache->data)) { |
|
1252 // If the cache entry exists, it contains the parameters for |
|
1253 // menu_build_tree(). |
|
1254 $tree_parameters = $cache->data; |
|
1255 } |
|
1256 // If the tree data was not in the cache, build $tree_parameters. |
|
1257 if (!isset($tree_parameters)) { |
|
1258 $tree_parameters = array( |
|
1259 'min_depth' => 1, |
|
1260 'max_depth' => $max_depth, |
|
1261 ); |
|
1262 // Parent mlids; used both as key and value to ensure uniqueness. |
|
1263 // We always want all the top-level links with plid == 0. |
|
1264 $active_trail = array(0 => 0); |
|
1265 |
|
1266 // If the item for the current page is accessible, build the tree |
|
1267 // parameters accordingly. |
|
1268 if ($item['access']) { |
|
1269 // Find a menu link corresponding to the current path. If $active_path |
|
1270 // is NULL, let menu_link_get_preferred() determine the path. |
|
1271 if ($active_link = menu_link_get_preferred($active_path, $menu_name)) { |
|
1272 // The active link may only be taken into account to build the |
|
1273 // active trail, if it resides in the requested menu. Otherwise, |
|
1274 // we'd needlessly re-run _menu_build_tree() queries for every menu |
|
1275 // on every page. |
|
1276 if ($active_link['menu_name'] == $menu_name) { |
|
1277 // Use all the coordinates, except the last one because there |
|
1278 // can be no child beyond the last column. |
|
1279 for ($i = 1; $i < MENU_MAX_DEPTH; $i++) { |
|
1280 if ($active_link['p' . $i]) { |
|
1281 $active_trail[$active_link['p' . $i]] = $active_link['p' . $i]; |
|
1282 } |
|
1283 } |
|
1284 // If we are asked to build links for the active trail only, skip |
|
1285 // the entire 'expanded' handling. |
|
1286 if ($only_active_trail) { |
|
1287 $tree_parameters['only_active_trail'] = TRUE; |
|
1288 } |
|
1289 } |
|
1290 } |
|
1291 $parents = $active_trail; |
|
1292 |
|
1293 $expanded = variable_get('menu_expanded', array()); |
|
1294 // Check whether the current menu has any links set to be expanded. |
|
1295 if (!$only_active_trail && in_array($menu_name, $expanded)) { |
|
1296 // Collect all the links set to be expanded, and then add all of |
|
1297 // their children to the list as well. |
|
1298 do { |
|
1299 $result = db_select('menu_links', NULL, array('fetch' => PDO::FETCH_ASSOC)) |
|
1300 ->fields('menu_links', array('mlid')) |
|
1301 ->condition('menu_name', $menu_name) |
|
1302 ->condition('expanded', 1) |
|
1303 ->condition('has_children', 1) |
|
1304 ->condition('plid', $parents, 'IN') |
|
1305 ->condition('mlid', $parents, 'NOT IN') |
|
1306 ->execute(); |
|
1307 $num_rows = FALSE; |
|
1308 foreach ($result as $item) { |
|
1309 $parents[$item['mlid']] = $item['mlid']; |
|
1310 $num_rows = TRUE; |
|
1311 } |
|
1312 } while ($num_rows); |
|
1313 } |
|
1314 $tree_parameters['expanded'] = $parents; |
|
1315 $tree_parameters['active_trail'] = $active_trail; |
|
1316 } |
|
1317 // If access is denied, we only show top-level links in menus. |
|
1318 else { |
|
1319 $tree_parameters['expanded'] = $active_trail; |
|
1320 $tree_parameters['active_trail'] = $active_trail; |
|
1321 } |
|
1322 // Cache the tree building parameters using the page-specific cid. |
|
1323 cache_set($cid, $tree_parameters, 'cache_menu'); |
|
1324 } |
|
1325 |
|
1326 // Build the tree using the parameters; the resulting tree will be cached |
|
1327 // by _menu_build_tree(). |
|
1328 $tree[$cid] = menu_build_tree($menu_name, $tree_parameters); |
|
1329 } |
|
1330 return $tree[$cid]; |
|
1331 } |
|
1332 |
|
1333 return array(); |
|
1334 } |
|
1335 |
|
1336 /** |
|
1337 * Builds a menu tree, translates links, and checks access. |
|
1338 * |
|
1339 * @param $menu_name |
|
1340 * The name of the menu. |
|
1341 * @param $parameters |
|
1342 * (optional) An associative array of build parameters. Possible keys: |
|
1343 * - expanded: An array of parent link ids to return only menu links that are |
|
1344 * children of one of the plids in this list. If empty, the whole menu tree |
|
1345 * is built, unless 'only_active_trail' is TRUE. |
|
1346 * - active_trail: An array of mlids, representing the coordinates of the |
|
1347 * currently active menu link. |
|
1348 * - only_active_trail: Whether to only return links that are in the active |
|
1349 * trail. This option is ignored, if 'expanded' is non-empty. Internally |
|
1350 * used for breadcrumbs. |
|
1351 * - min_depth: The minimum depth of menu links in the resulting tree. |
|
1352 * Defaults to 1, which is the default to build a whole tree for a menu |
|
1353 * (excluding menu container itself). |
|
1354 * - max_depth: The maximum depth of menu links in the resulting tree. |
|
1355 * - conditions: An associative array of custom database select query |
|
1356 * condition key/value pairs; see _menu_build_tree() for the actual query. |
|
1357 * |
|
1358 * @return |
|
1359 * A fully built menu tree. |
|
1360 */ |
|
1361 function menu_build_tree($menu_name, array $parameters = array()) { |
|
1362 // Build the menu tree. |
|
1363 $data = _menu_build_tree($menu_name, $parameters); |
|
1364 // Check access for the current user to each item in the tree. |
|
1365 menu_tree_check_access($data['tree'], $data['node_links']); |
|
1366 return $data['tree']; |
|
1367 } |
|
1368 |
|
1369 /** |
|
1370 * Builds a menu tree. |
|
1371 * |
|
1372 * This function may be used build the data for a menu tree only, for example |
|
1373 * to further massage the data manually before further processing happens. |
|
1374 * menu_tree_check_access() needs to be invoked afterwards. |
|
1375 * |
|
1376 * @see menu_build_tree() |
|
1377 */ |
|
1378 function _menu_build_tree($menu_name, array $parameters = array()) { |
|
1379 // Static cache of already built menu trees. |
|
1380 $trees = &drupal_static(__FUNCTION__, array()); |
|
1381 |
|
1382 // Build the cache id; sort parents to prevent duplicate storage and remove |
|
1383 // default parameter values. |
|
1384 if (isset($parameters['expanded'])) { |
|
1385 sort($parameters['expanded']); |
|
1386 } |
|
1387 $tree_cid = 'links:' . $menu_name . ':tree-data:' . $GLOBALS['language']->language . ':' . hash('sha256', serialize($parameters)); |
|
1388 |
|
1389 // If we do not have this tree in the static cache, check {cache_menu}. |
|
1390 if (!isset($trees[$tree_cid])) { |
|
1391 $cache = cache_get($tree_cid, 'cache_menu'); |
|
1392 if ($cache && isset($cache->data)) { |
|
1393 $trees[$tree_cid] = $cache->data; |
|
1394 } |
|
1395 } |
|
1396 |
|
1397 if (!isset($trees[$tree_cid])) { |
|
1398 // Select the links from the table, and recursively build the tree. We |
|
1399 // LEFT JOIN since there is no match in {menu_router} for an external |
|
1400 // link. |
|
1401 $query = db_select('menu_links', 'ml', array('fetch' => PDO::FETCH_ASSOC)); |
|
1402 $query->addTag('translatable'); |
|
1403 $query->leftJoin('menu_router', 'm', 'm.path = ml.router_path'); |
|
1404 $query->fields('ml'); |
|
1405 $query->fields('m', array( |
|
1406 'load_functions', |
|
1407 'to_arg_functions', |
|
1408 'access_callback', |
|
1409 'access_arguments', |
|
1410 'page_callback', |
|
1411 'page_arguments', |
|
1412 'delivery_callback', |
|
1413 'tab_parent', |
|
1414 'tab_root', |
|
1415 'title', |
|
1416 'title_callback', |
|
1417 'title_arguments', |
|
1418 'theme_callback', |
|
1419 'theme_arguments', |
|
1420 'type', |
|
1421 'description', |
|
1422 )); |
|
1423 for ($i = 1; $i <= MENU_MAX_DEPTH; $i++) { |
|
1424 $query->orderBy('p' . $i, 'ASC'); |
|
1425 } |
|
1426 $query->condition('ml.menu_name', $menu_name); |
|
1427 if (!empty($parameters['expanded'])) { |
|
1428 $query->condition('ml.plid', $parameters['expanded'], 'IN'); |
|
1429 } |
|
1430 elseif (!empty($parameters['only_active_trail'])) { |
|
1431 $query->condition('ml.mlid', $parameters['active_trail'], 'IN'); |
|
1432 } |
|
1433 $min_depth = (isset($parameters['min_depth']) ? $parameters['min_depth'] : 1); |
|
1434 if ($min_depth != 1) { |
|
1435 $query->condition('ml.depth', $min_depth, '>='); |
|
1436 } |
|
1437 if (isset($parameters['max_depth'])) { |
|
1438 $query->condition('ml.depth', $parameters['max_depth'], '<='); |
|
1439 } |
|
1440 // Add custom query conditions, if any were passed. |
|
1441 if (isset($parameters['conditions'])) { |
|
1442 foreach ($parameters['conditions'] as $column => $value) { |
|
1443 $query->condition($column, $value); |
|
1444 } |
|
1445 } |
|
1446 |
|
1447 // Build an ordered array of links using the query result object. |
|
1448 $links = array(); |
|
1449 foreach ($query->execute() as $item) { |
|
1450 $links[] = $item; |
|
1451 } |
|
1452 $active_trail = (isset($parameters['active_trail']) ? $parameters['active_trail'] : array()); |
|
1453 $data['tree'] = menu_tree_data($links, $active_trail, $min_depth); |
|
1454 $data['node_links'] = array(); |
|
1455 menu_tree_collect_node_links($data['tree'], $data['node_links']); |
|
1456 |
|
1457 // Cache the data, if it is not already in the cache. |
|
1458 cache_set($tree_cid, $data, 'cache_menu'); |
|
1459 $trees[$tree_cid] = $data; |
|
1460 } |
|
1461 |
|
1462 return $trees[$tree_cid]; |
|
1463 } |
|
1464 |
|
1465 /** |
|
1466 * Collects node links from a given menu tree recursively. |
|
1467 * |
|
1468 * @param $tree |
|
1469 * The menu tree you wish to collect node links from. |
|
1470 * @param $node_links |
|
1471 * An array in which to store the collected node links. |
|
1472 */ |
|
1473 function menu_tree_collect_node_links(&$tree, &$node_links) { |
|
1474 foreach ($tree as $key => $v) { |
|
1475 if ($tree[$key]['link']['router_path'] == 'node/%') { |
|
1476 $nid = substr($tree[$key]['link']['link_path'], 5); |
|
1477 if (is_numeric($nid)) { |
|
1478 $node_links[$nid][$tree[$key]['link']['mlid']] = &$tree[$key]['link']; |
|
1479 $tree[$key]['link']['access'] = FALSE; |
|
1480 } |
|
1481 } |
|
1482 if ($tree[$key]['below']) { |
|
1483 menu_tree_collect_node_links($tree[$key]['below'], $node_links); |
|
1484 } |
|
1485 } |
|
1486 } |
|
1487 |
|
1488 /** |
|
1489 * Checks access and performs dynamic operations for each link in the tree. |
|
1490 * |
|
1491 * @param $tree |
|
1492 * The menu tree you wish to operate on. |
|
1493 * @param $node_links |
|
1494 * A collection of node link references generated from $tree by |
|
1495 * menu_tree_collect_node_links(). |
|
1496 */ |
|
1497 function menu_tree_check_access(&$tree, $node_links = array()) { |
|
1498 if ($node_links && (user_access('access content') || user_access('bypass node access'))) { |
|
1499 $nids = array_keys($node_links); |
|
1500 $select = db_select('node', 'n'); |
|
1501 $select->addField('n', 'nid'); |
|
1502 $select->condition('n.status', 1); |
|
1503 $select->condition('n.nid', $nids, 'IN'); |
|
1504 $select->addTag('node_access'); |
|
1505 $nids = $select->execute()->fetchCol(); |
|
1506 foreach ($nids as $nid) { |
|
1507 foreach ($node_links[$nid] as $mlid => $link) { |
|
1508 $node_links[$nid][$mlid]['access'] = TRUE; |
|
1509 } |
|
1510 } |
|
1511 } |
|
1512 _menu_tree_check_access($tree); |
|
1513 } |
|
1514 |
|
1515 /** |
|
1516 * Sorts the menu tree and recursively checks access for each item. |
|
1517 */ |
|
1518 function _menu_tree_check_access(&$tree) { |
|
1519 $new_tree = array(); |
|
1520 foreach ($tree as $key => $v) { |
|
1521 $item = &$tree[$key]['link']; |
|
1522 _menu_link_translate($item); |
|
1523 if ($item['access'] || ($item['in_active_trail'] && strpos($item['href'], '%') !== FALSE)) { |
|
1524 if ($tree[$key]['below']) { |
|
1525 _menu_tree_check_access($tree[$key]['below']); |
|
1526 } |
|
1527 // The weights are made a uniform 5 digits by adding 50000 as an offset. |
|
1528 // After _menu_link_translate(), $item['title'] has the localized link title. |
|
1529 // Adding the mlid to the end of the index insures that it is unique. |
|
1530 $new_tree[(50000 + $item['weight']) . ' ' . $item['title'] . ' ' . $item['mlid']] = $tree[$key]; |
|
1531 } |
|
1532 } |
|
1533 // Sort siblings in the tree based on the weights and localized titles. |
|
1534 ksort($new_tree); |
|
1535 $tree = $new_tree; |
|
1536 } |
|
1537 |
|
1538 /** |
|
1539 * Sorts and returns the built data representing a menu tree. |
|
1540 * |
|
1541 * @param $links |
|
1542 * A flat array of menu links that are part of the menu. Each array element |
|
1543 * is an associative array of information about the menu link, containing the |
|
1544 * fields from the {menu_links} table, and optionally additional information |
|
1545 * from the {menu_router} table, if the menu item appears in both tables. |
|
1546 * This array must be ordered depth-first. See _menu_build_tree() for a sample |
|
1547 * query. |
|
1548 * @param $parents |
|
1549 * An array of the menu link ID values that are in the path from the current |
|
1550 * page to the root of the menu tree. |
|
1551 * @param $depth |
|
1552 * The minimum depth to include in the returned menu tree. |
|
1553 * |
|
1554 * @return |
|
1555 * An array of menu links in the form of a tree. Each item in the tree is an |
|
1556 * associative array containing: |
|
1557 * - link: The menu link item from $links, with additional element |
|
1558 * 'in_active_trail' (TRUE if the link ID was in $parents). |
|
1559 * - below: An array containing the sub-tree of this item, where each element |
|
1560 * is a tree item array with 'link' and 'below' elements. This array will be |
|
1561 * empty if the menu item has no items in its sub-tree having a depth |
|
1562 * greater than or equal to $depth. |
|
1563 */ |
|
1564 function menu_tree_data(array $links, array $parents = array(), $depth = 1) { |
|
1565 // Reverse the array so we can use the more efficient array_pop() function. |
|
1566 $links = array_reverse($links); |
|
1567 return _menu_tree_data($links, $parents, $depth); |
|
1568 } |
|
1569 |
|
1570 /** |
|
1571 * Builds the data representing a menu tree. |
|
1572 * |
|
1573 * The function is a bit complex because the rendering of a link depends on |
|
1574 * the next menu link. |
|
1575 */ |
|
1576 function _menu_tree_data(&$links, $parents, $depth) { |
|
1577 $tree = array(); |
|
1578 while ($item = array_pop($links)) { |
|
1579 // We need to determine if we're on the path to root so we can later build |
|
1580 // the correct active trail and breadcrumb. |
|
1581 $item['in_active_trail'] = in_array($item['mlid'], $parents); |
|
1582 // Add the current link to the tree. |
|
1583 $tree[$item['mlid']] = array( |
|
1584 'link' => $item, |
|
1585 'below' => array(), |
|
1586 ); |
|
1587 // Look ahead to the next link, but leave it on the array so it's available |
|
1588 // to other recursive function calls if we return or build a sub-tree. |
|
1589 $next = end($links); |
|
1590 // Check whether the next link is the first in a new sub-tree. |
|
1591 if ($next && $next['depth'] > $depth) { |
|
1592 // Recursively call _menu_tree_data to build the sub-tree. |
|
1593 $tree[$item['mlid']]['below'] = _menu_tree_data($links, $parents, $next['depth']); |
|
1594 // Fetch next link after filling the sub-tree. |
|
1595 $next = end($links); |
|
1596 } |
|
1597 // Determine if we should exit the loop and return. |
|
1598 if (!$next || $next['depth'] < $depth) { |
|
1599 break; |
|
1600 } |
|
1601 } |
|
1602 return $tree; |
|
1603 } |
|
1604 |
|
1605 /** |
|
1606 * Implements template_preprocess_HOOK() for theme_menu_tree(). |
|
1607 */ |
|
1608 function template_preprocess_menu_tree(&$variables) { |
|
1609 $variables['#tree'] = $variables['tree']; |
|
1610 $variables['tree'] = $variables['tree']['#children']; |
|
1611 } |
|
1612 |
|
1613 /** |
|
1614 * Returns HTML for a wrapper for a menu sub-tree. |
|
1615 * |
|
1616 * @param $variables |
|
1617 * An associative array containing: |
|
1618 * - tree: An HTML string containing the tree's items. |
|
1619 * |
|
1620 * @see template_preprocess_menu_tree() |
|
1621 * @ingroup themeable |
|
1622 */ |
|
1623 function theme_menu_tree($variables) { |
|
1624 return '<ul class="menu">' . $variables['tree'] . '</ul>'; |
|
1625 } |
|
1626 |
|
1627 /** |
|
1628 * Returns HTML for a menu link and submenu. |
|
1629 * |
|
1630 * @param $variables |
|
1631 * An associative array containing: |
|
1632 * - element: Structured array data for a menu link. |
|
1633 * |
|
1634 * @ingroup themeable |
|
1635 */ |
|
1636 function theme_menu_link(array $variables) { |
|
1637 $element = $variables['element']; |
|
1638 $sub_menu = ''; |
|
1639 |
|
1640 if ($element['#below']) { |
|
1641 $sub_menu = drupal_render($element['#below']); |
|
1642 } |
|
1643 $output = l($element['#title'], $element['#href'], $element['#localized_options']); |
|
1644 return '<li' . drupal_attributes($element['#attributes']) . '>' . $output . $sub_menu . "</li>\n"; |
|
1645 } |
|
1646 |
|
1647 /** |
|
1648 * Returns HTML for a single local task link. |
|
1649 * |
|
1650 * @param $variables |
|
1651 * An associative array containing: |
|
1652 * - element: A render element containing: |
|
1653 * - #link: A menu link array with 'title', 'href', and 'localized_options' |
|
1654 * keys. |
|
1655 * - #active: A boolean indicating whether the local task is active. |
|
1656 * |
|
1657 * @ingroup themeable |
|
1658 */ |
|
1659 function theme_menu_local_task($variables) { |
|
1660 $link = $variables['element']['#link']; |
|
1661 $link_text = $link['title']; |
|
1662 |
|
1663 if (!empty($variables['element']['#active'])) { |
|
1664 // Add text to indicate active tab for non-visual users. |
|
1665 $active = '<span class="element-invisible">' . t('(active tab)') . '</span>'; |
|
1666 |
|
1667 // If the link does not contain HTML already, check_plain() it now. |
|
1668 // After we set 'html'=TRUE the link will not be sanitized by l(). |
|
1669 if (empty($link['localized_options']['html'])) { |
|
1670 $link['title'] = check_plain($link['title']); |
|
1671 } |
|
1672 $link['localized_options']['html'] = TRUE; |
|
1673 $link_text = t('!local-task-title!active', array('!local-task-title' => $link['title'], '!active' => $active)); |
|
1674 } |
|
1675 |
|
1676 return '<li' . (!empty($variables['element']['#active']) ? ' class="active"' : '') . '>' . l($link_text, $link['href'], $link['localized_options']) . "</li>\n"; |
|
1677 } |
|
1678 |
|
1679 /** |
|
1680 * Returns HTML for a single local action link. |
|
1681 * |
|
1682 * @param $variables |
|
1683 * An associative array containing: |
|
1684 * - element: A render element containing: |
|
1685 * - #link: A menu link array with 'title', 'href', and 'localized_options' |
|
1686 * keys. |
|
1687 * |
|
1688 * @ingroup themeable |
|
1689 */ |
|
1690 function theme_menu_local_action($variables) { |
|
1691 $link = $variables['element']['#link']; |
|
1692 |
|
1693 $output = '<li>'; |
|
1694 if (isset($link['href'])) { |
|
1695 $output .= l($link['title'], $link['href'], isset($link['localized_options']) ? $link['localized_options'] : array()); |
|
1696 } |
|
1697 elseif (!empty($link['localized_options']['html'])) { |
|
1698 $output .= $link['title']; |
|
1699 } |
|
1700 else { |
|
1701 $output .= check_plain($link['title']); |
|
1702 } |
|
1703 $output .= "</li>\n"; |
|
1704 |
|
1705 return $output; |
|
1706 } |
|
1707 |
|
1708 /** |
|
1709 * Generates elements for the $arg array in the help hook. |
|
1710 */ |
|
1711 function drupal_help_arg($arg = array()) { |
|
1712 // Note - the number of empty elements should be > MENU_MAX_PARTS. |
|
1713 return $arg + array('', '', '', '', '', '', '', '', '', '', '', ''); |
|
1714 } |
|
1715 |
|
1716 /** |
|
1717 * Returns the help associated with the active menu item. |
|
1718 */ |
|
1719 function menu_get_active_help() { |
|
1720 $output = ''; |
|
1721 $router_path = menu_tab_root_path(); |
|
1722 // We will always have a path unless we are on a 403 or 404. |
|
1723 if (!$router_path) { |
|
1724 return ''; |
|
1725 } |
|
1726 |
|
1727 $arg = drupal_help_arg(arg(NULL)); |
|
1728 |
|
1729 foreach (module_implements('help') as $module) { |
|
1730 $function = $module . '_help'; |
|
1731 // Lookup help for this path. |
|
1732 if ($help = $function($router_path, $arg)) { |
|
1733 $output .= $help . "\n"; |
|
1734 } |
|
1735 } |
|
1736 return $output; |
|
1737 } |
|
1738 |
|
1739 /** |
|
1740 * Gets the custom theme for the current page, if there is one. |
|
1741 * |
|
1742 * @param $initialize |
|
1743 * This parameter should only be used internally; it is set to TRUE in order |
|
1744 * to force the custom theme to be initialized for the current page request. |
|
1745 * |
|
1746 * @return |
|
1747 * The machine-readable name of the custom theme, if there is one. |
|
1748 * |
|
1749 * @see menu_set_custom_theme() |
|
1750 */ |
|
1751 function menu_get_custom_theme($initialize = FALSE) { |
|
1752 $custom_theme = &drupal_static(__FUNCTION__); |
|
1753 // Skip this if the site is offline or being installed or updated, since the |
|
1754 // menu system may not be correctly initialized then. |
|
1755 if ($initialize && !_menu_site_is_offline(TRUE) && (!defined('MAINTENANCE_MODE') || (MAINTENANCE_MODE != 'update' && MAINTENANCE_MODE != 'install'))) { |
|
1756 // First allow modules to dynamically set a custom theme for the current |
|
1757 // page. Since we can only have one, the last module to return a valid |
|
1758 // theme takes precedence. |
|
1759 $custom_themes = array_filter(module_invoke_all('custom_theme'), 'drupal_theme_access'); |
|
1760 if (!empty($custom_themes)) { |
|
1761 $custom_theme = array_pop($custom_themes); |
|
1762 } |
|
1763 // If there is a theme callback function for the current page, execute it. |
|
1764 // If this returns a valid theme, it will override any theme that was set |
|
1765 // by a hook_custom_theme() implementation above. |
|
1766 $router_item = menu_get_item(); |
|
1767 if (!empty($router_item['access']) && !empty($router_item['theme_callback']) && function_exists($router_item['theme_callback'])) { |
|
1768 $theme_name = call_user_func_array($router_item['theme_callback'], $router_item['theme_arguments']); |
|
1769 if (drupal_theme_access($theme_name)) { |
|
1770 $custom_theme = $theme_name; |
|
1771 } |
|
1772 } |
|
1773 } |
|
1774 return $custom_theme; |
|
1775 } |
|
1776 |
|
1777 /** |
|
1778 * Sets a custom theme for the current page, if there is one. |
|
1779 */ |
|
1780 function menu_set_custom_theme() { |
|
1781 menu_get_custom_theme(TRUE); |
|
1782 } |
|
1783 |
|
1784 /** |
|
1785 * Build a list of named menus. |
|
1786 */ |
|
1787 function menu_get_names() { |
|
1788 $names = &drupal_static(__FUNCTION__); |
|
1789 |
|
1790 if (empty($names)) { |
|
1791 $names = db_select('menu_links') |
|
1792 ->distinct() |
|
1793 ->fields('menu_links', array('menu_name')) |
|
1794 ->orderBy('menu_name') |
|
1795 ->execute()->fetchCol(); |
|
1796 } |
|
1797 return $names; |
|
1798 } |
|
1799 |
|
1800 /** |
|
1801 * Returns an array containing the names of system-defined (default) menus. |
|
1802 */ |
|
1803 function menu_list_system_menus() { |
|
1804 return array( |
|
1805 'navigation' => 'Navigation', |
|
1806 'management' => 'Management', |
|
1807 'user-menu' => 'User menu', |
|
1808 'main-menu' => 'Main menu', |
|
1809 ); |
|
1810 } |
|
1811 |
|
1812 /** |
|
1813 * Returns an array of links to be rendered as the Main menu. |
|
1814 */ |
|
1815 function menu_main_menu() { |
|
1816 return menu_navigation_links(variable_get('menu_main_links_source', 'main-menu')); |
|
1817 } |
|
1818 |
|
1819 /** |
|
1820 * Returns an array of links to be rendered as the Secondary links. |
|
1821 */ |
|
1822 function menu_secondary_menu() { |
|
1823 |
|
1824 // If the secondary menu source is set as the primary menu, we display the |
|
1825 // second level of the primary menu. |
|
1826 if (variable_get('menu_secondary_links_source', 'user-menu') == variable_get('menu_main_links_source', 'main-menu')) { |
|
1827 return menu_navigation_links(variable_get('menu_main_links_source', 'main-menu'), 1); |
|
1828 } |
|
1829 else { |
|
1830 return menu_navigation_links(variable_get('menu_secondary_links_source', 'user-menu'), 0); |
|
1831 } |
|
1832 } |
|
1833 |
|
1834 /** |
|
1835 * Returns an array of links for a navigation menu. |
|
1836 * |
|
1837 * @param $menu_name |
|
1838 * The name of the menu. |
|
1839 * @param $level |
|
1840 * Optional, the depth of the menu to be returned. |
|
1841 * |
|
1842 * @return |
|
1843 * An array of links of the specified menu and level. |
|
1844 */ |
|
1845 function menu_navigation_links($menu_name, $level = 0) { |
|
1846 // Don't even bother querying the menu table if no menu is specified. |
|
1847 if (empty($menu_name)) { |
|
1848 return array(); |
|
1849 } |
|
1850 |
|
1851 // Get the menu hierarchy for the current page. |
|
1852 $tree = menu_tree_page_data($menu_name, $level + 1); |
|
1853 |
|
1854 // Go down the active trail until the right level is reached. |
|
1855 while ($level-- > 0 && $tree) { |
|
1856 // Loop through the current level's items until we find one that is in trail. |
|
1857 while ($item = array_shift($tree)) { |
|
1858 if ($item['link']['in_active_trail']) { |
|
1859 // If the item is in the active trail, we continue in the subtree. |
|
1860 $tree = empty($item['below']) ? array() : $item['below']; |
|
1861 break; |
|
1862 } |
|
1863 } |
|
1864 } |
|
1865 |
|
1866 // Create a single level of links. |
|
1867 $router_item = menu_get_item(); |
|
1868 $links = array(); |
|
1869 foreach ($tree as $item) { |
|
1870 if (!$item['link']['hidden']) { |
|
1871 $class = ''; |
|
1872 $l = $item['link']['localized_options']; |
|
1873 $l['href'] = $item['link']['href']; |
|
1874 $l['title'] = $item['link']['title']; |
|
1875 if ($item['link']['in_active_trail']) { |
|
1876 $class = ' active-trail'; |
|
1877 $l['attributes']['class'][] = 'active-trail'; |
|
1878 } |
|
1879 // Normally, l() compares the href of every link with $_GET['q'] and sets |
|
1880 // the active class accordingly. But local tasks do not appear in menu |
|
1881 // trees, so if the current path is a local task, and this link is its |
|
1882 // tab root, then we have to set the class manually. |
|
1883 if ($item['link']['href'] == $router_item['tab_root_href'] && $item['link']['href'] != $_GET['q']) { |
|
1884 $l['attributes']['class'][] = 'active'; |
|
1885 } |
|
1886 // Keyed with the unique mlid to generate classes in theme_links(). |
|
1887 $links['menu-' . $item['link']['mlid'] . $class] = $l; |
|
1888 } |
|
1889 } |
|
1890 return $links; |
|
1891 } |
|
1892 |
|
1893 /** |
|
1894 * Collects the local tasks (tabs), action links, and the root path. |
|
1895 * |
|
1896 * @param $level |
|
1897 * The level of tasks you ask for. Primary tasks are 0, secondary are 1. |
|
1898 * |
|
1899 * @return |
|
1900 * An array containing |
|
1901 * - tabs: Local tasks for the requested level: |
|
1902 * - count: The number of local tasks. |
|
1903 * - output: The themed output of local tasks. |
|
1904 * - actions: Action links for the requested level: |
|
1905 * - count: The number of action links. |
|
1906 * - output: The themed output of action links. |
|
1907 * - root_path: The router path for the current page. If the current page is |
|
1908 * a default local task, then this corresponds to the parent tab. |
|
1909 */ |
|
1910 function menu_local_tasks($level = 0) { |
|
1911 $data = &drupal_static(__FUNCTION__); |
|
1912 $root_path = &drupal_static(__FUNCTION__ . ':root_path', ''); |
|
1913 $empty = array( |
|
1914 'tabs' => array('count' => 0, 'output' => array()), |
|
1915 'actions' => array('count' => 0, 'output' => array()), |
|
1916 'root_path' => &$root_path, |
|
1917 ); |
|
1918 |
|
1919 if (!isset($data)) { |
|
1920 $data = array(); |
|
1921 // Set defaults in case there are no actions or tabs. |
|
1922 $actions = $empty['actions']; |
|
1923 $tabs = array(); |
|
1924 |
|
1925 $router_item = menu_get_item(); |
|
1926 |
|
1927 // If this router item points to its parent, start from the parents to |
|
1928 // compute tabs and actions. |
|
1929 if ($router_item && ($router_item['type'] & MENU_LINKS_TO_PARENT)) { |
|
1930 $router_item = menu_get_item($router_item['tab_parent_href']); |
|
1931 } |
|
1932 |
|
1933 // If we failed to fetch a router item or the current user doesn't have |
|
1934 // access to it, don't bother computing the tabs. |
|
1935 if (!$router_item || !$router_item['access']) { |
|
1936 return $empty; |
|
1937 } |
|
1938 |
|
1939 // Get all tabs (also known as local tasks) and the root page. |
|
1940 $cid = 'local_tasks:' . $router_item['tab_root']; |
|
1941 if ($cache = cache_get($cid, 'cache_menu')) { |
|
1942 $result = $cache->data; |
|
1943 } |
|
1944 else { |
|
1945 $result = db_select('menu_router', NULL, array('fetch' => PDO::FETCH_ASSOC)) |
|
1946 ->fields('menu_router') |
|
1947 ->condition('tab_root', $router_item['tab_root']) |
|
1948 ->condition('context', MENU_CONTEXT_INLINE, '<>') |
|
1949 ->orderBy('weight') |
|
1950 ->orderBy('title') |
|
1951 ->execute() |
|
1952 ->fetchAll(); |
|
1953 cache_set($cid, $result, 'cache_menu'); |
|
1954 } |
|
1955 $map = $router_item['original_map']; |
|
1956 $children = array(); |
|
1957 $tasks = array(); |
|
1958 $root_path = $router_item['path']; |
|
1959 |
|
1960 foreach ($result as $item) { |
|
1961 _menu_translate($item, $map, TRUE); |
|
1962 if ($item['tab_parent']) { |
|
1963 // All tabs, but not the root page. |
|
1964 $children[$item['tab_parent']][$item['path']] = $item; |
|
1965 } |
|
1966 // Store the translated item for later use. |
|
1967 $tasks[$item['path']] = $item; |
|
1968 } |
|
1969 |
|
1970 // Find all tabs below the current path. |
|
1971 $path = $router_item['path']; |
|
1972 // Tab parenting may skip levels, so the number of parts in the path may not |
|
1973 // equal the depth. Thus we use the $depth counter (offset by 1000 for ksort). |
|
1974 $depth = 1001; |
|
1975 $actions['count'] = 0; |
|
1976 $actions['output'] = array(); |
|
1977 while (isset($children[$path])) { |
|
1978 $tabs_current = array(); |
|
1979 $actions_current = array(); |
|
1980 $next_path = ''; |
|
1981 $tab_count = 0; |
|
1982 $action_count = 0; |
|
1983 foreach ($children[$path] as $item) { |
|
1984 // Local tasks can be normal items too, so bitmask with |
|
1985 // MENU_IS_LOCAL_TASK before checking. |
|
1986 if (!($item['type'] & MENU_IS_LOCAL_TASK)) { |
|
1987 // This item is not a tab, skip it. |
|
1988 continue; |
|
1989 } |
|
1990 if ($item['access']) { |
|
1991 $link = $item; |
|
1992 // The default task is always active. As tabs can be normal items |
|
1993 // too, so bitmask with MENU_LINKS_TO_PARENT before checking. |
|
1994 if (($item['type'] & MENU_LINKS_TO_PARENT) == MENU_LINKS_TO_PARENT) { |
|
1995 // Find the first parent which is not a default local task or action. |
|
1996 for ($p = $item['tab_parent']; ($tasks[$p]['type'] & MENU_LINKS_TO_PARENT) == MENU_LINKS_TO_PARENT; $p = $tasks[$p]['tab_parent']); |
|
1997 // Use the path of the parent instead. |
|
1998 $link['href'] = $tasks[$p]['href']; |
|
1999 // Mark the link as active, if the current path happens to be the |
|
2000 // path of the default local task itself (i.e., instead of its |
|
2001 // tab_parent_href or tab_root_href). Normally, links for default |
|
2002 // local tasks link to their parent, but the path of default local |
|
2003 // tasks can still be accessed directly, in which case this link |
|
2004 // would not be marked as active, since l() only compares the href |
|
2005 // with $_GET['q']. |
|
2006 if ($link['href'] != $_GET['q']) { |
|
2007 $link['localized_options']['attributes']['class'][] = 'active'; |
|
2008 } |
|
2009 $tabs_current[] = array( |
|
2010 '#theme' => 'menu_local_task', |
|
2011 '#link' => $link, |
|
2012 '#active' => TRUE, |
|
2013 ); |
|
2014 $next_path = $item['path']; |
|
2015 $tab_count++; |
|
2016 } |
|
2017 else { |
|
2018 // Actions can be normal items too, so bitmask with |
|
2019 // MENU_IS_LOCAL_ACTION before checking. |
|
2020 if (($item['type'] & MENU_IS_LOCAL_ACTION) == MENU_IS_LOCAL_ACTION) { |
|
2021 // The item is an action, display it as such. |
|
2022 $actions_current[] = array( |
|
2023 '#theme' => 'menu_local_action', |
|
2024 '#link' => $link, |
|
2025 ); |
|
2026 $action_count++; |
|
2027 } |
|
2028 else { |
|
2029 // Otherwise, it's a normal tab. |
|
2030 $tabs_current[] = array( |
|
2031 '#theme' => 'menu_local_task', |
|
2032 '#link' => $link, |
|
2033 ); |
|
2034 $tab_count++; |
|
2035 } |
|
2036 } |
|
2037 } |
|
2038 } |
|
2039 $path = $next_path; |
|
2040 $tabs[$depth]['count'] = $tab_count; |
|
2041 $tabs[$depth]['output'] = $tabs_current; |
|
2042 $actions['count'] += $action_count; |
|
2043 $actions['output'] = array_merge($actions['output'], $actions_current); |
|
2044 $depth++; |
|
2045 } |
|
2046 $data['actions'] = $actions; |
|
2047 // Find all tabs at the same level or above the current one. |
|
2048 $parent = $router_item['tab_parent']; |
|
2049 $path = $router_item['path']; |
|
2050 $current = $router_item; |
|
2051 $depth = 1000; |
|
2052 while (isset($children[$parent])) { |
|
2053 $tabs_current = array(); |
|
2054 $next_path = ''; |
|
2055 $next_parent = ''; |
|
2056 $count = 0; |
|
2057 foreach ($children[$parent] as $item) { |
|
2058 // Skip local actions. |
|
2059 if ($item['type'] & MENU_IS_LOCAL_ACTION) { |
|
2060 continue; |
|
2061 } |
|
2062 if ($item['access']) { |
|
2063 $count++; |
|
2064 $link = $item; |
|
2065 // Local tasks can be normal items too, so bitmask with |
|
2066 // MENU_LINKS_TO_PARENT before checking. |
|
2067 if (($item['type'] & MENU_LINKS_TO_PARENT) == MENU_LINKS_TO_PARENT) { |
|
2068 // Find the first parent which is not a default local task. |
|
2069 for ($p = $item['tab_parent']; ($tasks[$p]['type'] & MENU_LINKS_TO_PARENT) == MENU_LINKS_TO_PARENT; $p = $tasks[$p]['tab_parent']); |
|
2070 // Use the path of the parent instead. |
|
2071 $link['href'] = $tasks[$p]['href']; |
|
2072 if ($item['path'] == $router_item['path']) { |
|
2073 $root_path = $tasks[$p]['path']; |
|
2074 } |
|
2075 } |
|
2076 // We check for the active tab. |
|
2077 if ($item['path'] == $path) { |
|
2078 // Mark the link as active, if the current path is a (second-level) |
|
2079 // local task of a default local task. Since this default local task |
|
2080 // links to its parent, l() will not mark it as active, as it only |
|
2081 // compares the link's href to $_GET['q']. |
|
2082 if ($link['href'] != $_GET['q']) { |
|
2083 $link['localized_options']['attributes']['class'][] = 'active'; |
|
2084 } |
|
2085 $tabs_current[] = array( |
|
2086 '#theme' => 'menu_local_task', |
|
2087 '#link' => $link, |
|
2088 '#active' => TRUE, |
|
2089 ); |
|
2090 $next_path = $item['tab_parent']; |
|
2091 if (isset($tasks[$next_path])) { |
|
2092 $next_parent = $tasks[$next_path]['tab_parent']; |
|
2093 } |
|
2094 } |
|
2095 else { |
|
2096 $tabs_current[] = array( |
|
2097 '#theme' => 'menu_local_task', |
|
2098 '#link' => $link, |
|
2099 ); |
|
2100 } |
|
2101 } |
|
2102 } |
|
2103 $path = $next_path; |
|
2104 $parent = $next_parent; |
|
2105 $tabs[$depth]['count'] = $count; |
|
2106 $tabs[$depth]['output'] = $tabs_current; |
|
2107 $depth--; |
|
2108 } |
|
2109 // Sort by depth. |
|
2110 ksort($tabs); |
|
2111 // Remove the depth, we are interested only in their relative placement. |
|
2112 $tabs = array_values($tabs); |
|
2113 $data['tabs'] = $tabs; |
|
2114 |
|
2115 // Allow modules to alter local tasks or dynamically append further tasks. |
|
2116 drupal_alter('menu_local_tasks', $data, $router_item, $root_path); |
|
2117 } |
|
2118 |
|
2119 if (isset($data['tabs'][$level])) { |
|
2120 return array( |
|
2121 'tabs' => $data['tabs'][$level], |
|
2122 'actions' => $data['actions'], |
|
2123 'root_path' => $root_path, |
|
2124 ); |
|
2125 } |
|
2126 // @todo If there are no tabs, then there still can be actions; for example, |
|
2127 // when added via hook_menu_local_tasks_alter(). |
|
2128 elseif (!empty($data['actions']['output'])) { |
|
2129 return array('actions' => $data['actions']) + $empty; |
|
2130 } |
|
2131 return $empty; |
|
2132 } |
|
2133 |
|
2134 /** |
|
2135 * Retrieves contextual links for a path based on registered local tasks. |
|
2136 * |
|
2137 * This leverages the menu system to retrieve the first layer of registered |
|
2138 * local tasks for a given system path. All local tasks of the tab type |
|
2139 * MENU_CONTEXT_INLINE are taken into account. |
|
2140 * |
|
2141 * For example, when considering the following registered local tasks: |
|
2142 * - node/%node/view (default local task) with no 'context' defined |
|
2143 * - node/%node/edit with context: MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE |
|
2144 * - node/%node/revisions with context: MENU_CONTEXT_PAGE |
|
2145 * - node/%node/report-as-spam with context: MENU_CONTEXT_INLINE |
|
2146 * |
|
2147 * If the path "node/123" is passed to this function, then it will return the |
|
2148 * links for 'edit' and 'report-as-spam'. |
|
2149 * |
|
2150 * @param $module |
|
2151 * The name of the implementing module. This is used to prefix the key for |
|
2152 * each contextual link, which is transformed into a CSS class during |
|
2153 * rendering by theme_links(). For example, if $module is 'block' and the |
|
2154 * retrieved local task path argument is 'edit', then the resulting CSS class |
|
2155 * will be 'block-edit'. |
|
2156 * @param $parent_path |
|
2157 * The static menu router path of the object to retrieve local tasks for, for |
|
2158 * example 'node' or 'admin/structure/block/manage'. |
|
2159 * @param $args |
|
2160 * A list of dynamic path arguments to append to $parent_path to form the |
|
2161 * fully-qualified menu router path; for example, array(123) for a certain |
|
2162 * node or array('system', 'navigation') for a certain block. |
|
2163 * |
|
2164 * @return |
|
2165 * A list of menu router items that are local tasks for the passed-in path. |
|
2166 * |
|
2167 * @see contextual_links_preprocess() |
|
2168 * @see hook_menu() |
|
2169 */ |
|
2170 function menu_contextual_links($module, $parent_path, $args) { |
|
2171 static $path_empty = array(); |
|
2172 |
|
2173 $links = array(); |
|
2174 // Performance: In case a previous invocation for the same parent path did not |
|
2175 // return any links, we immediately return here. |
|
2176 if (isset($path_empty[$parent_path]) && strpos($parent_path, '%') !== FALSE) { |
|
2177 return $links; |
|
2178 } |
|
2179 // Construct the item-specific parent path. |
|
2180 $path = $parent_path . '/' . implode('/', $args); |
|
2181 |
|
2182 // Get the router item for the given parent link path. |
|
2183 $router_item = menu_get_item($path); |
|
2184 if (!$router_item || !$router_item['access']) { |
|
2185 $path_empty[$parent_path] = TRUE; |
|
2186 return $links; |
|
2187 } |
|
2188 $data = &drupal_static(__FUNCTION__, array()); |
|
2189 $root_path = $router_item['path']; |
|
2190 |
|
2191 // Performance: For a single, normalized path (such as 'node/%') we only query |
|
2192 // available tasks once per request. |
|
2193 if (!isset($data[$root_path])) { |
|
2194 // Get all contextual links that are direct children of the router item and |
|
2195 // not of the tab type 'view'. |
|
2196 $data[$root_path] = db_select('menu_router', 'm') |
|
2197 ->fields('m') |
|
2198 ->condition('tab_parent', $router_item['tab_root']) |
|
2199 ->condition('context', MENU_CONTEXT_NONE, '<>') |
|
2200 ->condition('context', MENU_CONTEXT_PAGE, '<>') |
|
2201 ->orderBy('weight') |
|
2202 ->orderBy('title') |
|
2203 ->execute() |
|
2204 ->fetchAllAssoc('path', PDO::FETCH_ASSOC); |
|
2205 } |
|
2206 $parent_length = drupal_strlen($root_path) + 1; |
|
2207 $map = $router_item['original_map']; |
|
2208 foreach ($data[$root_path] as $item) { |
|
2209 // Extract the actual "task" string from the path argument. |
|
2210 $key = drupal_substr($item['path'], $parent_length); |
|
2211 |
|
2212 // Denormalize and translate the contextual link. |
|
2213 _menu_translate($item, $map, TRUE); |
|
2214 if (!$item['access']) { |
|
2215 continue; |
|
2216 } |
|
2217 // All contextual links are keyed by the actual "task" path argument, |
|
2218 // prefixed with the name of the implementing module. |
|
2219 $links[$module . '-' . $key] = $item; |
|
2220 } |
|
2221 |
|
2222 // Allow modules to alter contextual links. |
|
2223 drupal_alter('menu_contextual_links', $links, $router_item, $root_path); |
|
2224 |
|
2225 // Performance: If the current user does not have access to any links for this |
|
2226 // router path and no other module added further links, we assign FALSE here |
|
2227 // to skip the entire process the next time the same router path is requested. |
|
2228 if (empty($links)) { |
|
2229 $path_empty[$parent_path] = TRUE; |
|
2230 } |
|
2231 |
|
2232 return $links; |
|
2233 } |
|
2234 |
|
2235 /** |
|
2236 * Returns the rendered local tasks at the top level. |
|
2237 */ |
|
2238 function menu_primary_local_tasks() { |
|
2239 $links = menu_local_tasks(0); |
|
2240 // Do not display single tabs. |
|
2241 return ($links['tabs']['count'] > 1 ? $links['tabs']['output'] : ''); |
|
2242 } |
|
2243 |
|
2244 /** |
|
2245 * Returns the rendered local tasks at the second level. |
|
2246 */ |
|
2247 function menu_secondary_local_tasks() { |
|
2248 $links = menu_local_tasks(1); |
|
2249 // Do not display single tabs. |
|
2250 return ($links['tabs']['count'] > 1 ? $links['tabs']['output'] : ''); |
|
2251 } |
|
2252 |
|
2253 /** |
|
2254 * Returns the rendered local actions at the current level. |
|
2255 */ |
|
2256 function menu_local_actions() { |
|
2257 $links = menu_local_tasks(); |
|
2258 return $links['actions']['output']; |
|
2259 } |
|
2260 |
|
2261 /** |
|
2262 * Returns the router path, or the path for a default local task's parent. |
|
2263 */ |
|
2264 function menu_tab_root_path() { |
|
2265 $links = menu_local_tasks(); |
|
2266 return $links['root_path']; |
|
2267 } |
|
2268 |
|
2269 /** |
|
2270 * Returns a renderable element for the primary and secondary tabs. |
|
2271 */ |
|
2272 function menu_local_tabs() { |
|
2273 return array( |
|
2274 '#theme' => 'menu_local_tasks', |
|
2275 '#primary' => menu_primary_local_tasks(), |
|
2276 '#secondary' => menu_secondary_local_tasks(), |
|
2277 ); |
|
2278 } |
|
2279 |
|
2280 /** |
|
2281 * Returns HTML for primary and secondary local tasks. |
|
2282 * |
|
2283 * @param $variables |
|
2284 * An associative array containing: |
|
2285 * - primary: (optional) An array of local tasks (tabs). |
|
2286 * - secondary: (optional) An array of local tasks (tabs). |
|
2287 * |
|
2288 * @ingroup themeable |
|
2289 * @see menu_local_tasks() |
|
2290 */ |
|
2291 function theme_menu_local_tasks(&$variables) { |
|
2292 $output = ''; |
|
2293 |
|
2294 if (!empty($variables['primary'])) { |
|
2295 $variables['primary']['#prefix'] = '<h2 class="element-invisible">' . t('Primary tabs') . '</h2>'; |
|
2296 $variables['primary']['#prefix'] .= '<ul class="tabs primary">'; |
|
2297 $variables['primary']['#suffix'] = '</ul>'; |
|
2298 $output .= drupal_render($variables['primary']); |
|
2299 } |
|
2300 if (!empty($variables['secondary'])) { |
|
2301 $variables['secondary']['#prefix'] = '<h2 class="element-invisible">' . t('Secondary tabs') . '</h2>'; |
|
2302 $variables['secondary']['#prefix'] .= '<ul class="tabs secondary">'; |
|
2303 $variables['secondary']['#suffix'] = '</ul>'; |
|
2304 $output .= drupal_render($variables['secondary']); |
|
2305 } |
|
2306 |
|
2307 return $output; |
|
2308 } |
|
2309 |
|
2310 /** |
|
2311 * Sets (or gets) the active menu for the current page. |
|
2312 * |
|
2313 * The active menu for the page determines the active trail. |
|
2314 * |
|
2315 * @return |
|
2316 * An array of menu machine names, in order of preference. The |
|
2317 * 'menu_default_active_menus' variable may be used to assert a menu order |
|
2318 * different from the order of creation, or to prevent a particular menu from |
|
2319 * being used at all in the active trail. |
|
2320 * E.g., $conf['menu_default_active_menus'] = array('navigation', 'main-menu') |
|
2321 */ |
|
2322 function menu_set_active_menu_names($menu_names = NULL) { |
|
2323 $active = &drupal_static(__FUNCTION__); |
|
2324 |
|
2325 if (isset($menu_names) && is_array($menu_names)) { |
|
2326 $active = $menu_names; |
|
2327 } |
|
2328 elseif (!isset($active)) { |
|
2329 $active = variable_get('menu_default_active_menus', array_keys(menu_list_system_menus())); |
|
2330 } |
|
2331 return $active; |
|
2332 } |
|
2333 |
|
2334 /** |
|
2335 * Gets the active menu for the current page. |
|
2336 */ |
|
2337 function menu_get_active_menu_names() { |
|
2338 return menu_set_active_menu_names(); |
|
2339 } |
|
2340 |
|
2341 /** |
|
2342 * Sets the active path, which determines which page is loaded. |
|
2343 * |
|
2344 * Note that this may not have the desired effect unless invoked very early |
|
2345 * in the page load, such as during hook_boot(), or unless you call |
|
2346 * menu_execute_active_handler() to generate your page output. |
|
2347 * |
|
2348 * @param $path |
|
2349 * A Drupal path - not a path alias. |
|
2350 */ |
|
2351 function menu_set_active_item($path) { |
|
2352 $_GET['q'] = $path; |
|
2353 // Since the active item has changed, the active menu trail may also be out |
|
2354 // of date. |
|
2355 drupal_static_reset('menu_set_active_trail'); |
|
2356 } |
|
2357 |
|
2358 /** |
|
2359 * Sets the active trail (path to the menu tree root) of the current page. |
|
2360 * |
|
2361 * Any trail set by this function will only be used for functionality that calls |
|
2362 * menu_get_active_trail(). Drupal core only uses trails set here for |
|
2363 * breadcrumbs and the page title and not for menu trees or page content. |
|
2364 * Additionally, breadcrumbs set by drupal_set_breadcrumb() will override any |
|
2365 * trail set here. |
|
2366 * |
|
2367 * To affect the trail used by menu trees, use menu_tree_set_path(). To affect |
|
2368 * the page content, use menu_set_active_item() instead. |
|
2369 * |
|
2370 * @param $new_trail |
|
2371 * Menu trail to set; the value is saved in a static variable and can be |
|
2372 * retrieved by menu_get_active_trail(). The format of this array should be |
|
2373 * the same as the return value of menu_get_active_trail(). |
|
2374 * |
|
2375 * @return |
|
2376 * The active trail. See menu_get_active_trail() for details. |
|
2377 */ |
|
2378 function menu_set_active_trail($new_trail = NULL) { |
|
2379 $trail = &drupal_static(__FUNCTION__); |
|
2380 |
|
2381 if (isset($new_trail)) { |
|
2382 $trail = $new_trail; |
|
2383 } |
|
2384 elseif (!isset($trail)) { |
|
2385 $trail = array(); |
|
2386 $trail[] = array( |
|
2387 'title' => t('Home'), |
|
2388 'href' => '<front>', |
|
2389 'link_path' => '', |
|
2390 'localized_options' => array(), |
|
2391 'type' => 0, |
|
2392 ); |
|
2393 |
|
2394 // Try to retrieve a menu link corresponding to the current path. If more |
|
2395 // than one exists, the link from the most preferred menu is returned. |
|
2396 $preferred_link = menu_link_get_preferred(); |
|
2397 $current_item = menu_get_item(); |
|
2398 |
|
2399 // There is a link for the current path. |
|
2400 if ($preferred_link) { |
|
2401 // Pass TRUE for $only_active_trail to make menu_tree_page_data() build |
|
2402 // a stripped down menu tree containing the active trail only, in case |
|
2403 // the given menu has not been built in this request yet. |
|
2404 $tree = menu_tree_page_data($preferred_link['menu_name'], NULL, TRUE); |
|
2405 list($key, $curr) = each($tree); |
|
2406 } |
|
2407 // There is no link for the current path. |
|
2408 else { |
|
2409 $preferred_link = $current_item; |
|
2410 $curr = FALSE; |
|
2411 } |
|
2412 |
|
2413 while ($curr) { |
|
2414 $link = $curr['link']; |
|
2415 if ($link['in_active_trail']) { |
|
2416 // Add the link to the trail, unless it links to its parent. |
|
2417 if (!($link['type'] & MENU_LINKS_TO_PARENT)) { |
|
2418 // The menu tree for the active trail may contain additional links |
|
2419 // that have not been translated yet, since they contain dynamic |
|
2420 // argument placeholders (%). Such links are not contained in regular |
|
2421 // menu trees, and have only been loaded for the additional |
|
2422 // translation that happens here, so as to be able to display them in |
|
2423 // the breadcrumb for the current page. |
|
2424 // @see _menu_tree_check_access() |
|
2425 // @see _menu_link_translate() |
|
2426 if (strpos($link['href'], '%') !== FALSE) { |
|
2427 _menu_link_translate($link, TRUE); |
|
2428 } |
|
2429 if ($link['access']) { |
|
2430 $trail[] = $link; |
|
2431 } |
|
2432 } |
|
2433 $tree = $curr['below'] ? $curr['below'] : array(); |
|
2434 } |
|
2435 list($key, $curr) = each($tree); |
|
2436 } |
|
2437 // Make sure the current page is in the trail to build the page title, by |
|
2438 // appending either the preferred link or the menu router item for the |
|
2439 // current page. Exclude it if we are on the front page. |
|
2440 $last = end($trail); |
|
2441 if ($preferred_link && $last['href'] != $preferred_link['href'] && !drupal_is_front_page()) { |
|
2442 $trail[] = $preferred_link; |
|
2443 } |
|
2444 } |
|
2445 return $trail; |
|
2446 } |
|
2447 |
|
2448 /** |
|
2449 * Looks up the preferred menu link for a given system path. |
|
2450 * |
|
2451 * @param $path |
|
2452 * The path; for example, 'node/5'. The function will find the corresponding |
|
2453 * menu link ('node/5' if it exists, or fallback to 'node/%'). |
|
2454 * @param $selected_menu |
|
2455 * The name of a menu used to restrict the search for a preferred menu link. |
|
2456 * If not specified, all the menus returned by menu_get_active_menu_names() |
|
2457 * will be used. |
|
2458 * |
|
2459 * @return |
|
2460 * A fully translated menu link, or FALSE if no matching menu link was |
|
2461 * found. The most specific menu link ('node/5' preferred over 'node/%') in |
|
2462 * the most preferred menu (as defined by menu_get_active_menu_names()) is |
|
2463 * returned. |
|
2464 */ |
|
2465 function menu_link_get_preferred($path = NULL, $selected_menu = NULL) { |
|
2466 $preferred_links = &drupal_static(__FUNCTION__); |
|
2467 |
|
2468 if (!isset($path)) { |
|
2469 $path = $_GET['q']; |
|
2470 } |
|
2471 |
|
2472 if (empty($selected_menu)) { |
|
2473 // Use an illegal menu name as the key for the preferred menu link. |
|
2474 $selected_menu = MENU_PREFERRED_LINK; |
|
2475 } |
|
2476 |
|
2477 if (!isset($preferred_links[$path])) { |
|
2478 // Look for the correct menu link by building a list of candidate paths, |
|
2479 // which are ordered by priority (translated hrefs are preferred over |
|
2480 // untranslated paths). Afterwards, the most relevant path is picked from |
|
2481 // the menus, ordered by menu preference. |
|
2482 $item = menu_get_item($path); |
|
2483 $path_candidates = array(); |
|
2484 // 1. The current item href. |
|
2485 $path_candidates[$item['href']] = $item['href']; |
|
2486 // 2. The tab root href of the current item (if any). |
|
2487 if ($item['tab_parent'] && ($tab_root = menu_get_item($item['tab_root_href']))) { |
|
2488 $path_candidates[$tab_root['href']] = $tab_root['href']; |
|
2489 } |
|
2490 // 3. The current item path (with wildcards). |
|
2491 $path_candidates[$item['path']] = $item['path']; |
|
2492 // 4. The tab root path of the current item (if any). |
|
2493 if (!empty($tab_root)) { |
|
2494 $path_candidates[$tab_root['path']] = $tab_root['path']; |
|
2495 } |
|
2496 |
|
2497 // Retrieve a list of menu names, ordered by preference. |
|
2498 $menu_names = menu_get_active_menu_names(); |
|
2499 // Put the selected menu at the front of the list. |
|
2500 array_unshift($menu_names, $selected_menu); |
|
2501 |
|
2502 $query = db_select('menu_links', 'ml', array('fetch' => PDO::FETCH_ASSOC)); |
|
2503 $query->leftJoin('menu_router', 'm', 'm.path = ml.router_path'); |
|
2504 $query->fields('ml'); |
|
2505 // Weight must be taken from {menu_links}, not {menu_router}. |
|
2506 $query->addField('ml', 'weight', 'link_weight'); |
|
2507 $query->fields('m'); |
|
2508 $query->condition('ml.link_path', $path_candidates, 'IN'); |
|
2509 $query->addTag('preferred_menu_links'); |
|
2510 |
|
2511 // Sort candidates by link path and menu name. |
|
2512 $candidates = array(); |
|
2513 foreach ($query->execute() as $candidate) { |
|
2514 $candidate['weight'] = $candidate['link_weight']; |
|
2515 $candidates[$candidate['link_path']][$candidate['menu_name']] = $candidate; |
|
2516 // Add any menus not already in the menu name search list. |
|
2517 if (!in_array($candidate['menu_name'], $menu_names)) { |
|
2518 $menu_names[] = $candidate['menu_name']; |
|
2519 } |
|
2520 } |
|
2521 |
|
2522 // Store the most specific link for each menu. Also save the most specific |
|
2523 // link of the most preferred menu in $preferred_link. |
|
2524 foreach ($path_candidates as $link_path) { |
|
2525 if (isset($candidates[$link_path])) { |
|
2526 foreach ($menu_names as $menu_name) { |
|
2527 if (empty($preferred_links[$path][$menu_name]) && isset($candidates[$link_path][$menu_name])) { |
|
2528 $candidate_item = $candidates[$link_path][$menu_name]; |
|
2529 $map = explode('/', $path); |
|
2530 _menu_translate($candidate_item, $map); |
|
2531 if ($candidate_item['access']) { |
|
2532 $preferred_links[$path][$menu_name] = $candidate_item; |
|
2533 if (empty($preferred_links[$path][MENU_PREFERRED_LINK])) { |
|
2534 // Store the most specific link. |
|
2535 $preferred_links[$path][MENU_PREFERRED_LINK] = $candidate_item; |
|
2536 } |
|
2537 } |
|
2538 } |
|
2539 } |
|
2540 } |
|
2541 } |
|
2542 } |
|
2543 |
|
2544 return isset($preferred_links[$path][$selected_menu]) ? $preferred_links[$path][$selected_menu] : FALSE; |
|
2545 } |
|
2546 |
|
2547 /** |
|
2548 * Gets the active trail (path to root menu root) of the current page. |
|
2549 * |
|
2550 * If a trail is supplied to menu_set_active_trail(), that value is returned. If |
|
2551 * a trail is not supplied to menu_set_active_trail(), the path to the current |
|
2552 * page is calculated and returned. The calculated trail is also saved as a |
|
2553 * static value for use by subsequent calls to menu_get_active_trail(). |
|
2554 * |
|
2555 * @return |
|
2556 * Path to menu root of the current page, as an array of menu link items, |
|
2557 * starting with the site's home page. Each link item is an associative array |
|
2558 * with the following components: |
|
2559 * - title: Title of the item. |
|
2560 * - href: Drupal path of the item. |
|
2561 * - localized_options: Options for passing into the l() function. |
|
2562 * - type: A menu type constant, such as MENU_DEFAULT_LOCAL_TASK, or 0 to |
|
2563 * indicate it's not really in the menu (used for the home page item). |
|
2564 */ |
|
2565 function menu_get_active_trail() { |
|
2566 return menu_set_active_trail(); |
|
2567 } |
|
2568 |
|
2569 /** |
|
2570 * Gets the breadcrumb for the current page, as determined by the active trail. |
|
2571 * |
|
2572 * @see menu_set_active_trail() |
|
2573 */ |
|
2574 function menu_get_active_breadcrumb() { |
|
2575 $breadcrumb = array(); |
|
2576 |
|
2577 // No breadcrumb for the front page. |
|
2578 if (drupal_is_front_page()) { |
|
2579 return $breadcrumb; |
|
2580 } |
|
2581 |
|
2582 $item = menu_get_item(); |
|
2583 if (!empty($item['access'])) { |
|
2584 $active_trail = menu_get_active_trail(); |
|
2585 |
|
2586 // Allow modules to alter the breadcrumb, if possible, as that is much |
|
2587 // faster than rebuilding an entirely new active trail. |
|
2588 drupal_alter('menu_breadcrumb', $active_trail, $item); |
|
2589 |
|
2590 // Don't show a link to the current page in the breadcrumb trail. |
|
2591 $end = end($active_trail); |
|
2592 if ($item['href'] == $end['href']) { |
|
2593 array_pop($active_trail); |
|
2594 } |
|
2595 |
|
2596 // Remove the tab root (parent) if the current path links to its parent. |
|
2597 // Normally, the tab root link is included in the breadcrumb, as soon as we |
|
2598 // are on a local task or any other child link. However, if we are on a |
|
2599 // default local task (e.g., node/%/view), then we do not want the tab root |
|
2600 // link (e.g., node/%) to appear, as it would be identical to the current |
|
2601 // page. Since this behavior also needs to work recursively (i.e., on |
|
2602 // default local tasks of default local tasks), and since the last non-task |
|
2603 // link in the trail is used as page title (see menu_get_active_title()), |
|
2604 // this condition cannot be cleanly integrated into menu_get_active_trail(). |
|
2605 // menu_get_active_trail() already skips all links that link to their parent |
|
2606 // (commonly MENU_DEFAULT_LOCAL_TASK). In order to also hide the parent link |
|
2607 // itself, we always remove the last link in the trail, if the current |
|
2608 // router item links to its parent. |
|
2609 if (($item['type'] & MENU_LINKS_TO_PARENT) == MENU_LINKS_TO_PARENT) { |
|
2610 array_pop($active_trail); |
|
2611 } |
|
2612 |
|
2613 foreach ($active_trail as $parent) { |
|
2614 $breadcrumb[] = l($parent['title'], $parent['href'], $parent['localized_options']); |
|
2615 } |
|
2616 } |
|
2617 return $breadcrumb; |
|
2618 } |
|
2619 |
|
2620 /** |
|
2621 * Gets the title of the current page, as determined by the active trail. |
|
2622 */ |
|
2623 function menu_get_active_title() { |
|
2624 $active_trail = menu_get_active_trail(); |
|
2625 $local_task_title = NULL; |
|
2626 |
|
2627 foreach (array_reverse($active_trail) as $item) { |
|
2628 // Local task titles are displayed as tabs and therefore should not be |
|
2629 // repeated as the page title. However, if the local task appears in a |
|
2630 // top-level menu, it is no longer a "local task" anymore (the front page |
|
2631 // of the site does not have tabs) so it is better to use the local task |
|
2632 // title in that case than to fall back on the front page link in the |
|
2633 // active trail (which is usually "Home" and would not make sense in this |
|
2634 // context). |
|
2635 if ((bool) ($item['type'] & MENU_IS_LOCAL_TASK)) { |
|
2636 // A local task title is being skipped; track it in case it needs to be |
|
2637 // used later. |
|
2638 $local_task_title = $item['title']; |
|
2639 } |
|
2640 else { |
|
2641 // This is not a local task, so use it for the page title (unless the |
|
2642 // conditions described above are met). |
|
2643 if (isset($local_task_title) && isset($item['href']) && $item['href'] == '<front>') { |
|
2644 return $local_task_title; |
|
2645 } |
|
2646 else { |
|
2647 return $item['title']; |
|
2648 } |
|
2649 } |
|
2650 } |
|
2651 } |
|
2652 |
|
2653 /** |
|
2654 * Gets a translated, access-checked menu link that is ready for rendering. |
|
2655 * |
|
2656 * This function should never be called from within node_load() or any other |
|
2657 * function used as a menu object load function since an infinite recursion may |
|
2658 * occur. |
|
2659 * |
|
2660 * @param $mlid |
|
2661 * The mlid of the menu item. |
|
2662 * |
|
2663 * @return |
|
2664 * A menu link, with $item['access'] filled and link translated for |
|
2665 * rendering. |
|
2666 */ |
|
2667 function menu_link_load($mlid) { |
|
2668 if (is_numeric($mlid)) { |
|
2669 $query = db_select('menu_links', 'ml'); |
|
2670 $query->leftJoin('menu_router', 'm', 'm.path = ml.router_path'); |
|
2671 $query->fields('ml'); |
|
2672 // Weight should be taken from {menu_links}, not {menu_router}. |
|
2673 $query->addField('ml', 'weight', 'link_weight'); |
|
2674 $query->fields('m'); |
|
2675 $query->condition('ml.mlid', $mlid); |
|
2676 if ($item = $query->execute()->fetchAssoc()) { |
|
2677 $item['weight'] = $item['link_weight']; |
|
2678 _menu_link_translate($item); |
|
2679 return $item; |
|
2680 } |
|
2681 } |
|
2682 return FALSE; |
|
2683 } |
|
2684 |
|
2685 /** |
|
2686 * Clears the cached data for a single named menu. |
|
2687 */ |
|
2688 function menu_cache_clear($menu_name = 'navigation') { |
|
2689 $cache_cleared = &drupal_static(__FUNCTION__, array()); |
|
2690 |
|
2691 if (empty($cache_cleared[$menu_name])) { |
|
2692 cache_clear_all('links:' . $menu_name . ':', 'cache_menu', TRUE); |
|
2693 $cache_cleared[$menu_name] = 1; |
|
2694 } |
|
2695 elseif ($cache_cleared[$menu_name] == 1) { |
|
2696 drupal_register_shutdown_function('cache_clear_all', 'links:' . $menu_name . ':', 'cache_menu', TRUE); |
|
2697 $cache_cleared[$menu_name] = 2; |
|
2698 } |
|
2699 |
|
2700 // Also clear the menu system static caches. |
|
2701 menu_reset_static_cache(); |
|
2702 } |
|
2703 |
|
2704 /** |
|
2705 * Clears all cached menu data. |
|
2706 * |
|
2707 * This should be called any time broad changes |
|
2708 * might have been made to the router items or menu links. |
|
2709 */ |
|
2710 function menu_cache_clear_all() { |
|
2711 cache_clear_all('*', 'cache_menu', TRUE); |
|
2712 menu_reset_static_cache(); |
|
2713 } |
|
2714 |
|
2715 /** |
|
2716 * Resets the menu system static cache. |
|
2717 */ |
|
2718 function menu_reset_static_cache() { |
|
2719 drupal_static_reset('_menu_build_tree'); |
|
2720 drupal_static_reset('menu_tree'); |
|
2721 drupal_static_reset('menu_tree_all_data'); |
|
2722 drupal_static_reset('menu_tree_page_data'); |
|
2723 drupal_static_reset('menu_load_all'); |
|
2724 drupal_static_reset('menu_link_get_preferred'); |
|
2725 } |
|
2726 |
|
2727 /** |
|
2728 * Checks whether a menu_rebuild() is necessary. |
|
2729 */ |
|
2730 function _menu_check_rebuild() { |
|
2731 // To absolutely ensure that the menu rebuild is required, re-load the |
|
2732 // variables in case they were set by another process. |
|
2733 $variables = variable_initialize(); |
|
2734 if (empty($variables['menu_rebuild_needed']) && !empty($variables['menu_masks'])) { |
|
2735 unset($GLOBALS['conf']['menu_rebuild_needed']); |
|
2736 $GLOBALS['conf']['menu_masks'] = $variables['menu_masks']; |
|
2737 return FALSE; |
|
2738 } |
|
2739 return TRUE; |
|
2740 } |
|
2741 |
|
2742 /** |
|
2743 * Populates the database tables used by various menu functions. |
|
2744 * |
|
2745 * This function will clear and populate the {menu_router} table, add entries |
|
2746 * to {menu_links} for new router items, and then remove stale items from |
|
2747 * {menu_links}. If called from update.php or install.php, it will also |
|
2748 * schedule a call to itself on the first real page load from |
|
2749 * menu_execute_active_handler(), because the maintenance page environment |
|
2750 * is different and leaves stale data in the menu tables. |
|
2751 * |
|
2752 * @return |
|
2753 * TRUE if the menu was rebuilt, FALSE if another thread was rebuilding |
|
2754 * in parallel and the current thread just waited for completion. |
|
2755 */ |
|
2756 function menu_rebuild() { |
|
2757 if (!lock_acquire('menu_rebuild')) { |
|
2758 // Wait for another request that is already doing this work. |
|
2759 // We choose to block here since otherwise the router item may not |
|
2760 // be available in menu_execute_active_handler() resulting in a 404. |
|
2761 lock_wait('menu_rebuild'); |
|
2762 |
|
2763 if (_menu_check_rebuild()) { |
|
2764 // If we get here and menu_masks was not set, then it is possible a menu |
|
2765 // is being reloaded, or that the process rebuilding the menu was unable |
|
2766 // to complete successfully. A missing menu_masks variable could result |
|
2767 // in a 404, so re-run the function. |
|
2768 return menu_rebuild(); |
|
2769 } |
|
2770 return FALSE; |
|
2771 } |
|
2772 |
|
2773 $transaction = db_transaction(); |
|
2774 |
|
2775 try { |
|
2776 list($menu, $masks) = menu_router_build(); |
|
2777 _menu_router_save($menu, $masks); |
|
2778 _menu_navigation_links_rebuild($menu); |
|
2779 // Clear the menu, page and block caches. |
|
2780 menu_cache_clear_all(); |
|
2781 _menu_clear_page_cache(); |
|
2782 |
|
2783 if (defined('MAINTENANCE_MODE')) { |
|
2784 variable_set('menu_rebuild_needed', TRUE); |
|
2785 } |
|
2786 else { |
|
2787 variable_del('menu_rebuild_needed'); |
|
2788 } |
|
2789 } |
|
2790 catch (Exception $e) { |
|
2791 $transaction->rollback(); |
|
2792 watchdog_exception('menu', $e); |
|
2793 } |
|
2794 // Explicitly commit the transaction now; this ensures that the database |
|
2795 // operations during the menu rebuild are committed before the lock is made |
|
2796 // available again, since locks may not always reside in the same database |
|
2797 // connection. The lock is acquired outside of the transaction so should also |
|
2798 // be released outside of it. |
|
2799 unset($transaction); |
|
2800 |
|
2801 lock_release('menu_rebuild'); |
|
2802 return TRUE; |
|
2803 } |
|
2804 |
|
2805 /** |
|
2806 * Collects and alters the menu definitions. |
|
2807 */ |
|
2808 function menu_router_build() { |
|
2809 // We need to manually call each module so that we can know which module |
|
2810 // a given item came from. |
|
2811 $callbacks = array(); |
|
2812 foreach (module_implements('menu') as $module) { |
|
2813 $router_items = call_user_func($module . '_menu'); |
|
2814 if (isset($router_items) && is_array($router_items)) { |
|
2815 foreach (array_keys($router_items) as $path) { |
|
2816 $router_items[$path]['module'] = $module; |
|
2817 } |
|
2818 $callbacks = array_merge($callbacks, $router_items); |
|
2819 } |
|
2820 } |
|
2821 // Alter the menu as defined in modules, keys are like user/%user. |
|
2822 drupal_alter('menu', $callbacks); |
|
2823 list($menu, $masks) = _menu_router_build($callbacks); |
|
2824 _menu_router_cache($menu); |
|
2825 |
|
2826 return array($menu, $masks); |
|
2827 } |
|
2828 |
|
2829 /** |
|
2830 * Stores the menu router if we have it in memory. |
|
2831 */ |
|
2832 function _menu_router_cache($new_menu = NULL) { |
|
2833 $menu = &drupal_static(__FUNCTION__); |
|
2834 |
|
2835 if (isset($new_menu)) { |
|
2836 $menu = $new_menu; |
|
2837 } |
|
2838 return $menu; |
|
2839 } |
|
2840 |
|
2841 /** |
|
2842 * Gets the menu router. |
|
2843 */ |
|
2844 function menu_get_router() { |
|
2845 // Check first if we have it in memory already. |
|
2846 $menu = _menu_router_cache(); |
|
2847 if (empty($menu)) { |
|
2848 list($menu, $masks) = menu_router_build(); |
|
2849 } |
|
2850 return $menu; |
|
2851 } |
|
2852 |
|
2853 /** |
|
2854 * Builds a link from a router item. |
|
2855 */ |
|
2856 function _menu_link_build($item) { |
|
2857 // Suggested items are disabled by default. |
|
2858 if ($item['type'] == MENU_SUGGESTED_ITEM) { |
|
2859 $item['hidden'] = 1; |
|
2860 } |
|
2861 // Hide all items that are not visible in the tree. |
|
2862 elseif (!($item['type'] & MENU_VISIBLE_IN_TREE)) { |
|
2863 $item['hidden'] = -1; |
|
2864 } |
|
2865 // Note, we set this as 'system', so that we can be sure to distinguish all |
|
2866 // the menu links generated automatically from entries in {menu_router}. |
|
2867 $item['module'] = 'system'; |
|
2868 $item += array( |
|
2869 'menu_name' => 'navigation', |
|
2870 'link_title' => $item['title'], |
|
2871 'link_path' => $item['path'], |
|
2872 'hidden' => 0, |
|
2873 'options' => empty($item['description']) ? array() : array('attributes' => array('title' => $item['description'])), |
|
2874 ); |
|
2875 return $item; |
|
2876 } |
|
2877 |
|
2878 /** |
|
2879 * Builds menu links for the items in the menu router. |
|
2880 */ |
|
2881 function _menu_navigation_links_rebuild($menu) { |
|
2882 // Add normal and suggested items as links. |
|
2883 $menu_links = array(); |
|
2884 foreach ($menu as $path => $item) { |
|
2885 if ($item['_visible']) { |
|
2886 $menu_links[$path] = $item; |
|
2887 $sort[$path] = $item['_number_parts']; |
|
2888 } |
|
2889 } |
|
2890 if ($menu_links) { |
|
2891 // Keep an array of processed menu links, to allow menu_link_save() to |
|
2892 // check this for parents instead of querying the database. |
|
2893 $parent_candidates = array(); |
|
2894 // Make sure no child comes before its parent. |
|
2895 array_multisort($sort, SORT_NUMERIC, $menu_links); |
|
2896 |
|
2897 foreach ($menu_links as $key => $item) { |
|
2898 $existing_item = db_select('menu_links') |
|
2899 ->fields('menu_links') |
|
2900 ->condition('link_path', $item['path']) |
|
2901 ->condition('module', 'system') |
|
2902 ->execute()->fetchAssoc(); |
|
2903 if ($existing_item) { |
|
2904 $item['mlid'] = $existing_item['mlid']; |
|
2905 // A change in hook_menu may move the link to a different menu |
|
2906 if (empty($item['menu_name']) || ($item['menu_name'] == $existing_item['menu_name'])) { |
|
2907 $item['menu_name'] = $existing_item['menu_name']; |
|
2908 $item['plid'] = $existing_item['plid']; |
|
2909 } |
|
2910 else { |
|
2911 // It moved to a new menu. Let menu_link_save() try to find a new |
|
2912 // parent based on the path. |
|
2913 unset($item['plid']); |
|
2914 } |
|
2915 $item['has_children'] = $existing_item['has_children']; |
|
2916 $item['updated'] = $existing_item['updated']; |
|
2917 } |
|
2918 if ($existing_item && $existing_item['customized']) { |
|
2919 $parent_candidates[$existing_item['mlid']] = $existing_item; |
|
2920 } |
|
2921 else { |
|
2922 $item = _menu_link_build($item); |
|
2923 menu_link_save($item, $existing_item, $parent_candidates); |
|
2924 $parent_candidates[$item['mlid']] = $item; |
|
2925 unset($menu_links[$key]); |
|
2926 } |
|
2927 } |
|
2928 } |
|
2929 $paths = array_keys($menu); |
|
2930 // Updated and customized items whose router paths are gone need new ones. |
|
2931 $result = db_select('menu_links', NULL, array('fetch' => PDO::FETCH_ASSOC)) |
|
2932 ->fields('menu_links', array( |
|
2933 'link_path', |
|
2934 'mlid', |
|
2935 'router_path', |
|
2936 'updated', |
|
2937 )) |
|
2938 ->condition(db_or() |
|
2939 ->condition('updated', 1) |
|
2940 ->condition(db_and() |
|
2941 ->condition('router_path', $paths, 'NOT IN') |
|
2942 ->condition('external', 0) |
|
2943 ->condition('customized', 1) |
|
2944 ) |
|
2945 ) |
|
2946 ->execute(); |
|
2947 foreach ($result as $item) { |
|
2948 $router_path = _menu_find_router_path($item['link_path']); |
|
2949 if (!empty($router_path) && ($router_path != $item['router_path'] || $item['updated'])) { |
|
2950 // If the router path and the link path matches, it's surely a working |
|
2951 // item, so we clear the updated flag. |
|
2952 $updated = $item['updated'] && $router_path != $item['link_path']; |
|
2953 db_update('menu_links') |
|
2954 ->fields(array( |
|
2955 'router_path' => $router_path, |
|
2956 'updated' => (int) $updated, |
|
2957 )) |
|
2958 ->condition('mlid', $item['mlid']) |
|
2959 ->execute(); |
|
2960 } |
|
2961 } |
|
2962 // Find any item whose router path does not exist any more. |
|
2963 $result = db_select('menu_links') |
|
2964 ->fields('menu_links') |
|
2965 ->condition('router_path', $paths, 'NOT IN') |
|
2966 ->condition('external', 0) |
|
2967 ->condition('updated', 0) |
|
2968 ->condition('customized', 0) |
|
2969 ->orderBy('depth', 'DESC') |
|
2970 ->execute(); |
|
2971 // Remove all such items. Starting from those with the greatest depth will |
|
2972 // minimize the amount of re-parenting done by menu_link_delete(). |
|
2973 foreach ($result as $item) { |
|
2974 _menu_delete_item($item, TRUE); |
|
2975 } |
|
2976 } |
|
2977 |
|
2978 /** |
|
2979 * Clones an array of menu links. |
|
2980 * |
|
2981 * @param $links |
|
2982 * An array of menu links to clone. |
|
2983 * @param $menu_name |
|
2984 * (optional) The name of a menu that the links will be cloned for. If not |
|
2985 * set, the cloned links will be in the same menu as the original set of |
|
2986 * links that were passed in. |
|
2987 * |
|
2988 * @return |
|
2989 * An array of menu links with the same properties as the passed-in array, |
|
2990 * but with the link identifiers removed so that a new link will be created |
|
2991 * when any of them is passed in to menu_link_save(). |
|
2992 * |
|
2993 * @see menu_link_save() |
|
2994 */ |
|
2995 function menu_links_clone($links, $menu_name = NULL) { |
|
2996 foreach ($links as &$link) { |
|
2997 unset($link['mlid']); |
|
2998 unset($link['plid']); |
|
2999 if (isset($menu_name)) { |
|
3000 $link['menu_name'] = $menu_name; |
|
3001 } |
|
3002 } |
|
3003 return $links; |
|
3004 } |
|
3005 |
|
3006 /** |
|
3007 * Returns an array containing all links for a menu. |
|
3008 * |
|
3009 * @param $menu_name |
|
3010 * The name of the menu whose links should be returned. |
|
3011 * |
|
3012 * @return |
|
3013 * An array of menu links. |
|
3014 */ |
|
3015 function menu_load_links($menu_name) { |
|
3016 $links = db_select('menu_links', 'ml', array('fetch' => PDO::FETCH_ASSOC)) |
|
3017 ->fields('ml') |
|
3018 ->condition('ml.menu_name', $menu_name) |
|
3019 // Order by weight so as to be helpful for menus that are only one level |
|
3020 // deep. |
|
3021 ->orderBy('weight') |
|
3022 ->execute() |
|
3023 ->fetchAll(); |
|
3024 |
|
3025 foreach ($links as &$link) { |
|
3026 $link['options'] = unserialize($link['options']); |
|
3027 } |
|
3028 return $links; |
|
3029 } |
|
3030 |
|
3031 /** |
|
3032 * Deletes all links for a menu. |
|
3033 * |
|
3034 * @param $menu_name |
|
3035 * The name of the menu whose links will be deleted. |
|
3036 */ |
|
3037 function menu_delete_links($menu_name) { |
|
3038 $links = menu_load_links($menu_name); |
|
3039 foreach ($links as $link) { |
|
3040 // To speed up the deletion process, we reset some link properties that |
|
3041 // would trigger re-parenting logic in _menu_delete_item() and |
|
3042 // _menu_update_parental_status(). |
|
3043 $link['has_children'] = FALSE; |
|
3044 $link['plid'] = 0; |
|
3045 _menu_delete_item($link); |
|
3046 } |
|
3047 } |
|
3048 |
|
3049 /** |
|
3050 * Delete one or several menu links. |
|
3051 * |
|
3052 * @param $mlid |
|
3053 * A valid menu link mlid or NULL. If NULL, $path is used. |
|
3054 * @param $path |
|
3055 * The path to the menu items to be deleted. $mlid must be NULL. |
|
3056 */ |
|
3057 function menu_link_delete($mlid, $path = NULL) { |
|
3058 if (isset($mlid)) { |
|
3059 _menu_delete_item(db_query("SELECT * FROM {menu_links} WHERE mlid = :mlid", array(':mlid' => $mlid))->fetchAssoc()); |
|
3060 } |
|
3061 else { |
|
3062 $result = db_query("SELECT * FROM {menu_links} WHERE link_path = :link_path", array(':link_path' => $path)); |
|
3063 foreach ($result as $link) { |
|
3064 _menu_delete_item($link); |
|
3065 } |
|
3066 } |
|
3067 } |
|
3068 |
|
3069 /** |
|
3070 * Deletes a single menu link. |
|
3071 * |
|
3072 * @param $item |
|
3073 * Item to be deleted. |
|
3074 * @param $force |
|
3075 * Forces deletion. Internal use only, setting to TRUE is discouraged. |
|
3076 * |
|
3077 * @see menu_link_delete() |
|
3078 */ |
|
3079 function _menu_delete_item($item, $force = FALSE) { |
|
3080 $item = is_object($item) ? get_object_vars($item) : $item; |
|
3081 if ($item && ($item['module'] != 'system' || $item['updated'] || $force)) { |
|
3082 // Children get re-attached to the item's parent. |
|
3083 if ($item['has_children']) { |
|
3084 $result = db_query("SELECT mlid FROM {menu_links} WHERE plid = :plid", array(':plid' => $item['mlid'])); |
|
3085 foreach ($result as $m) { |
|
3086 $child = menu_link_load($m->mlid); |
|
3087 $child['plid'] = $item['plid']; |
|
3088 menu_link_save($child); |
|
3089 } |
|
3090 } |
|
3091 |
|
3092 // Notify modules we are deleting the item. |
|
3093 module_invoke_all('menu_link_delete', $item); |
|
3094 |
|
3095 db_delete('menu_links')->condition('mlid', $item['mlid'])->execute(); |
|
3096 |
|
3097 // Update the has_children status of the parent. |
|
3098 _menu_update_parental_status($item); |
|
3099 menu_cache_clear($item['menu_name']); |
|
3100 _menu_clear_page_cache(); |
|
3101 } |
|
3102 } |
|
3103 |
|
3104 /** |
|
3105 * Saves a menu link. |
|
3106 * |
|
3107 * After calling this function, rebuild the menu cache using |
|
3108 * menu_cache_clear_all(). |
|
3109 * |
|
3110 * @param $item |
|
3111 * An associative array representing a menu link item, with elements: |
|
3112 * - link_path: (required) The path of the menu item, which should be |
|
3113 * normalized first by calling drupal_get_normal_path() on it. |
|
3114 * - link_title: (required) Title to appear in menu for the link. |
|
3115 * - menu_name: (optional) The machine name of the menu for the link. |
|
3116 * Defaults to 'navigation'. |
|
3117 * - weight: (optional) Integer to determine position in menu. Default is 0. |
|
3118 * - expanded: (optional) Boolean that determines if the item is expanded. |
|
3119 * - options: (optional) An array of options, see l() for more. |
|
3120 * - mlid: (optional) Menu link identifier, the primary integer key for each |
|
3121 * menu link. Can be set to an existing value, or to 0 or NULL |
|
3122 * to insert a new link. |
|
3123 * - plid: (optional) The mlid of the parent. |
|
3124 * - router_path: (optional) The path of the relevant router item. |
|
3125 * @param $existing_item |
|
3126 * Optional, the current record from the {menu_links} table as an array. |
|
3127 * @param $parent_candidates |
|
3128 * Optional array of menu links keyed by mlid. Used by |
|
3129 * _menu_navigation_links_rebuild() only. |
|
3130 * |
|
3131 * @return |
|
3132 * The mlid of the saved menu link, or FALSE if the menu link could not be |
|
3133 * saved. |
|
3134 */ |
|
3135 function menu_link_save(&$item, $existing_item = array(), $parent_candidates = array()) { |
|
3136 drupal_alter('menu_link', $item); |
|
3137 |
|
3138 // This is the easiest way to handle the unique internal path '<front>', |
|
3139 // since a path marked as external does not need to match a router path. |
|
3140 $item['external'] = (url_is_external($item['link_path']) || $item['link_path'] == '<front>') ? 1 : 0; |
|
3141 // Load defaults. |
|
3142 $item += array( |
|
3143 'menu_name' => 'navigation', |
|
3144 'weight' => 0, |
|
3145 'link_title' => '', |
|
3146 'hidden' => 0, |
|
3147 'has_children' => 0, |
|
3148 'expanded' => 0, |
|
3149 'options' => array(), |
|
3150 'module' => 'menu', |
|
3151 'customized' => 0, |
|
3152 'updated' => 0, |
|
3153 ); |
|
3154 if (isset($item['mlid'])) { |
|
3155 if (!$existing_item) { |
|
3156 $existing_item = db_query('SELECT * FROM {menu_links} WHERE mlid = :mlid', array('mlid' => $item['mlid']))->fetchAssoc(); |
|
3157 } |
|
3158 if ($existing_item) { |
|
3159 $existing_item['options'] = unserialize($existing_item['options']); |
|
3160 } |
|
3161 } |
|
3162 else { |
|
3163 $existing_item = FALSE; |
|
3164 } |
|
3165 |
|
3166 // Try to find a parent link. If found, assign it and derive its menu. |
|
3167 $parent = _menu_link_find_parent($item, $parent_candidates); |
|
3168 if (!empty($parent['mlid'])) { |
|
3169 $item['plid'] = $parent['mlid']; |
|
3170 $item['menu_name'] = $parent['menu_name']; |
|
3171 } |
|
3172 // If no corresponding parent link was found, move the link to the top-level. |
|
3173 else { |
|
3174 $item['plid'] = 0; |
|
3175 } |
|
3176 $menu_name = $item['menu_name']; |
|
3177 |
|
3178 if (!$existing_item) { |
|
3179 $item['mlid'] = db_insert('menu_links') |
|
3180 ->fields(array( |
|
3181 'menu_name' => $item['menu_name'], |
|
3182 'plid' => $item['plid'], |
|
3183 'link_path' => $item['link_path'], |
|
3184 'hidden' => $item['hidden'], |
|
3185 'external' => $item['external'], |
|
3186 'has_children' => $item['has_children'], |
|
3187 'expanded' => $item['expanded'], |
|
3188 'weight' => $item['weight'], |
|
3189 'module' => $item['module'], |
|
3190 'link_title' => $item['link_title'], |
|
3191 'options' => serialize($item['options']), |
|
3192 'customized' => $item['customized'], |
|
3193 'updated' => $item['updated'], |
|
3194 )) |
|
3195 ->execute(); |
|
3196 } |
|
3197 |
|
3198 // Directly fill parents for top-level links. |
|
3199 if ($item['plid'] == 0) { |
|
3200 $item['p1'] = $item['mlid']; |
|
3201 for ($i = 2; $i <= MENU_MAX_DEPTH; $i++) { |
|
3202 $item["p$i"] = 0; |
|
3203 } |
|
3204 $item['depth'] = 1; |
|
3205 } |
|
3206 // Otherwise, ensure that this link's depth is not beyond the maximum depth |
|
3207 // and fill parents based on the parent link. |
|
3208 else { |
|
3209 if ($item['has_children'] && $existing_item) { |
|
3210 $limit = MENU_MAX_DEPTH - menu_link_children_relative_depth($existing_item) - 1; |
|
3211 } |
|
3212 else { |
|
3213 $limit = MENU_MAX_DEPTH - 1; |
|
3214 } |
|
3215 if ($parent['depth'] > $limit) { |
|
3216 return FALSE; |
|
3217 } |
|
3218 $item['depth'] = $parent['depth'] + 1; |
|
3219 _menu_link_parents_set($item, $parent); |
|
3220 } |
|
3221 // Need to check both plid and menu_name, since plid can be 0 in any menu. |
|
3222 if ($existing_item && ($item['plid'] != $existing_item['plid'] || $menu_name != $existing_item['menu_name'])) { |
|
3223 _menu_link_move_children($item, $existing_item); |
|
3224 } |
|
3225 // Find the router_path. |
|
3226 if (empty($item['router_path']) || !$existing_item || ($existing_item['link_path'] != $item['link_path'])) { |
|
3227 if ($item['external']) { |
|
3228 $item['router_path'] = ''; |
|
3229 } |
|
3230 else { |
|
3231 // Find the router path which will serve this path. |
|
3232 $item['parts'] = explode('/', $item['link_path'], MENU_MAX_PARTS); |
|
3233 $item['router_path'] = _menu_find_router_path($item['link_path']); |
|
3234 } |
|
3235 } |
|
3236 // If every value in $existing_item is the same in the $item, there is no |
|
3237 // reason to run the update queries or clear the caches. We use |
|
3238 // array_intersect_key() with the $item as the first parameter because |
|
3239 // $item may have additional keys left over from building a router entry. |
|
3240 // The intersect removes the extra keys, allowing a meaningful comparison. |
|
3241 if (!$existing_item || (array_intersect_key($item, $existing_item) != $existing_item)) { |
|
3242 db_update('menu_links') |
|
3243 ->fields(array( |
|
3244 'menu_name' => $item['menu_name'], |
|
3245 'plid' => $item['plid'], |
|
3246 'link_path' => $item['link_path'], |
|
3247 'router_path' => $item['router_path'], |
|
3248 'hidden' => $item['hidden'], |
|
3249 'external' => $item['external'], |
|
3250 'has_children' => $item['has_children'], |
|
3251 'expanded' => $item['expanded'], |
|
3252 'weight' => $item['weight'], |
|
3253 'depth' => $item['depth'], |
|
3254 'p1' => $item['p1'], |
|
3255 'p2' => $item['p2'], |
|
3256 'p3' => $item['p3'], |
|
3257 'p4' => $item['p4'], |
|
3258 'p5' => $item['p5'], |
|
3259 'p6' => $item['p6'], |
|
3260 'p7' => $item['p7'], |
|
3261 'p8' => $item['p8'], |
|
3262 'p9' => $item['p9'], |
|
3263 'module' => $item['module'], |
|
3264 'link_title' => $item['link_title'], |
|
3265 'options' => serialize($item['options']), |
|
3266 'customized' => $item['customized'], |
|
3267 )) |
|
3268 ->condition('mlid', $item['mlid']) |
|
3269 ->execute(); |
|
3270 // Check the has_children status of the parent. |
|
3271 _menu_update_parental_status($item); |
|
3272 menu_cache_clear($menu_name); |
|
3273 if ($existing_item && $menu_name != $existing_item['menu_name']) { |
|
3274 menu_cache_clear($existing_item['menu_name']); |
|
3275 } |
|
3276 // Notify modules we have acted on a menu item. |
|
3277 $hook = 'menu_link_insert'; |
|
3278 if ($existing_item) { |
|
3279 $hook = 'menu_link_update'; |
|
3280 } |
|
3281 module_invoke_all($hook, $item); |
|
3282 // Now clear the cache. |
|
3283 _menu_clear_page_cache(); |
|
3284 } |
|
3285 return $item['mlid']; |
|
3286 } |
|
3287 |
|
3288 /** |
|
3289 * Finds a possible parent for a given menu link. |
|
3290 * |
|
3291 * Because the parent of a given link might not exist anymore in the database, |
|
3292 * we apply a set of heuristics to determine a proper parent: |
|
3293 * |
|
3294 * - use the passed parent link if specified and existing. |
|
3295 * - else, use the first existing link down the previous link hierarchy |
|
3296 * - else, for system menu links (derived from hook_menu()), reparent |
|
3297 * based on the path hierarchy. |
|
3298 * |
|
3299 * @param $menu_link |
|
3300 * A menu link. |
|
3301 * @param $parent_candidates |
|
3302 * An array of menu links keyed by mlid. |
|
3303 * |
|
3304 * @return |
|
3305 * A menu link structure of the possible parent or FALSE if no valid parent |
|
3306 * has been found. |
|
3307 */ |
|
3308 function _menu_link_find_parent($menu_link, $parent_candidates = array()) { |
|
3309 $parent = FALSE; |
|
3310 |
|
3311 // This item is explicitely top-level, skip the rest of the parenting. |
|
3312 if (isset($menu_link['plid']) && empty($menu_link['plid'])) { |
|
3313 return $parent; |
|
3314 } |
|
3315 |
|
3316 // If we have a parent link ID, try to use that. |
|
3317 $candidates = array(); |
|
3318 if (isset($menu_link['plid'])) { |
|
3319 $candidates[] = $menu_link['plid']; |
|
3320 } |
|
3321 |
|
3322 // Else, if we have a link hierarchy try to find a valid parent in there. |
|
3323 if (!empty($menu_link['depth']) && $menu_link['depth'] > 1) { |
|
3324 for ($depth = $menu_link['depth'] - 1; $depth >= 1; $depth--) { |
|
3325 $candidates[] = $menu_link['p' . $depth]; |
|
3326 } |
|
3327 } |
|
3328 |
|
3329 foreach ($candidates as $mlid) { |
|
3330 if (isset($parent_candidates[$mlid])) { |
|
3331 $parent = $parent_candidates[$mlid]; |
|
3332 } |
|
3333 else { |
|
3334 $parent = db_query("SELECT * FROM {menu_links} WHERE mlid = :mlid", array(':mlid' => $mlid))->fetchAssoc(); |
|
3335 } |
|
3336 if ($parent) { |
|
3337 return $parent; |
|
3338 } |
|
3339 } |
|
3340 |
|
3341 // If everything else failed, try to derive the parent from the path |
|
3342 // hierarchy. This only makes sense for links derived from menu router |
|
3343 // items (ie. from hook_menu()). |
|
3344 if ($menu_link['module'] == 'system') { |
|
3345 $query = db_select('menu_links'); |
|
3346 $query->condition('module', 'system'); |
|
3347 // We always respect the link's 'menu_name'; inheritance for router items is |
|
3348 // ensured in _menu_router_build(). |
|
3349 $query->condition('menu_name', $menu_link['menu_name']); |
|
3350 |
|
3351 // Find the parent - it must be unique. |
|
3352 $parent_path = $menu_link['link_path']; |
|
3353 do { |
|
3354 $parent = FALSE; |
|
3355 $parent_path = substr($parent_path, 0, strrpos($parent_path, '/')); |
|
3356 $new_query = clone $query; |
|
3357 $new_query->condition('link_path', $parent_path); |
|
3358 // Only valid if we get a unique result. |
|
3359 if ($new_query->countQuery()->execute()->fetchField() == 1) { |
|
3360 $parent = $new_query->fields('menu_links')->execute()->fetchAssoc(); |
|
3361 } |
|
3362 } while ($parent === FALSE && $parent_path); |
|
3363 } |
|
3364 |
|
3365 return $parent; |
|
3366 } |
|
3367 |
|
3368 /** |
|
3369 * Clears the page and block caches at most twice per page load. |
|
3370 */ |
|
3371 function _menu_clear_page_cache() { |
|
3372 $cache_cleared = &drupal_static(__FUNCTION__, 0); |
|
3373 |
|
3374 // Clear the page and block caches, but at most twice, including at |
|
3375 // the end of the page load when there are multiple links saved or deleted. |
|
3376 if ($cache_cleared == 0) { |
|
3377 cache_clear_all(); |
|
3378 // Keep track of which menus have expanded items. |
|
3379 _menu_set_expanded_menus(); |
|
3380 $cache_cleared = 1; |
|
3381 } |
|
3382 elseif ($cache_cleared == 1) { |
|
3383 drupal_register_shutdown_function('cache_clear_all'); |
|
3384 // Keep track of which menus have expanded items. |
|
3385 drupal_register_shutdown_function('_menu_set_expanded_menus'); |
|
3386 $cache_cleared = 2; |
|
3387 } |
|
3388 } |
|
3389 |
|
3390 /** |
|
3391 * Updates a list of menus with expanded items. |
|
3392 */ |
|
3393 function _menu_set_expanded_menus() { |
|
3394 $names = db_query("SELECT menu_name FROM {menu_links} WHERE expanded <> 0 GROUP BY menu_name")->fetchCol(); |
|
3395 variable_set('menu_expanded', $names); |
|
3396 } |
|
3397 |
|
3398 /** |
|
3399 * Finds the router path which will serve this path. |
|
3400 * |
|
3401 * @param $link_path |
|
3402 * The path for we are looking up its router path. |
|
3403 * |
|
3404 * @return |
|
3405 * A path from $menu keys or empty if $link_path points to a nonexisting |
|
3406 * place. |
|
3407 */ |
|
3408 function _menu_find_router_path($link_path) { |
|
3409 // $menu will only have data during a menu rebuild. |
|
3410 $menu = _menu_router_cache(); |
|
3411 |
|
3412 $router_path = $link_path; |
|
3413 $parts = explode('/', $link_path, MENU_MAX_PARTS); |
|
3414 $ancestors = menu_get_ancestors($parts); |
|
3415 |
|
3416 if (empty($menu)) { |
|
3417 // Not during a menu rebuild, so look up in the database. |
|
3418 $router_path = (string) db_select('menu_router') |
|
3419 ->fields('menu_router', array('path')) |
|
3420 ->condition('path', $ancestors, 'IN') |
|
3421 ->orderBy('fit', 'DESC') |
|
3422 ->range(0, 1) |
|
3423 ->execute()->fetchField(); |
|
3424 } |
|
3425 elseif (!isset($menu[$router_path])) { |
|
3426 // Add an empty router path as a fallback. |
|
3427 $ancestors[] = ''; |
|
3428 foreach ($ancestors as $key => $router_path) { |
|
3429 if (isset($menu[$router_path])) { |
|
3430 // Exit the loop leaving $router_path as the first match. |
|
3431 break; |
|
3432 } |
|
3433 } |
|
3434 // If we did not find the path, $router_path will be the empty string |
|
3435 // at the end of $ancestors. |
|
3436 } |
|
3437 return $router_path; |
|
3438 } |
|
3439 |
|
3440 /** |
|
3441 * Inserts, updates, or deletes an uncustomized menu link related to a module. |
|
3442 * |
|
3443 * @param $module |
|
3444 * The name of the module. |
|
3445 * @param $op |
|
3446 * Operation to perform: insert, update or delete. |
|
3447 * @param $link_path |
|
3448 * The path this link points to. |
|
3449 * @param $link_title |
|
3450 * Title of the link to insert or new title to update the link to. |
|
3451 * Unused for delete. |
|
3452 * |
|
3453 * @return |
|
3454 * The insert op returns the mlid of the new item. Others op return NULL. |
|
3455 */ |
|
3456 function menu_link_maintain($module, $op, $link_path, $link_title) { |
|
3457 switch ($op) { |
|
3458 case 'insert': |
|
3459 $menu_link = array( |
|
3460 'link_title' => $link_title, |
|
3461 'link_path' => $link_path, |
|
3462 'module' => $module, |
|
3463 ); |
|
3464 return menu_link_save($menu_link); |
|
3465 break; |
|
3466 case 'update': |
|
3467 $result = db_query("SELECT * FROM {menu_links} WHERE link_path = :link_path AND module = :module AND customized = 0", array(':link_path' => $link_path, ':module' => $module))->fetchAll(PDO::FETCH_ASSOC); |
|
3468 foreach ($result as $link) { |
|
3469 $link['link_title'] = $link_title; |
|
3470 $link['options'] = unserialize($link['options']); |
|
3471 menu_link_save($link); |
|
3472 } |
|
3473 break; |
|
3474 case 'delete': |
|
3475 menu_link_delete(NULL, $link_path); |
|
3476 break; |
|
3477 } |
|
3478 } |
|
3479 |
|
3480 /** |
|
3481 * Finds the depth of an item's children relative to its depth. |
|
3482 * |
|
3483 * For example, if the item has a depth of 2, and the maximum of any child in |
|
3484 * the menu link tree is 5, the relative depth is 3. |
|
3485 * |
|
3486 * @param $item |
|
3487 * An array representing a menu link item. |
|
3488 * |
|
3489 * @return |
|
3490 * The relative depth, or zero. |
|
3491 * |
|
3492 */ |
|
3493 function menu_link_children_relative_depth($item) { |
|
3494 $query = db_select('menu_links'); |
|
3495 $query->addField('menu_links', 'depth'); |
|
3496 $query->condition('menu_name', $item['menu_name']); |
|
3497 $query->orderBy('depth', 'DESC'); |
|
3498 $query->range(0, 1); |
|
3499 |
|
3500 $i = 1; |
|
3501 $p = 'p1'; |
|
3502 while ($i <= MENU_MAX_DEPTH && $item[$p]) { |
|
3503 $query->condition($p, $item[$p]); |
|
3504 $p = 'p' . ++$i; |
|
3505 } |
|
3506 |
|
3507 $max_depth = $query->execute()->fetchField(); |
|
3508 |
|
3509 return ($max_depth > $item['depth']) ? $max_depth - $item['depth'] : 0; |
|
3510 } |
|
3511 |
|
3512 /** |
|
3513 * Updates the children of a menu link that is being moved. |
|
3514 * |
|
3515 * The menu name, parents (p1 - p6), and depth are updated for all children of |
|
3516 * the link, and the has_children status of the previous parent is updated. |
|
3517 */ |
|
3518 function _menu_link_move_children($item, $existing_item) { |
|
3519 $query = db_update('menu_links'); |
|
3520 |
|
3521 $query->fields(array('menu_name' => $item['menu_name'])); |
|
3522 |
|
3523 $p = 'p1'; |
|
3524 $expressions = array(); |
|
3525 for ($i = 1; $i <= $item['depth']; $p = 'p' . ++$i) { |
|
3526 $expressions[] = array($p, ":p_$i", array(":p_$i" => $item[$p])); |
|
3527 } |
|
3528 $j = $existing_item['depth'] + 1; |
|
3529 while ($i <= MENU_MAX_DEPTH && $j <= MENU_MAX_DEPTH) { |
|
3530 $expressions[] = array('p' . $i++, 'p' . $j++, array()); |
|
3531 } |
|
3532 while ($i <= MENU_MAX_DEPTH) { |
|
3533 $expressions[] = array('p' . $i++, 0, array()); |
|
3534 } |
|
3535 |
|
3536 $shift = $item['depth'] - $existing_item['depth']; |
|
3537 if ($shift > 0) { |
|
3538 // The order of expressions must be reversed so the new values don't |
|
3539 // overwrite the old ones before they can be used because "Single-table |
|
3540 // UPDATE assignments are generally evaluated from left to right" |
|
3541 // see: http://dev.mysql.com/doc/refman/5.0/en/update.html |
|
3542 $expressions = array_reverse($expressions); |
|
3543 } |
|
3544 foreach ($expressions as $expression) { |
|
3545 $query->expression($expression[0], $expression[1], $expression[2]); |
|
3546 } |
|
3547 |
|
3548 $query->expression('depth', 'depth + :depth', array(':depth' => $shift)); |
|
3549 $query->condition('menu_name', $existing_item['menu_name']); |
|
3550 $p = 'p1'; |
|
3551 for ($i = 1; $i <= MENU_MAX_DEPTH && $existing_item[$p]; $p = 'p' . ++$i) { |
|
3552 $query->condition($p, $existing_item[$p]); |
|
3553 } |
|
3554 |
|
3555 $query->execute(); |
|
3556 |
|
3557 // Check the has_children status of the parent, while excluding this item. |
|
3558 _menu_update_parental_status($existing_item, TRUE); |
|
3559 } |
|
3560 |
|
3561 /** |
|
3562 * Checks and updates the 'has_children' status for the parent of a link. |
|
3563 */ |
|
3564 function _menu_update_parental_status($item, $exclude = FALSE) { |
|
3565 // If plid == 0, there is nothing to update. |
|
3566 if ($item['plid']) { |
|
3567 // Check if at least one visible child exists in the table. |
|
3568 $query = db_select('menu_links'); |
|
3569 $query->addField('menu_links', 'mlid'); |
|
3570 $query->condition('menu_name', $item['menu_name']); |
|
3571 $query->condition('hidden', 0); |
|
3572 $query->condition('plid', $item['plid']); |
|
3573 $query->range(0, 1); |
|
3574 if ($exclude) { |
|
3575 $query->condition('mlid', $item['mlid'], '<>'); |
|
3576 } |
|
3577 $parent_has_children = ((bool) $query->execute()->fetchField()) ? 1 : 0; |
|
3578 db_update('menu_links') |
|
3579 ->fields(array('has_children' => $parent_has_children)) |
|
3580 ->condition('mlid', $item['plid']) |
|
3581 ->execute(); |
|
3582 } |
|
3583 } |
|
3584 |
|
3585 /** |
|
3586 * Sets the p1 through p9 values for a menu link being saved. |
|
3587 */ |
|
3588 function _menu_link_parents_set(&$item, $parent) { |
|
3589 $i = 1; |
|
3590 while ($i < $item['depth']) { |
|
3591 $p = 'p' . $i++; |
|
3592 $item[$p] = $parent[$p]; |
|
3593 } |
|
3594 $p = 'p' . $i++; |
|
3595 // The parent (p1 - p9) corresponding to the depth always equals the mlid. |
|
3596 $item[$p] = $item['mlid']; |
|
3597 while ($i <= MENU_MAX_DEPTH) { |
|
3598 $p = 'p' . $i++; |
|
3599 $item[$p] = 0; |
|
3600 } |
|
3601 } |
|
3602 |
|
3603 /** |
|
3604 * Builds the router table based on the data from hook_menu(). |
|
3605 */ |
|
3606 function _menu_router_build($callbacks) { |
|
3607 // First pass: separate callbacks from paths, making paths ready for |
|
3608 // matching. Calculate fitness, and fill some default values. |
|
3609 $menu = array(); |
|
3610 $masks = array(); |
|
3611 foreach ($callbacks as $path => $item) { |
|
3612 $load_functions = array(); |
|
3613 $to_arg_functions = array(); |
|
3614 $fit = 0; |
|
3615 $move = FALSE; |
|
3616 |
|
3617 $parts = explode('/', $path, MENU_MAX_PARTS); |
|
3618 $number_parts = count($parts); |
|
3619 // We store the highest index of parts here to save some work in the fit |
|
3620 // calculation loop. |
|
3621 $slashes = $number_parts - 1; |
|
3622 // Extract load and to_arg functions. |
|
3623 foreach ($parts as $k => $part) { |
|
3624 $match = FALSE; |
|
3625 // Look for wildcards in the form allowed to be used in PHP functions, |
|
3626 // because we are using these to construct the load function names. |
|
3627 if (preg_match('/^%(|' . DRUPAL_PHP_FUNCTION_PATTERN . ')$/', $part, $matches)) { |
|
3628 if (empty($matches[1])) { |
|
3629 $match = TRUE; |
|
3630 $load_functions[$k] = NULL; |
|
3631 } |
|
3632 else { |
|
3633 if (function_exists($matches[1] . '_to_arg')) { |
|
3634 $to_arg_functions[$k] = $matches[1] . '_to_arg'; |
|
3635 $load_functions[$k] = NULL; |
|
3636 $match = TRUE; |
|
3637 } |
|
3638 if (function_exists($matches[1] . '_load')) { |
|
3639 $function = $matches[1] . '_load'; |
|
3640 // Create an array of arguments that will be passed to the _load |
|
3641 // function when this menu path is checked, if 'load arguments' |
|
3642 // exists. |
|
3643 $load_functions[$k] = isset($item['load arguments']) ? array($function => $item['load arguments']) : $function; |
|
3644 $match = TRUE; |
|
3645 } |
|
3646 } |
|
3647 } |
|
3648 if ($match) { |
|
3649 $parts[$k] = '%'; |
|
3650 } |
|
3651 else { |
|
3652 $fit |= 1 << ($slashes - $k); |
|
3653 } |
|
3654 } |
|
3655 if ($fit) { |
|
3656 $move = TRUE; |
|
3657 } |
|
3658 else { |
|
3659 // If there is no %, it fits maximally. |
|
3660 $fit = (1 << $number_parts) - 1; |
|
3661 } |
|
3662 $masks[$fit] = 1; |
|
3663 $item['_load_functions'] = $load_functions; |
|
3664 $item['to_arg_functions'] = empty($to_arg_functions) ? '' : serialize($to_arg_functions); |
|
3665 $item += array( |
|
3666 'title' => '', |
|
3667 'weight' => 0, |
|
3668 'type' => MENU_NORMAL_ITEM, |
|
3669 'module' => '', |
|
3670 '_number_parts' => $number_parts, |
|
3671 '_parts' => $parts, |
|
3672 '_fit' => $fit, |
|
3673 ); |
|
3674 $item += array( |
|
3675 '_visible' => (bool) ($item['type'] & MENU_VISIBLE_IN_BREADCRUMB), |
|
3676 '_tab' => (bool) ($item['type'] & MENU_IS_LOCAL_TASK), |
|
3677 ); |
|
3678 if ($move) { |
|
3679 $new_path = implode('/', $item['_parts']); |
|
3680 $menu[$new_path] = $item; |
|
3681 $sort[$new_path] = $number_parts; |
|
3682 } |
|
3683 else { |
|
3684 $menu[$path] = $item; |
|
3685 $sort[$path] = $number_parts; |
|
3686 } |
|
3687 } |
|
3688 array_multisort($sort, SORT_NUMERIC, $menu); |
|
3689 // Apply inheritance rules. |
|
3690 foreach ($menu as $path => $v) { |
|
3691 $item = &$menu[$path]; |
|
3692 if (!$item['_tab']) { |
|
3693 // Non-tab items. |
|
3694 $item['tab_parent'] = ''; |
|
3695 $item['tab_root'] = $path; |
|
3696 } |
|
3697 // If not specified, assign the default tab type for local tasks. |
|
3698 elseif (!isset($item['context'])) { |
|
3699 $item['context'] = MENU_CONTEXT_PAGE; |
|
3700 } |
|
3701 for ($i = $item['_number_parts'] - 1; $i; $i--) { |
|
3702 $parent_path = implode('/', array_slice($item['_parts'], 0, $i)); |
|
3703 if (isset($menu[$parent_path])) { |
|
3704 |
|
3705 $parent = &$menu[$parent_path]; |
|
3706 |
|
3707 // If we have no menu name, try to inherit it from parent items. |
|
3708 if (!isset($item['menu_name'])) { |
|
3709 // If the parent item of this item does not define a menu name (and no |
|
3710 // previous iteration assigned one already), try to find the menu name |
|
3711 // of the parent item in the currently stored menu links. |
|
3712 if (!isset($parent['menu_name'])) { |
|
3713 $menu_name = db_query("SELECT menu_name FROM {menu_links} WHERE router_path = :router_path AND module = 'system'", array(':router_path' => $parent_path))->fetchField(); |
|
3714 if ($menu_name) { |
|
3715 $parent['menu_name'] = $menu_name; |
|
3716 } |
|
3717 } |
|
3718 // If the parent item defines a menu name, inherit it. |
|
3719 if (!empty($parent['menu_name'])) { |
|
3720 $item['menu_name'] = $parent['menu_name']; |
|
3721 } |
|
3722 } |
|
3723 if (!isset($item['tab_parent'])) { |
|
3724 // Parent stores the parent of the path. |
|
3725 $item['tab_parent'] = $parent_path; |
|
3726 } |
|
3727 if (!isset($item['tab_root']) && !$parent['_tab']) { |
|
3728 $item['tab_root'] = $parent_path; |
|
3729 } |
|
3730 // If an access callback is not found for a default local task we use |
|
3731 // the callback from the parent, since we expect them to be identical. |
|
3732 // In all other cases, the access parameters must be specified. |
|
3733 if (($item['type'] == MENU_DEFAULT_LOCAL_TASK) && !isset($item['access callback']) && isset($parent['access callback'])) { |
|
3734 $item['access callback'] = $parent['access callback']; |
|
3735 if (!isset($item['access arguments']) && isset($parent['access arguments'])) { |
|
3736 $item['access arguments'] = $parent['access arguments']; |
|
3737 } |
|
3738 } |
|
3739 // Same for page callbacks. |
|
3740 if (!isset($item['page callback']) && isset($parent['page callback'])) { |
|
3741 $item['page callback'] = $parent['page callback']; |
|
3742 if (!isset($item['page arguments']) && isset($parent['page arguments'])) { |
|
3743 $item['page arguments'] = $parent['page arguments']; |
|
3744 } |
|
3745 if (!isset($item['file path']) && isset($parent['file path'])) { |
|
3746 $item['file path'] = $parent['file path']; |
|
3747 } |
|
3748 if (!isset($item['file']) && isset($parent['file'])) { |
|
3749 $item['file'] = $parent['file']; |
|
3750 if (empty($item['file path']) && isset($item['module']) && isset($parent['module']) && $item['module'] != $parent['module']) { |
|
3751 $item['file path'] = drupal_get_path('module', $parent['module']); |
|
3752 } |
|
3753 } |
|
3754 } |
|
3755 // Same for delivery callbacks. |
|
3756 if (!isset($item['delivery callback']) && isset($parent['delivery callback'])) { |
|
3757 $item['delivery callback'] = $parent['delivery callback']; |
|
3758 } |
|
3759 // Same for theme callbacks. |
|
3760 if (!isset($item['theme callback']) && isset($parent['theme callback'])) { |
|
3761 $item['theme callback'] = $parent['theme callback']; |
|
3762 if (!isset($item['theme arguments']) && isset($parent['theme arguments'])) { |
|
3763 $item['theme arguments'] = $parent['theme arguments']; |
|
3764 } |
|
3765 } |
|
3766 // Same for load arguments: if a loader doesn't have any explict |
|
3767 // arguments, try to find arguments in the parent. |
|
3768 if (!isset($item['load arguments'])) { |
|
3769 foreach ($item['_load_functions'] as $k => $function) { |
|
3770 // This loader doesn't have any explict arguments... |
|
3771 if (!is_array($function)) { |
|
3772 // ... check the parent for a loader at the same position |
|
3773 // using the same function name and defining arguments... |
|
3774 if (isset($parent['_load_functions'][$k]) && is_array($parent['_load_functions'][$k]) && key($parent['_load_functions'][$k]) === $function) { |
|
3775 // ... and inherit the arguments on the child. |
|
3776 $item['_load_functions'][$k] = $parent['_load_functions'][$k]; |
|
3777 } |
|
3778 } |
|
3779 } |
|
3780 } |
|
3781 } |
|
3782 } |
|
3783 if (!isset($item['access callback']) && isset($item['access arguments'])) { |
|
3784 // Default callback. |
|
3785 $item['access callback'] = 'user_access'; |
|
3786 } |
|
3787 if (!isset($item['access callback']) || empty($item['page callback'])) { |
|
3788 $item['access callback'] = 0; |
|
3789 } |
|
3790 if (is_bool($item['access callback'])) { |
|
3791 $item['access callback'] = intval($item['access callback']); |
|
3792 } |
|
3793 |
|
3794 $item['load_functions'] = empty($item['_load_functions']) ? '' : serialize($item['_load_functions']); |
|
3795 $item += array( |
|
3796 'access arguments' => array(), |
|
3797 'access callback' => '', |
|
3798 'page arguments' => array(), |
|
3799 'page callback' => '', |
|
3800 'delivery callback' => '', |
|
3801 'title arguments' => array(), |
|
3802 'title callback' => 't', |
|
3803 'theme arguments' => array(), |
|
3804 'theme callback' => '', |
|
3805 'description' => '', |
|
3806 'position' => '', |
|
3807 'context' => 0, |
|
3808 'tab_parent' => '', |
|
3809 'tab_root' => $path, |
|
3810 'path' => $path, |
|
3811 'file' => '', |
|
3812 'file path' => '', |
|
3813 'include file' => '', |
|
3814 ); |
|
3815 |
|
3816 // Calculate out the file to be included for each callback, if any. |
|
3817 if ($item['file']) { |
|
3818 $file_path = $item['file path'] ? $item['file path'] : drupal_get_path('module', $item['module']); |
|
3819 $item['include file'] = $file_path . '/' . $item['file']; |
|
3820 } |
|
3821 } |
|
3822 |
|
3823 // Sort the masks so they are in order of descending fit. |
|
3824 $masks = array_keys($masks); |
|
3825 rsort($masks); |
|
3826 |
|
3827 return array($menu, $masks); |
|
3828 } |
|
3829 |
|
3830 /** |
|
3831 * Saves data from menu_router_build() to the router table. |
|
3832 */ |
|
3833 function _menu_router_save($menu, $masks) { |
|
3834 // Delete the existing router since we have some data to replace it. |
|
3835 db_truncate('menu_router')->execute(); |
|
3836 |
|
3837 // Prepare insert object. |
|
3838 $insert = db_insert('menu_router') |
|
3839 ->fields(array( |
|
3840 'path', |
|
3841 'load_functions', |
|
3842 'to_arg_functions', |
|
3843 'access_callback', |
|
3844 'access_arguments', |
|
3845 'page_callback', |
|
3846 'page_arguments', |
|
3847 'delivery_callback', |
|
3848 'fit', |
|
3849 'number_parts', |
|
3850 'context', |
|
3851 'tab_parent', |
|
3852 'tab_root', |
|
3853 'title', |
|
3854 'title_callback', |
|
3855 'title_arguments', |
|
3856 'theme_callback', |
|
3857 'theme_arguments', |
|
3858 'type', |
|
3859 'description', |
|
3860 'position', |
|
3861 'weight', |
|
3862 'include_file', |
|
3863 )); |
|
3864 |
|
3865 $num_records = 0; |
|
3866 |
|
3867 foreach ($menu as $path => $item) { |
|
3868 // Fill in insert object values. |
|
3869 $insert->values(array( |
|
3870 'path' => $item['path'], |
|
3871 'load_functions' => $item['load_functions'], |
|
3872 'to_arg_functions' => $item['to_arg_functions'], |
|
3873 'access_callback' => $item['access callback'], |
|
3874 'access_arguments' => serialize($item['access arguments']), |
|
3875 'page_callback' => $item['page callback'], |
|
3876 'page_arguments' => serialize($item['page arguments']), |
|
3877 'delivery_callback' => $item['delivery callback'], |
|
3878 'fit' => $item['_fit'], |
|
3879 'number_parts' => $item['_number_parts'], |
|
3880 'context' => $item['context'], |
|
3881 'tab_parent' => $item['tab_parent'], |
|
3882 'tab_root' => $item['tab_root'], |
|
3883 'title' => $item['title'], |
|
3884 'title_callback' => $item['title callback'], |
|
3885 'title_arguments' => ($item['title arguments'] ? serialize($item['title arguments']) : ''), |
|
3886 'theme_callback' => $item['theme callback'], |
|
3887 'theme_arguments' => serialize($item['theme arguments']), |
|
3888 'type' => $item['type'], |
|
3889 'description' => $item['description'], |
|
3890 'position' => $item['position'], |
|
3891 'weight' => $item['weight'], |
|
3892 'include_file' => $item['include file'], |
|
3893 )); |
|
3894 |
|
3895 // Execute in batches to avoid the memory overhead of all of those records |
|
3896 // in the query object. |
|
3897 if (++$num_records == 20) { |
|
3898 $insert->execute(); |
|
3899 $num_records = 0; |
|
3900 } |
|
3901 } |
|
3902 // Insert any remaining records. |
|
3903 $insert->execute(); |
|
3904 // Store the masks. |
|
3905 variable_set('menu_masks', $masks); |
|
3906 |
|
3907 return $menu; |
|
3908 } |
|
3909 |
|
3910 /** |
|
3911 * Checks whether the site is in maintenance mode. |
|
3912 * |
|
3913 * This function will log the current user out and redirect to front page |
|
3914 * if the current user has no 'access site in maintenance mode' permission. |
|
3915 * |
|
3916 * @param $check_only |
|
3917 * If this is set to TRUE, the function will perform the access checks and |
|
3918 * return the site offline status, but not log the user out or display any |
|
3919 * messages. |
|
3920 * |
|
3921 * @return |
|
3922 * FALSE if the site is not in maintenance mode, the user login page is |
|
3923 * displayed, or the user has the 'access site in maintenance mode' |
|
3924 * permission. TRUE for anonymous users not being on the login page when the |
|
3925 * site is in maintenance mode. |
|
3926 */ |
|
3927 function _menu_site_is_offline($check_only = FALSE) { |
|
3928 // Check if site is in maintenance mode. |
|
3929 if (variable_get('maintenance_mode', 0)) { |
|
3930 if (user_access('access site in maintenance mode')) { |
|
3931 // Ensure that the maintenance mode message is displayed only once |
|
3932 // (allowing for page redirects) and specifically suppress its display on |
|
3933 // the maintenance mode settings page. |
|
3934 if (!$check_only && $_GET['q'] != 'admin/config/development/maintenance') { |
|
3935 if (user_access('administer site configuration')) { |
|
3936 drupal_set_message(t('Operating in maintenance mode. <a href="@url">Go online.</a>', array('@url' => url('admin/config/development/maintenance'))), 'status', FALSE); |
|
3937 } |
|
3938 else { |
|
3939 drupal_set_message(t('Operating in maintenance mode.'), 'status', FALSE); |
|
3940 } |
|
3941 } |
|
3942 } |
|
3943 else { |
|
3944 return TRUE; |
|
3945 } |
|
3946 } |
|
3947 return FALSE; |
|
3948 } |
|
3949 |
|
3950 /** |
|
3951 * @} End of "defgroup menu". |
|
3952 */ |