Mercurial > defr > drupal > core
comparison includes/menu.inc @ 1:c1f4ac30525a 6.0
Drupal 6.0
author | Franck Deroche <webmaster@defr.org> |
---|---|
date | Tue, 23 Dec 2008 14:28:28 +0100 |
parents | |
children | 165d43f946a8 |
comparison
equal
deleted
inserted
replaced
0:5a113a1c4740 | 1:c1f4ac30525a |
---|---|
1 <?php | |
2 // $Id: menu.inc,v 1.255.2.7 2008/02/12 22:33:51 goba Exp $ | |
3 | |
4 /** | |
5 * @file | |
6 * API for the Drupal menu system. | |
7 */ | |
8 | |
9 /** | |
10 * @defgroup menu Menu system | |
11 * @{ | |
12 * Define the navigation menus, and route page requests to code based on URLs. | |
13 * | |
14 * The Drupal menu system drives both the navigation system from a user | |
15 * perspective and the callback system that Drupal uses to respond to URLs | |
16 * passed from the browser. For this reason, a good understanding of the | |
17 * menu system is fundamental to the creation of complex modules. | |
18 * | |
19 * Drupal's menu system follows a simple hierarchy defined by paths. | |
20 * Implementations of hook_menu() define menu items and assign them to | |
21 * paths (which should be unique). The menu system aggregates these items | |
22 * and determines the menu hierarchy from the paths. For example, if the | |
23 * paths defined were a, a/b, e, a/b/c/d, f/g, and a/b/h, the menu system | |
24 * would form the structure: | |
25 * - a | |
26 * - a/b | |
27 * - a/b/c/d | |
28 * - a/b/h | |
29 * - e | |
30 * - f/g | |
31 * Note that the number of elements in the path does not necessarily | |
32 * determine the depth of the menu item in the tree. | |
33 * | |
34 * When responding to a page request, the menu system looks to see if the | |
35 * path requested by the browser is registered as a menu item with a | |
36 * callback. If not, the system searches up the menu tree for the most | |
37 * complete match with a callback it can find. If the path a/b/i is | |
38 * requested in the tree above, the callback for a/b would be used. | |
39 * | |
40 * The found callback function is called with any arguments specified | |
41 * in the "page arguments" attribute of its menu item. The | |
42 * attribute must be an array. After these arguments, any remaining | |
43 * components of the path are appended as further arguments. In this | |
44 * way, the callback for a/b above could respond to a request for | |
45 * a/b/i differently than a request for a/b/j. | |
46 * | |
47 * For an illustration of this process, see page_example.module. | |
48 * | |
49 * Access to the callback functions is also protected by the menu system. | |
50 * The "access callback" with an optional "access arguments" of each menu | |
51 * item is called before the page callback proceeds. If this returns TRUE, | |
52 * then access is granted; if FALSE, then access is denied. Menu items may | |
53 * omit this attribute to use the value provided by an ancestor item. | |
54 * | |
55 * In the default Drupal interface, you will notice many links rendered as | |
56 * tabs. These are known in the menu system as "local tasks", and they are | |
57 * rendered as tabs by default, though other presentations are possible. | |
58 * Local tasks function just as other menu items in most respects. It is | |
59 * convention that the names of these tasks should be short verbs if | |
60 * possible. In addition, a "default" local task should be provided for | |
61 * each set. When visiting a local task's parent menu item, the default | |
62 * local task will be rendered as if it is selected; this provides for a | |
63 * normal tab user experience. This default task is special in that it | |
64 * links not to its provided path, but to its parent item's path instead. | |
65 * The default task's path is only used to place it appropriately in the | |
66 * menu hierarchy. | |
67 * | |
68 * Everything described so far is stored in the menu_router table. The | |
69 * menu_links table holds the visible menu links. By default these are | |
70 * derived from the same hook_menu definitions, however you are free to | |
71 * add more with menu_link_save(). | |
72 */ | |
73 | |
74 /** | |
75 * @name Menu flags | |
76 * @{ | |
77 * Flags for use in the "type" attribute of menu items. | |
78 */ | |
79 | |
80 define('MENU_IS_ROOT', 0x0001); | |
81 define('MENU_VISIBLE_IN_TREE', 0x0002); | |
82 define('MENU_VISIBLE_IN_BREADCRUMB', 0x0004); | |
83 define('MENU_LINKS_TO_PARENT', 0x0008); | |
84 define('MENU_MODIFIED_BY_ADMIN', 0x0020); | |
85 define('MENU_CREATED_BY_ADMIN', 0x0040); | |
86 define('MENU_IS_LOCAL_TASK', 0x0080); | |
87 | |
88 /** | |
89 * @} End of "Menu flags". | |
90 */ | |
91 | |
92 /** | |
93 * @name Menu item types | |
94 * @{ | |
95 * Menu item definitions provide one of these constants, which are shortcuts for | |
96 * combinations of the above flags. | |
97 */ | |
98 | |
99 /** | |
100 * Normal menu items show up in the menu tree and can be moved/hidden by | |
101 * the administrator. Use this for most menu items. It is the default value if | |
102 * no menu item type is specified. | |
103 */ | |
104 define('MENU_NORMAL_ITEM', MENU_VISIBLE_IN_TREE | MENU_VISIBLE_IN_BREADCRUMB); | |
105 | |
106 /** | |
107 * Callbacks simply register a path so that the correct function is fired | |
108 * when the URL is accessed. They are not shown in the menu. | |
109 */ | |
110 define('MENU_CALLBACK', MENU_VISIBLE_IN_BREADCRUMB); | |
111 | |
112 /** | |
113 * Modules may "suggest" menu items that the administrator may enable. They act | |
114 * just as callbacks do until enabled, at which time they act like normal items. | |
115 * Note for the value: 0x0010 was a flag which is no longer used, but this way | |
116 * the values of MENU_CALLBACK and MENU_SUGGESTED_ITEM are separate. | |
117 */ | |
118 define('MENU_SUGGESTED_ITEM', MENU_VISIBLE_IN_BREADCRUMB | 0x0010); | |
119 | |
120 /** | |
121 * Local tasks are rendered as tabs by default. Use this for menu items that | |
122 * describe actions to be performed on their parent item. An example is the path | |
123 * "node/52/edit", which performs the "edit" task on "node/52". | |
124 */ | |
125 define('MENU_LOCAL_TASK', MENU_IS_LOCAL_TASK); | |
126 | |
127 /** | |
128 * Every set of local tasks should provide one "default" task, that links to the | |
129 * same path as its parent when clicked. | |
130 */ | |
131 define('MENU_DEFAULT_LOCAL_TASK', MENU_IS_LOCAL_TASK | MENU_LINKS_TO_PARENT); | |
132 | |
133 /** | |
134 * @} End of "Menu item types". | |
135 */ | |
136 | |
137 /** | |
138 * @name Menu status codes | |
139 * @{ | |
140 * Status codes for menu callbacks. | |
141 */ | |
142 | |
143 define('MENU_FOUND', 1); | |
144 define('MENU_NOT_FOUND', 2); | |
145 define('MENU_ACCESS_DENIED', 3); | |
146 define('MENU_SITE_OFFLINE', 4); | |
147 | |
148 /** | |
149 * @} End of "Menu status codes". | |
150 */ | |
151 | |
152 /** | |
153 * @Name Menu tree parameters | |
154 * @{ | |
155 * Menu tree | |
156 */ | |
157 | |
158 /** | |
159 * The maximum number of path elements for a menu callback | |
160 */ | |
161 define('MENU_MAX_PARTS', 7); | |
162 | |
163 | |
164 /** | |
165 * The maximum depth of a menu links tree - matches the number of p columns. | |
166 */ | |
167 define('MENU_MAX_DEPTH', 9); | |
168 | |
169 | |
170 /** | |
171 * @} End of "Menu tree parameters". | |
172 */ | |
173 | |
174 /** | |
175 * Returns the ancestors (and relevant placeholders) for any given path. | |
176 * | |
177 * For example, the ancestors of node/12345/edit are: | |
178 * | |
179 * node/12345/edit | |
180 * node/12345/% | |
181 * node/%/edit | |
182 * node/%/% | |
183 * node/12345 | |
184 * node/% | |
185 * node | |
186 * | |
187 * To generate these, we will use binary numbers. Each bit represents a | |
188 * part of the path. If the bit is 1, then it represents the original | |
189 * value while 0 means wildcard. If the path is node/12/edit/foo | |
190 * then the 1011 bitstring represents node/%/edit/foo where % means that | |
191 * any argument matches that part. We limit ourselves to using binary | |
192 * numbers that correspond the patterns of wildcards of router items that | |
193 * actually exists. This list of 'masks' is built in menu_rebuild(). | |
194 * | |
195 * @param $parts | |
196 * An array of path parts, for the above example | |
197 * array('node', '12345', 'edit'). | |
198 * @return | |
199 * An array which contains the ancestors and placeholders. Placeholders | |
200 * simply contain as many '%s' as the ancestors. | |
201 */ | |
202 function menu_get_ancestors($parts) { | |
203 $number_parts = count($parts); | |
204 $placeholders = array(); | |
205 $ancestors = array(); | |
206 $length = $number_parts - 1; | |
207 $end = (1 << $number_parts) - 1; | |
208 $masks = variable_get('menu_masks', array()); | |
209 // Only examine patterns that actually exist as router items (the masks). | |
210 foreach ($masks as $i) { | |
211 if ($i > $end) { | |
212 // Only look at masks that are not longer than the path of interest. | |
213 continue; | |
214 } | |
215 elseif ($i < (1 << $length)) { | |
216 // We have exhausted the masks of a given length, so decrease the length. | |
217 --$length; | |
218 } | |
219 $current = ''; | |
220 for ($j = $length; $j >= 0; $j--) { | |
221 if ($i & (1 << $j)) { | |
222 $current .= $parts[$length - $j]; | |
223 } | |
224 else { | |
225 $current .= '%'; | |
226 } | |
227 if ($j) { | |
228 $current .= '/'; | |
229 } | |
230 } | |
231 $placeholders[] = "'%s'"; | |
232 $ancestors[] = $current; | |
233 } | |
234 return array($ancestors, $placeholders); | |
235 } | |
236 | |
237 /** | |
238 * The menu system uses serialized arrays stored in the database for | |
239 * arguments. However, often these need to change according to the | |
240 * current path. This function unserializes such an array and does the | |
241 * necessary change. | |
242 * | |
243 * Integer values are mapped according to the $map parameter. For | |
244 * example, if unserialize($data) is array('view', 1) and $map is | |
245 * array('node', '12345') then 'view' will not be changed because | |
246 * it is not an integer, but 1 will as it is an integer. As $map[1] | |
247 * is '12345', 1 will be replaced with '12345'. So the result will | |
248 * be array('node_load', '12345'). | |
249 * | |
250 * @param @data | |
251 * A serialized array. | |
252 * @param @map | |
253 * An array of potential replacements. | |
254 * @return | |
255 * The $data array unserialized and mapped. | |
256 */ | |
257 function menu_unserialize($data, $map) { | |
258 if ($data = unserialize($data)) { | |
259 foreach ($data as $k => $v) { | |
260 if (is_int($v)) { | |
261 $data[$k] = isset($map[$v]) ? $map[$v] : ''; | |
262 } | |
263 } | |
264 return $data; | |
265 } | |
266 else { | |
267 return array(); | |
268 } | |
269 } | |
270 | |
271 | |
272 | |
273 /** | |
274 * Replaces the statically cached item for a given path. | |
275 * | |
276 * @param $path | |
277 * The path. | |
278 * @param $router_item | |
279 * The router item. Usually you take a router entry from menu_get_item and | |
280 * set it back either modified or to a different path. This lets you modify the | |
281 * navigation block, the page title, the breadcrumb and the page help in one | |
282 * call. | |
283 */ | |
284 function menu_set_item($path, $router_item) { | |
285 menu_get_item($path, $router_item); | |
286 } | |
287 | |
288 /** | |
289 * Get a router item. | |
290 * | |
291 * @param $path | |
292 * The path, for example node/5. The function will find the corresponding | |
293 * node/% item and return that. | |
294 * @param $router_item | |
295 * Internal use only. | |
296 * @return | |
297 * The router item, an associate array corresponding to one row in the | |
298 * menu_router table. The value of key map holds the loaded objects. The | |
299 * value of key access is TRUE if the current user can access this page. | |
300 * The values for key title, page_arguments, access_arguments will be | |
301 * filled in based on the database values and the objects loaded. | |
302 */ | |
303 function menu_get_item($path = NULL, $router_item = NULL) { | |
304 static $router_items; | |
305 if (!isset($path)) { | |
306 $path = $_GET['q']; | |
307 } | |
308 if (isset($router_item)) { | |
309 $router_items[$path] = $router_item; | |
310 } | |
311 if (!isset($router_items[$path])) { | |
312 $original_map = arg(NULL, $path); | |
313 $parts = array_slice($original_map, 0, MENU_MAX_PARTS); | |
314 list($ancestors, $placeholders) = menu_get_ancestors($parts); | |
315 | |
316 if ($router_item = db_fetch_array(db_query_range('SELECT * FROM {menu_router} WHERE path IN ('. implode (',', $placeholders) .') ORDER BY fit DESC', $ancestors, 0, 1))) { | |
317 $map = _menu_translate($router_item, $original_map); | |
318 if ($map === FALSE) { | |
319 $router_items[$path] = FALSE; | |
320 return FALSE; | |
321 } | |
322 if ($router_item['access']) { | |
323 $router_item['map'] = $map; | |
324 $router_item['page_arguments'] = array_merge(menu_unserialize($router_item['page_arguments'], $map), array_slice($map, $router_item['number_parts'])); | |
325 } | |
326 } | |
327 $router_items[$path] = $router_item; | |
328 } | |
329 return $router_items[$path]; | |
330 } | |
331 | |
332 /** | |
333 * Execute the page callback associated with the current path | |
334 */ | |
335 function menu_execute_active_handler($path = NULL) { | |
336 if (_menu_site_is_offline()) { | |
337 return MENU_SITE_OFFLINE; | |
338 } | |
339 if (variable_get('menu_rebuild_needed', FALSE)) { | |
340 menu_rebuild(); | |
341 } | |
342 if ($router_item = menu_get_item($path)) { | |
343 if ($router_item['access']) { | |
344 if ($router_item['file']) { | |
345 require_once($router_item['file']); | |
346 } | |
347 return call_user_func_array($router_item['page_callback'], $router_item['page_arguments']); | |
348 } | |
349 else { | |
350 return MENU_ACCESS_DENIED; | |
351 } | |
352 } | |
353 return MENU_NOT_FOUND; | |
354 } | |
355 | |
356 /** | |
357 * Loads objects into the map as defined in the $item['load_functions']. | |
358 * | |
359 * @param $item | |
360 * A menu router or menu link item | |
361 * @param $map | |
362 * An array of path arguments (ex: array('node', '5')) | |
363 * @return | |
364 * Returns TRUE for success, FALSE if an object cannot be loaded. | |
365 * Names of object loading functions are placed in $item['load_functions']. | |
366 * Loaded objects are placed in $map[]; keys are the same as keys in the | |
367 * $item['load_functions'] array. | |
368 * $item['access'] is set to FALSE if an object cannot be loaded. | |
369 */ | |
370 function _menu_load_objects(&$item, &$map) { | |
371 if ($load_functions = $item['load_functions']) { | |
372 // If someone calls this function twice, then unserialize will fail. | |
373 if ($load_functions_unserialized = unserialize($load_functions)) { | |
374 $load_functions = $load_functions_unserialized; | |
375 } | |
376 $path_map = $map; | |
377 foreach ($load_functions as $index => $function) { | |
378 if ($function) { | |
379 $value = isset($path_map[$index]) ? $path_map[$index] : ''; | |
380 if (is_array($function)) { | |
381 // Set up arguments for the load function. These were pulled from | |
382 // 'load arguments' in the hook_menu() entry, but they need | |
383 // some processing. In this case the $function is the key to the | |
384 // load_function array, and the value is the list of arguments. | |
385 list($function, $args) = each($function); | |
386 $load_functions[$index] = $function; | |
387 | |
388 // Some arguments are placeholders for dynamic items to process. | |
389 foreach ($args as $i => $arg) { | |
390 if ($arg === '%index') { | |
391 // Pass on argument index to the load function, so multiple | |
392 // occurances of the same placeholder can be identified. | |
393 $args[$i] = $index; | |
394 } | |
395 if ($arg === '%map') { | |
396 // Pass on menu map by reference. The accepting function must | |
397 // also declare this as a reference if it wants to modify | |
398 // the map. | |
399 $args[$i] = &$map; | |
400 } | |
401 if (is_int($arg)) { | |
402 $args[$i] = isset($path_map[$arg]) ? $path_map[$arg] : ''; | |
403 } | |
404 } | |
405 array_unshift($args, $value); | |
406 $return = call_user_func_array($function, $args); | |
407 } | |
408 else { | |
409 $return = $function($value); | |
410 } | |
411 // If callback returned an error or there is no callback, trigger 404. | |
412 if ($return === FALSE) { | |
413 $item['access'] = FALSE; | |
414 $map = FALSE; | |
415 return FALSE; | |
416 } | |
417 $map[$index] = $return; | |
418 } | |
419 } | |
420 $item['load_functions'] = $load_functions; | |
421 } | |
422 return TRUE; | |
423 } | |
424 | |
425 /** | |
426 * Check access to a menu item using the access callback | |
427 * | |
428 * @param $item | |
429 * A menu router or menu link item | |
430 * @param $map | |
431 * An array of path arguments (ex: array('node', '5')) | |
432 * @return | |
433 * $item['access'] becomes TRUE if the item is accessible, FALSE otherwise. | |
434 */ | |
435 function _menu_check_access(&$item, $map) { | |
436 // Determine access callback, which will decide whether or not the current | |
437 // user has access to this path. | |
438 $callback = empty($item['access_callback']) ? 0 : trim($item['access_callback']); | |
439 // Check for a TRUE or FALSE value. | |
440 if (is_numeric($callback)) { | |
441 $item['access'] = (bool)$callback; | |
442 } | |
443 else { | |
444 $arguments = menu_unserialize($item['access_arguments'], $map); | |
445 // As call_user_func_array is quite slow and user_access is a very common | |
446 // callback, it is worth making a special case for it. | |
447 if ($callback == 'user_access') { | |
448 $item['access'] = (count($arguments) == 1) ? user_access($arguments[0]) : user_access($arguments[0], $arguments[1]); | |
449 } | |
450 else { | |
451 $item['access'] = call_user_func_array($callback, $arguments); | |
452 } | |
453 } | |
454 } | |
455 | |
456 /** | |
457 * Localize the router item title using t() or another callback. | |
458 * | |
459 * Translate the title and description to allow storage of English title | |
460 * strings in the database, yet display of them in the language required | |
461 * by the current user. | |
462 * | |
463 * @param $item | |
464 * A menu router item or a menu link item. | |
465 * @param $map | |
466 * The path as an array with objects already replaced. E.g., for path | |
467 * node/123 $map would be array('node', $node) where $node is the node | |
468 * object for node 123. | |
469 * @param $link_translate | |
470 * TRUE if we are translating a menu link item; FALSE if we are | |
471 * translating a menu router item. | |
472 * @return | |
473 * No return value. | |
474 * $item['title'] is localized according to $item['title_callback']. | |
475 * If an item's callback is check_plain(), $item['options']['html'] becomes | |
476 * TRUE. | |
477 * $item['description'] is translated using t(). | |
478 * When doing link translation and the $item['options']['attributes']['title'] | |
479 * (link title attribute) matches the description, it is translated as well. | |
480 */ | |
481 function _menu_item_localize(&$item, $map, $link_translate = FALSE) { | |
482 $callback = $item['title_callback']; | |
483 $item['localized_options'] = $item['options']; | |
484 // If we are not doing link translation or if the title matches the | |
485 // link title of its router item, localize it. | |
486 if (!$link_translate || (!empty($item['title']) && ($item['title'] == $item['link_title']))) { | |
487 // t() is a special case. Since it is used very close to all the time, | |
488 // we handle it directly instead of using indirect, slower methods. | |
489 if ($callback == 't') { | |
490 if (empty($item['title_arguments'])) { | |
491 $item['title'] = t($item['title']); | |
492 } | |
493 else { | |
494 $item['title'] = t($item['title'], menu_unserialize($item['title_arguments'], $map)); | |
495 } | |
496 } | |
497 elseif ($callback) { | |
498 if (empty($item['title_arguments'])) { | |
499 $item['title'] = $callback($item['title']); | |
500 } | |
501 else { | |
502 $item['title'] = call_user_func_array($callback, menu_unserialize($item['title_arguments'], $map)); | |
503 } | |
504 // Avoid calling check_plain again on l() function. | |
505 if ($callback == 'check_plain') { | |
506 $item['localized_options']['html'] = TRUE; | |
507 } | |
508 } | |
509 } | |
510 elseif ($link_translate) { | |
511 $item['title'] = $item['link_title']; | |
512 } | |
513 | |
514 // Translate description, see the motivation above. | |
515 if (!empty($item['description'])) { | |
516 $original_description = $item['description']; | |
517 $item['description'] = t($item['description']); | |
518 if ($link_translate && $item['options']['attributes']['title'] == $original_description) { | |
519 $item['localized_options']['attributes']['title'] = $item['description']; | |
520 } | |
521 } | |
522 } | |
523 | |
524 /** | |
525 * Handles dynamic path translation and menu access control. | |
526 * | |
527 * When a user arrives on a page such as node/5, this function determines | |
528 * what "5" corresponds to, by inspecting the page's menu path definition, | |
529 * node/%node. This will call node_load(5) to load the corresponding node | |
530 * object. | |
531 * | |
532 * It also works in reverse, to allow the display of tabs and menu items which | |
533 * contain these dynamic arguments, translating node/%node to node/5. | |
534 * | |
535 * Translation of menu item titles and descriptions are done here to | |
536 * allow for storage of English strings in the database, and translation | |
537 * to the language required to generate the current page | |
538 * | |
539 * @param $router_item | |
540 * A menu router item | |
541 * @param $map | |
542 * An array of path arguments (ex: array('node', '5')) | |
543 * @param $to_arg | |
544 * Execute $item['to_arg_functions'] or not. Use only if you want to render a | |
545 * path from the menu table, for example tabs. | |
546 * @return | |
547 * Returns the map with objects loaded as defined in the | |
548 * $item['load_functions. $item['access'] becomes TRUE if the item is | |
549 * accessible, FALSE otherwise. $item['href'] is set according to the map. | |
550 * If an error occurs during calling the load_functions (like trying to load | |
551 * a non existing node) then this function return FALSE. | |
552 */ | |
553 function _menu_translate(&$router_item, $map, $to_arg = FALSE) { | |
554 $path_map = $map; | |
555 if (!_menu_load_objects($router_item, $map)) { | |
556 // An error occurred loading an object. | |
557 $router_item['access'] = FALSE; | |
558 return FALSE; | |
559 } | |
560 if ($to_arg) { | |
561 _menu_link_map_translate($path_map, $router_item['to_arg_functions']); | |
562 } | |
563 | |
564 // Generate the link path for the page request or local tasks. | |
565 $link_map = explode('/', $router_item['path']); | |
566 for ($i = 0; $i < $router_item['number_parts']; $i++) { | |
567 if ($link_map[$i] == '%') { | |
568 $link_map[$i] = $path_map[$i]; | |
569 } | |
570 } | |
571 $router_item['href'] = implode('/', $link_map); | |
572 $router_item['options'] = array(); | |
573 _menu_check_access($router_item, $map); | |
574 | |
575 _menu_item_localize($router_item, $map); | |
576 | |
577 return $map; | |
578 } | |
579 | |
580 /** | |
581 * This function translates the path elements in the map using any to_arg | |
582 * helper function. These functions take an argument and return an object. | |
583 * See http://drupal.org/node/109153 for more information. | |
584 * | |
585 * @param map | |
586 * An array of path arguments (ex: array('node', '5')) | |
587 * @param $to_arg_functions | |
588 * An array of helper function (ex: array(2 => 'menu_tail_to_arg')) | |
589 */ | |
590 function _menu_link_map_translate(&$map, $to_arg_functions) { | |
591 if ($to_arg_functions) { | |
592 $to_arg_functions = unserialize($to_arg_functions); | |
593 foreach ($to_arg_functions as $index => $function) { | |
594 // Translate place-holders into real values. | |
595 $arg = $function(!empty($map[$index]) ? $map[$index] : '', $map, $index); | |
596 if (!empty($map[$index]) || isset($arg)) { | |
597 $map[$index] = $arg; | |
598 } | |
599 else { | |
600 unset($map[$index]); | |
601 } | |
602 } | |
603 } | |
604 } | |
605 | |
606 function menu_tail_to_arg($arg, $map, $index) { | |
607 return implode('/', array_slice($map, $index)); | |
608 } | |
609 | |
610 /** | |
611 * This function is similar to _menu_translate() but does link-specific | |
612 * preparation such as always calling to_arg functions | |
613 * | |
614 * @param $item | |
615 * A menu link | |
616 * @return | |
617 * Returns the map of path arguments with objects loaded as defined in the | |
618 * $item['load_functions']. | |
619 * $item['access'] becomes TRUE if the item is accessible, FALSE otherwise. | |
620 * $item['href'] is generated from link_path, possibly by to_arg functions. | |
621 * $item['title'] is generated from link_title, and may be localized. | |
622 * $item['options'] is unserialized; it is also changed within the call here | |
623 * to $item['localized_options'] by _menu_item_localize(). | |
624 */ | |
625 function _menu_link_translate(&$item) { | |
626 $item['options'] = unserialize($item['options']); | |
627 if ($item['external']) { | |
628 $item['access'] = 1; | |
629 $map = array(); | |
630 $item['href'] = $item['link_path']; | |
631 $item['title'] = $item['link_title']; | |
632 $item['localized_options'] = $item['options']; | |
633 } | |
634 else { | |
635 $map = explode('/', $item['link_path']); | |
636 _menu_link_map_translate($map, $item['to_arg_functions']); | |
637 $item['href'] = implode('/', $map); | |
638 | |
639 // Note - skip callbacks without real values for their arguments. | |
640 if (strpos($item['href'], '%') !== FALSE) { | |
641 $item['access'] = FALSE; | |
642 return FALSE; | |
643 } | |
644 // menu_tree_check_access() may set this ahead of time for links to nodes. | |
645 if (!isset($item['access'])) { | |
646 if (!_menu_load_objects($item, $map)) { | |
647 // An error occurred loading an object. | |
648 $item['access'] = FALSE; | |
649 return FALSE; | |
650 } | |
651 _menu_check_access($item, $map); | |
652 } | |
653 | |
654 _menu_item_localize($item, $map, TRUE); | |
655 } | |
656 | |
657 // Allow other customizations - e.g. adding a page-specific query string to the | |
658 // options array. For performance reasons we only invoke this hook if the link | |
659 // has the 'alter' flag set in the options array. | |
660 if (!empty($item['options']['alter'])) { | |
661 drupal_alter('translated_menu_link', $item, $map); | |
662 } | |
663 | |
664 return $map; | |
665 } | |
666 | |
667 /** | |
668 * Get a loaded object from a router item. | |
669 * | |
670 * menu_get_object() will provide you the current node on paths like node/5, | |
671 * node/5/revisions/48 etc. menu_get_object('user') will give you the user | |
672 * account on user/5 etc. Note - this function should never be called within a | |
673 * _to_arg function (like user_current_to_arg()) since this may result in an | |
674 * infinite recursion. | |
675 * | |
676 * @param $type | |
677 * Type of the object. These appear in hook_menu definitons as %type. Core | |
678 * provides aggregator_feed, aggregator_category, contact, filter_format, | |
679 * forum_term, menu, menu_link, node, taxonomy_vocabulary, user. See the | |
680 * relevant {$type}_load function for more on each. Defaults to node. | |
681 * @param $position | |
682 * The expected position for $type object. For node/%node this is 1, for | |
683 * comment/reply/%node this is 2. Defaults to 1. | |
684 * @param $path | |
685 * See @menu_get_item for more on this. Defaults to the current path. | |
686 */ | |
687 function menu_get_object($type = 'node', $position = 1, $path = NULL) { | |
688 $router_item = menu_get_item($path); | |
689 if (isset($router_item['load_functions'][$position]) && !empty($router_item['map'][$position]) && $router_item['load_functions'][$position] == $type .'_load') { | |
690 return $router_item['map'][$position]; | |
691 } | |
692 } | |
693 | |
694 /** | |
695 * Render a menu tree based on the current path. | |
696 * | |
697 * The tree is expanded based on the current path and dynamic paths are also | |
698 * changed according to the defined to_arg functions (for example the 'My account' | |
699 * link is changed from user/% to a link with the current user's uid). | |
700 * | |
701 * @param $menu_name | |
702 * The name of the menu. | |
703 * @return | |
704 * The rendered HTML of that menu on the current page. | |
705 */ | |
706 function menu_tree($menu_name = 'navigation') { | |
707 static $menu_output = array(); | |
708 | |
709 if (!isset($menu_output[$menu_name])) { | |
710 $tree = menu_tree_page_data($menu_name); | |
711 $menu_output[$menu_name] = menu_tree_output($tree); | |
712 } | |
713 return $menu_output[$menu_name]; | |
714 } | |
715 | |
716 /** | |
717 * Returns a rendered menu tree. | |
718 * | |
719 * @param $tree | |
720 * A data structure representing the tree as returned from menu_tree_data. | |
721 * @return | |
722 * The rendered HTML of that data structure. | |
723 */ | |
724 function menu_tree_output($tree) { | |
725 $output = ''; | |
726 $items = array(); | |
727 | |
728 // Pull out just the menu items we are going to render so that we | |
729 // get an accurate count for the first/last classes. | |
730 foreach ($tree as $data) { | |
731 if (!$data['link']['hidden']) { | |
732 $items[] = $data; | |
733 } | |
734 } | |
735 | |
736 $num_items = count($items); | |
737 foreach ($items as $i => $data) { | |
738 $extra_class = NULL; | |
739 if ($i == 0) { | |
740 $extra_class = 'first'; | |
741 } | |
742 if ($i == $num_items - 1) { | |
743 $extra_class = 'last'; | |
744 } | |
745 $link = theme('menu_item_link', $data['link']); | |
746 if ($data['below']) { | |
747 $output .= theme('menu_item', $link, $data['link']['has_children'], menu_tree_output($data['below']), $data['link']['in_active_trail'], $extra_class); | |
748 } | |
749 else { | |
750 $output .= theme('menu_item', $link, $data['link']['has_children'], '', $data['link']['in_active_trail'], $extra_class); | |
751 } | |
752 } | |
753 return $output ? theme('menu_tree', $output) : ''; | |
754 } | |
755 | |
756 /** | |
757 * Get the data structure representing a named menu tree. | |
758 * | |
759 * Since this can be the full tree including hidden items, the data returned | |
760 * may be used for generating an an admin interface or a select. | |
761 * | |
762 * @param $menu_name | |
763 * The named menu links to return | |
764 * @param $item | |
765 * A fully loaded menu link, or NULL. If a link is supplied, only the | |
766 * path to root will be included in the returned tree- as if this link | |
767 * represented the current page in a visible menu. | |
768 * @return | |
769 * An tree of menu links in an array, in the order they should be rendered. | |
770 */ | |
771 function menu_tree_all_data($menu_name = 'navigation', $item = NULL) { | |
772 static $tree = array(); | |
773 | |
774 // Use $mlid as a flag for whether the data being loaded is for the whole tree. | |
775 $mlid = isset($item['mlid']) ? $item['mlid'] : 0; | |
776 // Generate the cache ID. | |
777 $cid = 'links:'. $menu_name .':all:'. $mlid; | |
778 | |
779 if (!isset($tree[$cid])) { | |
780 // If the static variable doesn't have the data, check {cache_menu}. | |
781 $cache = cache_get($cid, 'cache_menu'); | |
782 if ($cache && isset($cache->data)) { | |
783 $data = $cache->data; | |
784 } | |
785 else { | |
786 // Build and run the query, and build the tree. | |
787 if ($mlid) { | |
788 // The tree is for a single item, so we need to match the values in its | |
789 // p columns and 0 (the top level) with the plid values of other links. | |
790 $args = array(0); | |
791 for ($i = 1; $i < MENU_MAX_DEPTH; $i++) { | |
792 $args[] = $item["p$i"]; | |
793 } | |
794 $args = array_unique($args); | |
795 $placeholders = implode(', ', array_fill(0, count($args), '%d')); | |
796 $where = ' AND ml.plid IN ('. $placeholders .')'; | |
797 $parents = $args; | |
798 $parents[] = $item['mlid']; | |
799 } | |
800 else { | |
801 // Get all links in this menu. | |
802 $where = ''; | |
803 $args = array(); | |
804 $parents = array(); | |
805 } | |
806 array_unshift($args, $menu_name); | |
807 // Select the links from the table, and recursively build the tree. We | |
808 // LEFT JOIN since there is no match in {menu_router} for an external | |
809 // link. | |
810 $data['tree'] = menu_tree_data(db_query(" | |
811 SELECT m.load_functions, m.to_arg_functions, m.access_callback, m.access_arguments, m.page_callback, m.page_arguments, m.title, m.title_callback, m.title_arguments, m.type, m.description, ml.* | |
812 FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path | |
813 WHERE ml.menu_name = '%s'". $where ." | |
814 ORDER BY p1 ASC, p2 ASC, p3 ASC, p4 ASC, p5 ASC, p6 ASC, p7 ASC, p8 ASC, p9 ASC", $args), $parents); | |
815 $data['node_links'] = array(); | |
816 menu_tree_collect_node_links($data['tree'], $data['node_links']); | |
817 // Cache the data. | |
818 cache_set($cid, $data, 'cache_menu'); | |
819 } | |
820 // Check access for the current user to each item in the tree. | |
821 menu_tree_check_access($data['tree'], $data['node_links']); | |
822 $tree[$cid] = $data['tree']; | |
823 } | |
824 | |
825 return $tree[$cid]; | |
826 } | |
827 | |
828 /** | |
829 * Get the data structure representing a named menu tree, based on the current page. | |
830 * | |
831 * The tree order is maintained by storing each parent in an individual | |
832 * field, see http://drupal.org/node/141866 for more. | |
833 * | |
834 * @param $menu_name | |
835 * The named menu links to return | |
836 * @return | |
837 * An array of menu links, in the order they should be rendered. The array | |
838 * is a list of associative arrays -- these have two keys, link and below. | |
839 * link is a menu item, ready for theming as a link. Below represents the | |
840 * submenu below the link if there is one, and it is a subtree that has the | |
841 * same structure described for the top-level array. | |
842 */ | |
843 function menu_tree_page_data($menu_name = 'navigation') { | |
844 static $tree = array(); | |
845 | |
846 // Load the menu item corresponding to the current page. | |
847 if ($item = menu_get_item()) { | |
848 // Generate the cache ID. | |
849 $cid = 'links:'. $menu_name .':page:'. $item['href'] .':'. (int)$item['access']; | |
850 | |
851 if (!isset($tree[$cid])) { | |
852 // If the static variable doesn't have the data, check {cache_menu}. | |
853 $cache = cache_get($cid, 'cache_menu'); | |
854 if ($cache && isset($cache->data)) { | |
855 $data = $cache->data; | |
856 } | |
857 else { | |
858 // Build and run the query, and build the tree. | |
859 if ($item['access']) { | |
860 // Check whether a menu link exists that corresponds to the current path. | |
861 $args = array($menu_name, $item['href']); | |
862 $placeholders = "'%s'"; | |
863 if (drupal_is_front_page()) { | |
864 $args[] = '<front>'; | |
865 $placeholders .= ", '%s'"; | |
866 } | |
867 $parents = db_fetch_array(db_query("SELECT p1, p2, p3, p4, p5, p6, p7, p8 FROM {menu_links} WHERE menu_name = '%s' AND link_path IN (". $placeholders .")", $args)); | |
868 | |
869 if (empty($parents)) { | |
870 // If no link exists, we may be on a local task that's not in the links. | |
871 // TODO: Handle the case like a local task on a specific node in the menu. | |
872 $parents = db_fetch_array(db_query("SELECT p1, p2, p3, p4, p5, p6, p7, p8 FROM {menu_links} WHERE menu_name = '%s' AND link_path = '%s'", $menu_name, $item['tab_root'])); | |
873 } | |
874 // We always want all the top-level links with plid == 0. | |
875 $parents[] = '0'; | |
876 | |
877 // Use array_values() so that the indices are numeric for array_merge(). | |
878 $args = $parents = array_unique(array_values($parents)); | |
879 $placeholders = implode(', ', array_fill(0, count($args), '%d')); | |
880 $expanded = variable_get('menu_expanded', array()); | |
881 // Check whether the current menu has any links set to be expanded. | |
882 if (in_array($menu_name, $expanded)) { | |
883 // Collect all the links set to be expanded, and then add all of | |
884 // their children to the list as well. | |
885 do { | |
886 $result = db_query("SELECT mlid FROM {menu_links} WHERE menu_name = '%s' AND expanded = 1 AND has_children = 1 AND plid IN (". $placeholders .') AND mlid NOT IN ('. $placeholders .')', array_merge(array($menu_name), $args, $args)); | |
887 $num_rows = FALSE; | |
888 while ($item = db_fetch_array($result)) { | |
889 $args[] = $item['mlid']; | |
890 $num_rows = TRUE; | |
891 } | |
892 $placeholders = implode(', ', array_fill(0, count($args), '%d')); | |
893 } while ($num_rows); | |
894 } | |
895 array_unshift($args, $menu_name); | |
896 } | |
897 else { | |
898 // Show only the top-level menu items when access is denied. | |
899 $args = array($menu_name, '0'); | |
900 $placeholders = '%d'; | |
901 $parents = array(); | |
902 } | |
903 // Select the links from the table, and recursively build the tree. We | |
904 // LEFT JOIN since there is no match in {menu_router} for an external | |
905 // link. | |
906 $data['tree'] = menu_tree_data(db_query(" | |
907 SELECT m.load_functions, m.to_arg_functions, m.access_callback, m.access_arguments, m.page_callback, m.page_arguments, m.title, m.title_callback, m.title_arguments, m.type, m.description, ml.* | |
908 FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path | |
909 WHERE ml.menu_name = '%s' AND ml.plid IN (". $placeholders .") | |
910 ORDER BY p1 ASC, p2 ASC, p3 ASC, p4 ASC, p5 ASC, p6 ASC, p7 ASC, p8 ASC, p9 ASC", $args), $parents); | |
911 $data['node_links'] = array(); | |
912 menu_tree_collect_node_links($data['tree'], $data['node_links']); | |
913 // Cache the data. | |
914 cache_set($cid, $data, 'cache_menu'); | |
915 } | |
916 // Check access for the current user to each item in the tree. | |
917 menu_tree_check_access($data['tree'], $data['node_links']); | |
918 $tree[$cid] = $data['tree']; | |
919 } | |
920 return $tree[$cid]; | |
921 } | |
922 | |
923 return array(); | |
924 } | |
925 | |
926 /** | |
927 * Recursive helper function - collect node links. | |
928 */ | |
929 function menu_tree_collect_node_links(&$tree, &$node_links) { | |
930 foreach ($tree as $key => $v) { | |
931 if ($tree[$key]['link']['router_path'] == 'node/%') { | |
932 $nid = substr($tree[$key]['link']['link_path'], 5); | |
933 if (is_numeric($nid)) { | |
934 $node_links[$nid][$tree[$key]['link']['mlid']] = &$tree[$key]['link']; | |
935 $tree[$key]['link']['access'] = FALSE; | |
936 } | |
937 } | |
938 if ($tree[$key]['below']) { | |
939 menu_tree_collect_node_links($tree[$key]['below'], $node_links); | |
940 } | |
941 } | |
942 } | |
943 | |
944 /** | |
945 * Check access and perform other dynamic operations for each link in the tree. | |
946 */ | |
947 function menu_tree_check_access(&$tree, $node_links = array()) { | |
948 | |
949 if ($node_links) { | |
950 // Use db_rewrite_sql to evaluate view access without loading each full node. | |
951 $nids = array_keys($node_links); | |
952 $placeholders = '%d'. str_repeat(', %d', count($nids) - 1); | |
953 $result = db_query(db_rewrite_sql("SELECT n.nid FROM {node} n WHERE n.status = 1 AND n.nid IN (". $placeholders .")"), $nids); | |
954 while ($node = db_fetch_array($result)) { | |
955 $nid = $node['nid']; | |
956 foreach ($node_links[$nid] as $mlid => $link) { | |
957 $node_links[$nid][$mlid]['access'] = TRUE; | |
958 } | |
959 } | |
960 } | |
961 _menu_tree_check_access($tree); | |
962 return; | |
963 } | |
964 | |
965 /** | |
966 * Recursive helper function for menu_tree_check_access() | |
967 */ | |
968 function _menu_tree_check_access(&$tree) { | |
969 $new_tree = array(); | |
970 foreach ($tree as $key => $v) { | |
971 $item = &$tree[$key]['link']; | |
972 _menu_link_translate($item); | |
973 if ($item['access']) { | |
974 if ($tree[$key]['below']) { | |
975 _menu_tree_check_access($tree[$key]['below']); | |
976 } | |
977 // The weights are made a uniform 5 digits by adding 50000 as an offset. | |
978 // After _menu_link_translate(), $item['title'] has the localized link title. | |
979 // Adding the mlid to the end of the index insures that it is unique. | |
980 $new_tree[(50000 + $item['weight']) .' '. $item['title'] .' '. $item['mlid']] = $tree[$key]; | |
981 } | |
982 } | |
983 // Sort siblings in the tree based on the weights and localized titles. | |
984 ksort($new_tree); | |
985 $tree = $new_tree; | |
986 } | |
987 | |
988 /** | |
989 * Build the data representing a menu tree. | |
990 * | |
991 * @param $result | |
992 * The database result. | |
993 * @param $parents | |
994 * An array of the plid values that represent the path from the current page | |
995 * to the root of the menu tree. | |
996 * @param $depth | |
997 * The depth of the current menu tree. | |
998 * @return | |
999 * See menu_tree_page_data for a description of the data structure. | |
1000 */ | |
1001 function menu_tree_data($result = NULL, $parents = array(), $depth = 1) { | |
1002 list(, $tree) = _menu_tree_data($result, $parents, $depth); | |
1003 return $tree; | |
1004 } | |
1005 | |
1006 /** | |
1007 * Recursive helper function to build the data representing a menu tree. | |
1008 * | |
1009 * The function is a bit complex because the rendering of an item depends on | |
1010 * the next menu item. So we are always rendering the element previously | |
1011 * processed not the current one. | |
1012 */ | |
1013 function _menu_tree_data($result, $parents, $depth, $previous_element = '') { | |
1014 $remnant = NULL; | |
1015 $tree = array(); | |
1016 while ($item = db_fetch_array($result)) { | |
1017 // We need to determine if we're on the path to root so we can later build | |
1018 // the correct active trail and breadcrumb. | |
1019 $item['in_active_trail'] = in_array($item['mlid'], $parents); | |
1020 // The current item is the first in a new submenu. | |
1021 if ($item['depth'] > $depth) { | |
1022 // _menu_tree returns an item and the menu tree structure. | |
1023 list($item, $below) = _menu_tree_data($result, $parents, $item['depth'], $item); | |
1024 if ($previous_element) { | |
1025 $tree[$previous_element['mlid']] = array( | |
1026 'link' => $previous_element, | |
1027 'below' => $below, | |
1028 ); | |
1029 } | |
1030 else { | |
1031 $tree = $below; | |
1032 } | |
1033 // We need to fall back one level. | |
1034 if (!isset($item) || $item['depth'] < $depth) { | |
1035 return array($item, $tree); | |
1036 } | |
1037 // This will be the link to be output in the next iteration. | |
1038 $previous_element = $item; | |
1039 } | |
1040 // We are at the same depth, so we use the previous element. | |
1041 elseif ($item['depth'] == $depth) { | |
1042 if ($previous_element) { | |
1043 // Only the first time. | |
1044 $tree[$previous_element['mlid']] = array( | |
1045 'link' => $previous_element, | |
1046 'below' => FALSE, | |
1047 ); | |
1048 } | |
1049 // This will be the link to be output in the next iteration. | |
1050 $previous_element = $item; | |
1051 } | |
1052 // The submenu ended with the previous item, so pass back the current item. | |
1053 else { | |
1054 $remnant = $item; | |
1055 break; | |
1056 } | |
1057 } | |
1058 if ($previous_element) { | |
1059 // We have one more link dangling. | |
1060 $tree[$previous_element['mlid']] = array( | |
1061 'link' => $previous_element, | |
1062 'below' => FALSE, | |
1063 ); | |
1064 } | |
1065 return array($remnant, $tree); | |
1066 } | |
1067 | |
1068 /** | |
1069 * Generate the HTML output for a single menu link. | |
1070 * | |
1071 * @ingroup themeable | |
1072 */ | |
1073 function theme_menu_item_link($link) { | |
1074 if (empty($link['localized_options'])) { | |
1075 $link['localized_options'] = array(); | |
1076 } | |
1077 | |
1078 return l($link['title'], $link['href'], $link['localized_options']); | |
1079 } | |
1080 | |
1081 /** | |
1082 * Generate the HTML output for a menu tree | |
1083 * | |
1084 * @ingroup themeable | |
1085 */ | |
1086 function theme_menu_tree($tree) { | |
1087 return '<ul class="menu">'. $tree .'</ul>'; | |
1088 } | |
1089 | |
1090 /** | |
1091 * Generate the HTML output for a menu item and submenu. | |
1092 * | |
1093 * @ingroup themeable | |
1094 */ | |
1095 function theme_menu_item($link, $has_children, $menu = '', $in_active_trail = FALSE, $extra_class = NULL) { | |
1096 $class = ($menu ? 'expanded' : ($has_children ? 'collapsed' : 'leaf')); | |
1097 if (!empty($extra_class)) { | |
1098 $class .= ' '. $extra_class; | |
1099 } | |
1100 if ($in_active_trail) { | |
1101 $class .= ' active-trail'; | |
1102 } | |
1103 return '<li class="'. $class .'">'. $link . $menu ."</li>\n"; | |
1104 } | |
1105 | |
1106 /** | |
1107 * Generate the HTML output for a single local task link. | |
1108 * | |
1109 * @ingroup themeable | |
1110 */ | |
1111 function theme_menu_local_task($link, $active = FALSE) { | |
1112 return '<li '. ($active ? 'class="active" ' : '') .'>'. $link ."</li>\n"; | |
1113 } | |
1114 | |
1115 /** | |
1116 * Generates elements for the $arg array in the help hook. | |
1117 */ | |
1118 function drupal_help_arg($arg = array()) { | |
1119 // Note - the number of empty elements should be > MENU_MAX_PARTS. | |
1120 return $arg + array('', '', '', '', '', '', '', '', '', '', '', ''); | |
1121 } | |
1122 | |
1123 /** | |
1124 * Returns the help associated with the active menu item. | |
1125 */ | |
1126 function menu_get_active_help() { | |
1127 $output = ''; | |
1128 $router_path = menu_tab_root_path(); | |
1129 | |
1130 $arg = drupal_help_arg(arg(NULL)); | |
1131 $empty_arg = drupal_help_arg(); | |
1132 | |
1133 foreach (module_list() as $name) { | |
1134 if (module_hook($name, 'help')) { | |
1135 // Lookup help for this path. | |
1136 if ($help = module_invoke($name, 'help', $router_path, $arg)) { | |
1137 $output .= $help ."\n"; | |
1138 } | |
1139 // Add "more help" link on admin pages if the module provides a | |
1140 // standalone help page. | |
1141 if ($arg[0] == "admin" && module_exists('help') && module_invoke($name, 'help', 'admin/help#'. $arg[2], $empty_arg) && $help) { | |
1142 $output .= theme("more_help_link", url('admin/help/'. $arg[2])); | |
1143 } | |
1144 } | |
1145 } | |
1146 return $output; | |
1147 } | |
1148 | |
1149 /** | |
1150 * Build a list of named menus. | |
1151 */ | |
1152 function menu_get_names($reset = FALSE) { | |
1153 static $names; | |
1154 | |
1155 if ($reset || empty($names)) { | |
1156 $names = array(); | |
1157 $result = db_query("SELECT DISTINCT(menu_name) FROM {menu_links} ORDER BY menu_name"); | |
1158 while ($name = db_fetch_array($result)) { | |
1159 $names[] = $name['menu_name']; | |
1160 } | |
1161 } | |
1162 return $names; | |
1163 } | |
1164 | |
1165 /** | |
1166 * Return an array containing the names of system-defined (default) menus. | |
1167 */ | |
1168 function menu_list_system_menus() { | |
1169 return array('navigation', 'primary-links', 'secondary-links'); | |
1170 } | |
1171 | |
1172 /** | |
1173 * Return an array of links to be rendered as the Primary links. | |
1174 */ | |
1175 function menu_primary_links() { | |
1176 return menu_navigation_links(variable_get('menu_primary_links_source', 'primary-links')); | |
1177 } | |
1178 | |
1179 /** | |
1180 * Return an array of links to be rendered as the Secondary links. | |
1181 */ | |
1182 function menu_secondary_links() { | |
1183 | |
1184 // If the secondary menu source is set as the primary menu, we display the | |
1185 // second level of the primary menu. | |
1186 if (variable_get('menu_secondary_links_source', 'secondary-links') == variable_get('menu_primary_links_source', 'primary-links')) { | |
1187 return menu_navigation_links(variable_get('menu_primary_links_source', 'primary-links'), 1); | |
1188 } | |
1189 else { | |
1190 return menu_navigation_links(variable_get('menu_secondary_links_source', 'secondary-links'), 0); | |
1191 } | |
1192 } | |
1193 | |
1194 /** | |
1195 * Return an array of links for a navigation menu. | |
1196 * | |
1197 * @param $menu_name | |
1198 * The name of the menu. | |
1199 * @param $level | |
1200 * Optional, the depth of the menu to be returned. | |
1201 * @return | |
1202 * An array of links of the specified menu and level. | |
1203 */ | |
1204 function menu_navigation_links($menu_name, $level = 0) { | |
1205 // Don't even bother querying the menu table if no menu is specified. | |
1206 if (empty($menu_name)) { | |
1207 return array(); | |
1208 } | |
1209 | |
1210 // Get the menu hierarchy for the current page. | |
1211 $tree = menu_tree_page_data($menu_name); | |
1212 | |
1213 // Go down the active trail until the right level is reached. | |
1214 while ($level-- > 0 && $tree) { | |
1215 // Loop through the current level's items until we find one that is in trail. | |
1216 while ($item = array_shift($tree)) { | |
1217 if ($item['link']['in_active_trail']) { | |
1218 // If the item is in the active trail, we continue in the subtree. | |
1219 $tree = empty($item['below']) ? array() : $item['below']; | |
1220 break; | |
1221 } | |
1222 } | |
1223 } | |
1224 | |
1225 // Create a single level of links. | |
1226 $links = array(); | |
1227 foreach ($tree as $item) { | |
1228 if (!$item['link']['hidden']) { | |
1229 $l = $item['link']['localized_options']; | |
1230 $l['href'] = $item['link']['href']; | |
1231 $l['title'] = $item['link']['title']; | |
1232 // Keyed with unique menu id to generate classes from theme_links(). | |
1233 $links['menu-'. $item['link']['mlid']] = $l; | |
1234 } | |
1235 } | |
1236 return $links; | |
1237 } | |
1238 | |
1239 /** | |
1240 * Collects the local tasks (tabs) for a given level. | |
1241 * | |
1242 * @param $level | |
1243 * The level of tasks you ask for. Primary tasks are 0, secondary are 1. | |
1244 * @param $return_root | |
1245 * Whether to return the root path for the current page. | |
1246 * @return | |
1247 * Themed output corresponding to the tabs of the requested level, or | |
1248 * router path if $return_root == TRUE. This router path corresponds to | |
1249 * a parent tab, if the current page is a default local task. | |
1250 */ | |
1251 function menu_local_tasks($level = 0, $return_root = FALSE) { | |
1252 static $tabs; | |
1253 static $root_path; | |
1254 | |
1255 if (!isset($tabs)) { | |
1256 $tabs = array(); | |
1257 | |
1258 $router_item = menu_get_item(); | |
1259 if (!$router_item || !$router_item['access']) { | |
1260 return ''; | |
1261 } | |
1262 // Get all tabs and the root page. | |
1263 $result = db_query("SELECT * FROM {menu_router} WHERE tab_root = '%s' ORDER BY weight, title", $router_item['tab_root']); | |
1264 $map = arg(); | |
1265 $children = array(); | |
1266 $tasks = array(); | |
1267 $root_path = $router_item['path']; | |
1268 | |
1269 while ($item = db_fetch_array($result)) { | |
1270 _menu_translate($item, $map, TRUE); | |
1271 if ($item['tab_parent']) { | |
1272 // All tabs, but not the root page. | |
1273 $children[$item['tab_parent']][$item['path']] = $item; | |
1274 } | |
1275 // Store the translated item for later use. | |
1276 $tasks[$item['path']] = $item; | |
1277 } | |
1278 | |
1279 // Find all tabs below the current path. | |
1280 $path = $router_item['path']; | |
1281 // Tab parenting may skip levels, so the number of parts in the path may not | |
1282 // equal the depth. Thus we use the $depth counter (offset by 1000 for ksort). | |
1283 $depth = 1001; | |
1284 while (isset($children[$path])) { | |
1285 $tabs_current = ''; | |
1286 $next_path = ''; | |
1287 $count = 0; | |
1288 foreach ($children[$path] as $item) { | |
1289 if ($item['access']) { | |
1290 $count++; | |
1291 // The default task is always active. | |
1292 if ($item['type'] == MENU_DEFAULT_LOCAL_TASK) { | |
1293 // Find the first parent which is not a default local task. | |
1294 for ($p = $item['tab_parent']; $tasks[$p]['type'] == MENU_DEFAULT_LOCAL_TASK; $p = $tasks[$p]['tab_parent']); | |
1295 $link = theme('menu_item_link', array('href' => $tasks[$p]['href']) + $item); | |
1296 $tabs_current .= theme('menu_local_task', $link, TRUE); | |
1297 $next_path = $item['path']; | |
1298 } | |
1299 else { | |
1300 $link = theme('menu_item_link', $item); | |
1301 $tabs_current .= theme('menu_local_task', $link); | |
1302 } | |
1303 } | |
1304 } | |
1305 $path = $next_path; | |
1306 $tabs[$depth]['count'] = $count; | |
1307 $tabs[$depth]['output'] = $tabs_current; | |
1308 $depth++; | |
1309 } | |
1310 | |
1311 // Find all tabs at the same level or above the current one. | |
1312 $parent = $router_item['tab_parent']; | |
1313 $path = $router_item['path']; | |
1314 $current = $router_item; | |
1315 $depth = 1000; | |
1316 while (isset($children[$parent])) { | |
1317 $tabs_current = ''; | |
1318 $next_path = ''; | |
1319 $next_parent = ''; | |
1320 $count = 0; | |
1321 foreach ($children[$parent] as $item) { | |
1322 if ($item['access']) { | |
1323 $count++; | |
1324 if ($item['type'] == MENU_DEFAULT_LOCAL_TASK) { | |
1325 // Find the first parent which is not a default local task. | |
1326 for ($p = $item['tab_parent']; $tasks[$p]['type'] == MENU_DEFAULT_LOCAL_TASK; $p = $tasks[$p]['tab_parent']); | |
1327 $link = theme('menu_item_link', array('href' => $tasks[$p]['href']) + $item); | |
1328 if ($item['path'] == $router_item['path']) { | |
1329 $root_path = $tasks[$p]['path']; | |
1330 } | |
1331 } | |
1332 else { | |
1333 $link = theme('menu_item_link', $item); | |
1334 } | |
1335 // We check for the active tab. | |
1336 if ($item['path'] == $path) { | |
1337 $tabs_current .= theme('menu_local_task', $link, TRUE); | |
1338 $next_path = $item['tab_parent']; | |
1339 if (isset($tasks[$next_path])) { | |
1340 $next_parent = $tasks[$next_path]['tab_parent']; | |
1341 } | |
1342 } | |
1343 else { | |
1344 $tabs_current .= theme('menu_local_task', $link); | |
1345 } | |
1346 } | |
1347 } | |
1348 $path = $next_path; | |
1349 $parent = $next_parent; | |
1350 $tabs[$depth]['count'] = $count; | |
1351 $tabs[$depth]['output'] = $tabs_current; | |
1352 $depth--; | |
1353 } | |
1354 // Sort by depth. | |
1355 ksort($tabs); | |
1356 // Remove the depth, we are interested only in their relative placement. | |
1357 $tabs = array_values($tabs); | |
1358 } | |
1359 | |
1360 if ($return_root) { | |
1361 return $root_path; | |
1362 } | |
1363 else { | |
1364 // We do not display single tabs. | |
1365 return (isset($tabs[$level]) && $tabs[$level]['count'] > 1) ? $tabs[$level]['output'] : ''; | |
1366 } | |
1367 } | |
1368 | |
1369 /** | |
1370 * Returns the rendered local tasks at the top level. | |
1371 */ | |
1372 function menu_primary_local_tasks() { | |
1373 return menu_local_tasks(0); | |
1374 } | |
1375 | |
1376 /** | |
1377 * Returns the rendered local tasks at the second level. | |
1378 */ | |
1379 function menu_secondary_local_tasks() { | |
1380 return menu_local_tasks(1); | |
1381 } | |
1382 | |
1383 /** | |
1384 * Returns the router path, or the path of the parent tab of a default local task. | |
1385 */ | |
1386 function menu_tab_root_path() { | |
1387 return menu_local_tasks(0, TRUE); | |
1388 } | |
1389 | |
1390 /** | |
1391 * Returns the rendered local tasks. The default implementation renders them as tabs. | |
1392 * | |
1393 * @ingroup themeable | |
1394 */ | |
1395 function theme_menu_local_tasks() { | |
1396 $output = ''; | |
1397 | |
1398 if ($primary = menu_primary_local_tasks()) { | |
1399 $output .= "<ul class=\"tabs primary\">\n". $primary ."</ul>\n"; | |
1400 } | |
1401 if ($secondary = menu_secondary_local_tasks()) { | |
1402 $output .= "<ul class=\"tabs secondary\">\n". $secondary ."</ul>\n"; | |
1403 } | |
1404 | |
1405 return $output; | |
1406 } | |
1407 | |
1408 /** | |
1409 * Set (or get) the active menu for the current page - determines the active trail. | |
1410 */ | |
1411 function menu_set_active_menu_name($menu_name = NULL) { | |
1412 static $active; | |
1413 | |
1414 if (isset($menu_name)) { | |
1415 $active = $menu_name; | |
1416 } | |
1417 elseif (!isset($active)) { | |
1418 $active = 'navigation'; | |
1419 } | |
1420 return $active; | |
1421 } | |
1422 | |
1423 /** | |
1424 * Get the active menu for the current page - determines the active trail. | |
1425 */ | |
1426 function menu_get_active_menu_name() { | |
1427 return menu_set_active_menu_name(); | |
1428 } | |
1429 | |
1430 /** | |
1431 * Set the active path, which determines which page is loaded. | |
1432 * | |
1433 * @param $path | |
1434 * A Drupal path - not a path alias. | |
1435 * | |
1436 * Note that this may not have the desired effect unless invoked very early | |
1437 * in the page load, such as during hook_boot, or unless you call | |
1438 * menu_execute_active_handler() to generate your page output. | |
1439 */ | |
1440 function menu_set_active_item($path) { | |
1441 $_GET['q'] = $path; | |
1442 } | |
1443 | |
1444 /** | |
1445 * Set (or get) the active trail for the current page - the path to root in the menu tree. | |
1446 */ | |
1447 function menu_set_active_trail($new_trail = NULL) { | |
1448 static $trail; | |
1449 | |
1450 if (isset($new_trail)) { | |
1451 $trail = $new_trail; | |
1452 } | |
1453 elseif (!isset($trail)) { | |
1454 $trail = array(); | |
1455 $trail[] = array('title' => t('Home'), 'href' => '<front>', 'localized_options' => array(), 'type' => 0); | |
1456 $item = menu_get_item(); | |
1457 | |
1458 // Check whether the current item is a local task (displayed as a tab). | |
1459 if ($item['tab_parent']) { | |
1460 // The title of a local task is used for the tab, never the page title. | |
1461 // Thus, replace it with the item corresponding to the root path to get | |
1462 // the relevant href and title. For example, the menu item corresponding | |
1463 // to 'admin' is used when on the 'By module' tab at 'admin/by-module'. | |
1464 $parts = explode('/', $item['tab_root']); | |
1465 $args = arg(); | |
1466 // Replace wildcards in the root path using the current path. | |
1467 foreach ($parts as $index => $part) { | |
1468 if ($part == '%') { | |
1469 $parts[$index] = $args[$index]; | |
1470 } | |
1471 } | |
1472 // Retrieve the menu item using the root path after wildcard replacement. | |
1473 $root_item = menu_get_item(implode('/', $parts)); | |
1474 if ($root_item && $root_item['access']) { | |
1475 $item = $root_item; | |
1476 } | |
1477 } | |
1478 | |
1479 $tree = menu_tree_page_data(menu_get_active_menu_name()); | |
1480 list($key, $curr) = each($tree); | |
1481 | |
1482 while ($curr) { | |
1483 // Terminate the loop when we find the current path in the active trail. | |
1484 if ($curr['link']['href'] == $item['href']) { | |
1485 $trail[] = $curr['link']; | |
1486 $curr = FALSE; | |
1487 } | |
1488 else { | |
1489 // Move to the child link if it's in the active trail. | |
1490 if ($curr['below'] && $curr['link']['in_active_trail']) { | |
1491 $trail[] = $curr['link']; | |
1492 $tree = $curr['below']; | |
1493 } | |
1494 list($key, $curr) = each($tree); | |
1495 } | |
1496 } | |
1497 // Make sure the current page is in the trail (needed for the page title), | |
1498 // but exclude tabs and the front page. | |
1499 $last = count($trail) - 1; | |
1500 if ($trail[$last]['href'] != $item['href'] && !(bool)($item['type'] & MENU_IS_LOCAL_TASK) && !drupal_is_front_page()) { | |
1501 $trail[] = $item; | |
1502 } | |
1503 } | |
1504 return $trail; | |
1505 } | |
1506 | |
1507 /** | |
1508 * Get the active trail for the current page - the path to root in the menu tree. | |
1509 */ | |
1510 function menu_get_active_trail() { | |
1511 return menu_set_active_trail(); | |
1512 } | |
1513 | |
1514 /** | |
1515 * Get the breadcrumb for the current page, as determined by the active trail. | |
1516 */ | |
1517 function menu_get_active_breadcrumb() { | |
1518 $breadcrumb = array(); | |
1519 | |
1520 // No breadcrumb for the front page. | |
1521 if (drupal_is_front_page()) { | |
1522 return $breadcrumb; | |
1523 } | |
1524 | |
1525 $item = menu_get_item(); | |
1526 if ($item && $item['access']) { | |
1527 $active_trail = menu_get_active_trail(); | |
1528 | |
1529 foreach ($active_trail as $parent) { | |
1530 $breadcrumb[] = l($parent['title'], $parent['href'], $parent['localized_options']); | |
1531 } | |
1532 $end = end($active_trail); | |
1533 | |
1534 // Don't show a link to the current page in the breadcrumb trail. | |
1535 if ($item['href'] == $end['href'] || ($item['type'] == MENU_DEFAULT_LOCAL_TASK && $end['href'] != '<front>')) { | |
1536 array_pop($breadcrumb); | |
1537 } | |
1538 } | |
1539 return $breadcrumb; | |
1540 } | |
1541 | |
1542 /** | |
1543 * Get the title of the current page, as determined by the active trail. | |
1544 */ | |
1545 function menu_get_active_title() { | |
1546 $active_trail = menu_get_active_trail(); | |
1547 | |
1548 foreach (array_reverse($active_trail) as $item) { | |
1549 if (!(bool)($item['type'] & MENU_IS_LOCAL_TASK)) { | |
1550 return $item['title']; | |
1551 } | |
1552 } | |
1553 } | |
1554 | |
1555 /** | |
1556 * Get a menu link by its mlid, access checked and link translated for rendering. | |
1557 * | |
1558 * This function should never be called from within node_load() or any other | |
1559 * function used as a menu object load function since an infinite recursion may | |
1560 * occur. | |
1561 * | |
1562 * @param $mlid | |
1563 * The mlid of the menu item. | |
1564 * @return | |
1565 * A menu link, with $item['access'] filled and link translated for | |
1566 * rendering. | |
1567 */ | |
1568 function menu_link_load($mlid) { | |
1569 if (is_numeric($mlid) && $item = db_fetch_array(db_query("SELECT m.*, ml.* FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path WHERE ml.mlid = %d", $mlid))) { | |
1570 _menu_link_translate($item); | |
1571 return $item; | |
1572 } | |
1573 return FALSE; | |
1574 } | |
1575 | |
1576 /** | |
1577 * Clears the cached cached data for a single named menu. | |
1578 */ | |
1579 function menu_cache_clear($menu_name = 'navigation') { | |
1580 static $cache_cleared = array(); | |
1581 | |
1582 if (empty($cache_cleared[$menu_name])) { | |
1583 cache_clear_all('links:'. $menu_name .':', 'cache_menu', TRUE); | |
1584 $cache_cleared[$menu_name] = 1; | |
1585 } | |
1586 elseif ($cache_cleared[$menu_name] == 1) { | |
1587 register_shutdown_function('cache_clear_all', 'links:'. $menu_name .':', 'cache_menu', TRUE); | |
1588 $cache_cleared[$menu_name] = 2; | |
1589 } | |
1590 } | |
1591 | |
1592 /** | |
1593 * Clears all cached menu data. This should be called any time broad changes | |
1594 * might have been made to the router items or menu links. | |
1595 */ | |
1596 function menu_cache_clear_all() { | |
1597 cache_clear_all('*', 'cache_menu', TRUE); | |
1598 } | |
1599 | |
1600 /** | |
1601 * (Re)populate the database tables used by various menu functions. | |
1602 * | |
1603 * This function will clear and populate the {menu_router} table, add entries | |
1604 * to {menu_links} for new router items, then remove stale items from | |
1605 * {menu_links}. If called from update.php or install.php, it will also | |
1606 * schedule a call to itself on the first real page load from | |
1607 * menu_execute_active_handler(), because the maintenance page environment | |
1608 * is different and leaves stale data in the menu tables. | |
1609 */ | |
1610 function menu_rebuild() { | |
1611 variable_del('menu_rebuild_needed'); | |
1612 menu_cache_clear_all(); | |
1613 $menu = menu_router_build(TRUE); | |
1614 _menu_navigation_links_rebuild($menu); | |
1615 // Clear the page and block caches. | |
1616 _menu_clear_page_cache(); | |
1617 if (defined('MAINTENANCE_MODE')) { | |
1618 variable_set('menu_rebuild_needed', TRUE); | |
1619 } | |
1620 } | |
1621 | |
1622 /** | |
1623 * Collect, alter and store the menu definitions. | |
1624 */ | |
1625 function menu_router_build($reset = FALSE) { | |
1626 static $menu; | |
1627 | |
1628 if (!isset($menu) || $reset) { | |
1629 if (!$reset && ($cache = cache_get('router:', 'cache_menu')) && isset($cache->data)) { | |
1630 $menu = $cache->data; | |
1631 } | |
1632 else { | |
1633 db_query('DELETE FROM {menu_router}'); | |
1634 // We need to manually call each module so that we can know which module | |
1635 // a given item came from. | |
1636 $callbacks = array(); | |
1637 foreach (module_implements('menu') as $module) { | |
1638 $router_items = call_user_func($module .'_menu'); | |
1639 if (isset($router_items) && is_array($router_items)) { | |
1640 foreach (array_keys($router_items) as $path) { | |
1641 $router_items[$path]['module'] = $module; | |
1642 } | |
1643 $callbacks = array_merge($callbacks, $router_items); | |
1644 } | |
1645 } | |
1646 // Alter the menu as defined in modules, keys are like user/%user. | |
1647 drupal_alter('menu', $callbacks); | |
1648 $menu = _menu_router_build($callbacks); | |
1649 cache_set('router:', $menu, 'cache_menu'); | |
1650 } | |
1651 } | |
1652 return $menu; | |
1653 } | |
1654 | |
1655 /** | |
1656 * Builds a link from a router item. | |
1657 */ | |
1658 function _menu_link_build($item) { | |
1659 if ($item['type'] == MENU_CALLBACK) { | |
1660 $item['hidden'] = -1; | |
1661 } | |
1662 elseif ($item['type'] == MENU_SUGGESTED_ITEM) { | |
1663 $item['hidden'] = 1; | |
1664 } | |
1665 // Note, we set this as 'system', so that we can be sure to distinguish all | |
1666 // the menu links generated automatically from entries in {menu_router}. | |
1667 $item['module'] = 'system'; | |
1668 $item += array( | |
1669 'menu_name' => 'navigation', | |
1670 'link_title' => $item['title'], | |
1671 'link_path' => $item['path'], | |
1672 'hidden' => 0, | |
1673 'options' => empty($item['description']) ? array() : array('attributes' => array('title' => $item['description'])), | |
1674 ); | |
1675 return $item; | |
1676 } | |
1677 | |
1678 /** | |
1679 * Helper function to build menu links for the items in the menu router. | |
1680 */ | |
1681 function _menu_navigation_links_rebuild($menu) { | |
1682 // Add normal and suggested items as links. | |
1683 $menu_links = array(); | |
1684 foreach ($menu as $path => $item) { | |
1685 if ($item['_visible']) { | |
1686 $item = _menu_link_build($item); | |
1687 $menu_links[$path] = $item; | |
1688 $sort[$path] = $item['_number_parts']; | |
1689 } | |
1690 } | |
1691 if ($menu_links) { | |
1692 // Make sure no child comes before its parent. | |
1693 array_multisort($sort, SORT_NUMERIC, $menu_links); | |
1694 | |
1695 foreach ($menu_links as $item) { | |
1696 $existing_item = db_fetch_array(db_query("SELECT mlid, menu_name, plid, customized, has_children, updated FROM {menu_links} WHERE link_path = '%s' AND module = '%s'", $item['link_path'], 'system')); | |
1697 if ($existing_item) { | |
1698 $item['mlid'] = $existing_item['mlid']; | |
1699 $item['menu_name'] = $existing_item['menu_name']; | |
1700 $item['plid'] = $existing_item['plid']; | |
1701 $item['has_children'] = $existing_item['has_children']; | |
1702 $item['updated'] = $existing_item['updated']; | |
1703 } | |
1704 if (!$existing_item || !$existing_item['customized']) { | |
1705 menu_link_save($item); | |
1706 } | |
1707 } | |
1708 } | |
1709 $placeholders = db_placeholders($menu, 'varchar'); | |
1710 $paths = array_keys($menu); | |
1711 // Updated items and customized items which router paths are gone need new | |
1712 // router paths. | |
1713 $result = db_query("SELECT ml.link_path, ml.mlid, ml.router_path, ml.updated FROM {menu_links} ml WHERE ml.updated = 1 OR (router_path NOT IN ($placeholders) AND external = 0 AND customized = 1)", $paths); | |
1714 while ($item = db_fetch_array($result)) { | |
1715 $router_path = _menu_find_router_path($menu, $item['link_path']); | |
1716 if (!empty($router_path) && ($router_path != $item['router_path'] || $item['updated'])) { | |
1717 // If the router path and the link path matches, it's surely a working | |
1718 // item, so we clear the updated flag. | |
1719 $updated = $item['updated'] && $router_path != $item['link_path']; | |
1720 db_query("UPDATE {menu_links} SET router_path = '%s', updated = %d WHERE mlid = %d", $router_path, $updated, $item['mlid']); | |
1721 } | |
1722 } | |
1723 // Find any items where their router path does not exist any more. | |
1724 $result = db_query("SELECT * FROM {menu_links} WHERE router_path NOT IN ($placeholders) AND external = 0 AND updated = 0 AND customized = 0 ORDER BY depth DESC", $paths); | |
1725 // Remove all such items. Starting from those with the greatest depth will | |
1726 // minimize the amount of re-parenting done by menu_link_delete(). | |
1727 while ($item = db_fetch_array($result)) { | |
1728 _menu_delete_item($item, TRUE); | |
1729 } | |
1730 } | |
1731 | |
1732 /** | |
1733 * Delete one or several menu links. | |
1734 * | |
1735 * @param $mlid | |
1736 * A valid menu link mlid or NULL. If NULL, $path is used. | |
1737 * @param $path | |
1738 * The path to the menu items to be deleted. $mlid must be NULL. | |
1739 */ | |
1740 function menu_link_delete($mlid, $path = NULL) { | |
1741 if (isset($mlid)) { | |
1742 _menu_delete_item(db_fetch_array(db_query("SELECT * FROM {menu_links} WHERE mlid = %d", $mlid))); | |
1743 } | |
1744 else { | |
1745 $result = db_query("SELECT * FROM {menu_links} WHERE link_path = '%s'", $path); | |
1746 while ($link = db_fetch_array($result)) { | |
1747 _menu_delete_item($link); | |
1748 } | |
1749 } | |
1750 } | |
1751 | |
1752 /** | |
1753 * Helper function for menu_link_delete; deletes a single menu link. | |
1754 * | |
1755 * @param $item | |
1756 * Item to be deleted. | |
1757 * @param $force | |
1758 * Forces deletion. Internal use only, setting to TRUE is discouraged. | |
1759 */ | |
1760 function _menu_delete_item($item, $force = FALSE) { | |
1761 if ($item && ($item['module'] != 'system' || $item['updated'] || $force)) { | |
1762 // Children get re-attached to the item's parent. | |
1763 if ($item['has_children']) { | |
1764 $result = db_query("SELECT mlid FROM {menu_links} WHERE plid = %d", $item['mlid']); | |
1765 while ($m = db_fetch_array($result)) { | |
1766 $child = menu_link_load($m['mlid']); | |
1767 $child['plid'] = $item['plid']; | |
1768 menu_link_save($child); | |
1769 } | |
1770 } | |
1771 db_query('DELETE FROM {menu_links} WHERE mlid = %d', $item['mlid']); | |
1772 | |
1773 // Update the has_children status of the parent. | |
1774 _menu_update_parental_status($item); | |
1775 menu_cache_clear($item['menu_name']); | |
1776 _menu_clear_page_cache(); | |
1777 } | |
1778 } | |
1779 | |
1780 /** | |
1781 * Save a menu link. | |
1782 * | |
1783 * @param $item | |
1784 * An array representing a menu link item. The only mandatory keys are | |
1785 * link_path and link_title. Possible keys are | |
1786 * menu_name default is navigation | |
1787 * weight default is 0 | |
1788 * expanded whether the item is expanded. | |
1789 * options An array of options, @see l for more. | |
1790 * mlid Set to an existing value, or 0 or NULL to insert a new link. | |
1791 * plid The mlid of the parent. | |
1792 * router_path The path of the relevant router item. | |
1793 */ | |
1794 function menu_link_save(&$item) { | |
1795 $menu = menu_router_build(); | |
1796 | |
1797 drupal_alter('menu_link', $item, $menu); | |
1798 | |
1799 // This is the easiest way to handle the unique internal path '<front>', | |
1800 // since a path marked as external does not need to match a router path. | |
1801 $item['_external'] = menu_path_is_external($item['link_path']) || $item['link_path'] == '<front>'; | |
1802 // Load defaults. | |
1803 $item += array( | |
1804 'menu_name' => 'navigation', | |
1805 'weight' => 0, | |
1806 'link_title' => '', | |
1807 'hidden' => 0, | |
1808 'has_children' => 0, | |
1809 'expanded' => 0, | |
1810 'options' => array(), | |
1811 'module' => 'menu', | |
1812 'customized' => 0, | |
1813 'updated' => 0, | |
1814 ); | |
1815 $existing_item = FALSE; | |
1816 if (isset($item['mlid'])) { | |
1817 $existing_item = db_fetch_array(db_query("SELECT * FROM {menu_links} WHERE mlid = %d", $item['mlid'])); | |
1818 } | |
1819 | |
1820 if (isset($item['plid'])) { | |
1821 $parent = db_fetch_array(db_query("SELECT * FROM {menu_links} WHERE mlid = %d", $item['plid'])); | |
1822 } | |
1823 else { | |
1824 // Find the parent - it must be unique. | |
1825 $parent_path = $item['link_path']; | |
1826 $where = "WHERE link_path = '%s'"; | |
1827 // Only links derived from router items should have module == 'system', and | |
1828 // we want to find the parent even if it's in a different menu. | |
1829 if ($item['module'] == 'system') { | |
1830 $where .= " AND module = '%s'"; | |
1831 $arg2 = 'system'; | |
1832 } | |
1833 else { | |
1834 // If not derived from a router item, we respect the specified menu name. | |
1835 $where .= " AND menu_name = '%s'"; | |
1836 $arg2 = $item['menu_name']; | |
1837 } | |
1838 do { | |
1839 $parent = FALSE; | |
1840 $parent_path = substr($parent_path, 0, strrpos($parent_path, '/')); | |
1841 $result = db_query("SELECT COUNT(*) FROM {menu_links} ". $where, $parent_path, $arg2); | |
1842 // Only valid if we get a unique result. | |
1843 if (db_result($result) == 1) { | |
1844 $parent = db_fetch_array(db_query("SELECT * FROM {menu_links} ". $where, $parent_path, $arg2)); | |
1845 } | |
1846 } while ($parent === FALSE && $parent_path); | |
1847 } | |
1848 if ($parent !== FALSE) { | |
1849 $item['menu_name'] = $parent['menu_name']; | |
1850 } | |
1851 $menu_name = $item['menu_name']; | |
1852 // Menu callbacks need to be in the links table for breadcrumbs, but can't | |
1853 // be parents if they are generated directly from a router item. | |
1854 if (empty($parent['mlid']) || $parent['hidden'] < 0) { | |
1855 $item['plid'] = 0; | |
1856 } | |
1857 else { | |
1858 $item['plid'] = $parent['mlid']; | |
1859 } | |
1860 | |
1861 if (!$existing_item) { | |
1862 db_query("INSERT INTO {menu_links} ( | |
1863 menu_name, plid, link_path, | |
1864 hidden, external, has_children, | |
1865 expanded, weight, | |
1866 module, link_title, options, | |
1867 customized, updated) VALUES ( | |
1868 '%s', %d, '%s', | |
1869 %d, %d, %d, | |
1870 %d, %d, | |
1871 '%s', '%s', '%s', %d, %d)", | |
1872 $item['menu_name'], $item['plid'], $item['link_path'], | |
1873 $item['hidden'], $item['_external'], $item['has_children'], | |
1874 $item['expanded'], $item['weight'], | |
1875 $item['module'], $item['link_title'], serialize($item['options']), | |
1876 $item['customized'], $item['updated']); | |
1877 $item['mlid'] = db_last_insert_id('menu_links', 'mlid'); | |
1878 } | |
1879 | |
1880 if (!$item['plid']) { | |
1881 $item['p1'] = $item['mlid']; | |
1882 for ($i = 2; $i <= MENU_MAX_DEPTH; $i++) { | |
1883 $item["p$i"] = 0; | |
1884 } | |
1885 $item['depth'] = 1; | |
1886 } | |
1887 else { | |
1888 // Cannot add beyond the maximum depth. | |
1889 if ($item['has_children'] && $existing_item) { | |
1890 $limit = MENU_MAX_DEPTH - menu_link_children_relative_depth($existing_item) - 1; | |
1891 } | |
1892 else { | |
1893 $limit = MENU_MAX_DEPTH - 1; | |
1894 } | |
1895 if ($parent['depth'] > $limit) { | |
1896 return FALSE; | |
1897 } | |
1898 $item['depth'] = $parent['depth'] + 1; | |
1899 _menu_link_parents_set($item, $parent); | |
1900 } | |
1901 // Need to check both plid and menu_name, since plid can be 0 in any menu. | |
1902 if ($existing_item && ($item['plid'] != $existing_item['plid'] || $menu_name != $existing_item['menu_name'])) { | |
1903 _menu_link_move_children($item, $existing_item); | |
1904 } | |
1905 // Find the callback. During the menu update we store empty paths to be | |
1906 // fixed later, so we skip this. | |
1907 if (!isset($_SESSION['system_update_6021']) && (empty($item['router_path']) || !$existing_item || ($existing_item['link_path'] != $item['link_path']))) { | |
1908 if ($item['_external']) { | |
1909 $item['router_path'] = ''; | |
1910 } | |
1911 else { | |
1912 // Find the router path which will serve this path. | |
1913 $item['parts'] = explode('/', $item['link_path'], MENU_MAX_PARTS); | |
1914 $item['router_path'] = _menu_find_router_path($menu, $item['link_path']); | |
1915 } | |
1916 } | |
1917 db_query("UPDATE {menu_links} SET menu_name = '%s', plid = %d, link_path = '%s', | |
1918 router_path = '%s', hidden = %d, external = %d, has_children = %d, | |
1919 expanded = %d, weight = %d, depth = %d, | |
1920 p1 = %d, p2 = %d, p3 = %d, p4 = %d, p5 = %d, p6 = %d, p7 = %d, p8 = %d, p9 = %d, | |
1921 module = '%s', link_title = '%s', options = '%s', customized = %d WHERE mlid = %d", | |
1922 $item['menu_name'], $item['plid'], $item['link_path'], | |
1923 $item['router_path'], $item['hidden'], $item['_external'], $item['has_children'], | |
1924 $item['expanded'], $item['weight'], $item['depth'], | |
1925 $item['p1'], $item['p2'], $item['p3'], $item['p4'], $item['p5'], $item['p6'], $item['p7'], $item['p8'], $item['p9'], | |
1926 $item['module'], $item['link_title'], serialize($item['options']), $item['customized'], $item['mlid']); | |
1927 // Check the has_children status of the parent. | |
1928 _menu_update_parental_status($item); | |
1929 menu_cache_clear($menu_name); | |
1930 if ($existing_item && $menu_name != $existing_item['menu_name']) { | |
1931 menu_cache_clear($existing_item['menu_name']); | |
1932 } | |
1933 | |
1934 _menu_clear_page_cache(); | |
1935 return $item['mlid']; | |
1936 } | |
1937 | |
1938 /** | |
1939 * Helper function to clear the page and block caches at most twice per page load. | |
1940 */ | |
1941 function _menu_clear_page_cache() { | |
1942 static $cache_cleared = 0; | |
1943 | |
1944 // Clear the page and block caches, but at most twice, including at | |
1945 // the end of the page load when there are multple links saved or deleted. | |
1946 if (empty($cache_cleared)) { | |
1947 cache_clear_all(); | |
1948 // Keep track of which menus have expanded items. | |
1949 _menu_set_expanded_menus(); | |
1950 $cache_cleared = 1; | |
1951 } | |
1952 elseif ($cache_cleared == 1) { | |
1953 register_shutdown_function('cache_clear_all'); | |
1954 // Keep track of which menus have expanded items. | |
1955 register_shutdown_function('_menu_set_expanded_menus'); | |
1956 $cache_cleared = 2; | |
1957 } | |
1958 } | |
1959 | |
1960 /** | |
1961 * Helper function to update a list of menus with expanded items | |
1962 */ | |
1963 function _menu_set_expanded_menus() { | |
1964 $names = array(); | |
1965 $result = db_query("SELECT menu_name FROM {menu_links} WHERE expanded != 0 GROUP BY menu_name"); | |
1966 while ($n = db_fetch_array($result)) { | |
1967 $names[] = $n['menu_name']; | |
1968 } | |
1969 variable_set('menu_expanded', $names); | |
1970 } | |
1971 | |
1972 /** | |
1973 * Find the router path which will serve this path. | |
1974 * | |
1975 * @param $menu | |
1976 * The full built menu. | |
1977 * @param $link_path | |
1978 * The path for we are looking up its router path. | |
1979 * @return | |
1980 * A path from $menu keys or empty if $link_path points to a nonexisting | |
1981 * place. | |
1982 */ | |
1983 function _menu_find_router_path($menu, $link_path) { | |
1984 $parts = explode('/', $link_path, MENU_MAX_PARTS); | |
1985 $router_path = $link_path; | |
1986 if (!isset($menu[$router_path])) { | |
1987 list($ancestors) = menu_get_ancestors($parts); | |
1988 $ancestors[] = ''; | |
1989 foreach ($ancestors as $key => $router_path) { | |
1990 if (isset($menu[$router_path])) { | |
1991 break; | |
1992 } | |
1993 } | |
1994 } | |
1995 return $router_path; | |
1996 } | |
1997 | |
1998 /** | |
1999 * Insert, update or delete an uncustomized menu link related to a module. | |
2000 * | |
2001 * @param $module | |
2002 * The name of the module. | |
2003 * @param $op | |
2004 * Operation to perform: insert, update or delete. | |
2005 * @param $link_path | |
2006 * The path this link points to. | |
2007 * @param $link_title | |
2008 * Title of the link to insert or new title to update the link to. | |
2009 * Unused for delete. | |
2010 * @return | |
2011 * The insert op returns the mlid of the new item. Others op return NULL. | |
2012 */ | |
2013 function menu_link_maintain($module, $op, $link_path, $link_title) { | |
2014 switch ($op) { | |
2015 case 'insert': | |
2016 $menu_link = array( | |
2017 'link_title' => $link_title, | |
2018 'link_path' => $link_path, | |
2019 'module' => $module, | |
2020 ); | |
2021 return menu_link_save($menu_link); | |
2022 break; | |
2023 case 'update': | |
2024 db_query("UPDATE {menu_links} SET link_title = '%s' WHERE link_path = '%s' AND customized = 0 AND module = '%s'", $link_title, $link_path, $module); | |
2025 menu_cache_clear(); | |
2026 break; | |
2027 case 'delete': | |
2028 menu_link_delete(NULL, $link_path); | |
2029 break; | |
2030 } | |
2031 } | |
2032 | |
2033 /** | |
2034 * Find the depth of an item's children relative to its depth. | |
2035 * | |
2036 * For example, if the item has a depth of 2, and the maximum of any child in | |
2037 * the menu link tree is 5, the relative depth is 3. | |
2038 * | |
2039 * @param $item | |
2040 * An array representing a menu link item. | |
2041 * @return | |
2042 * The relative depth, or zero. | |
2043 * | |
2044 */ | |
2045 function menu_link_children_relative_depth($item) { | |
2046 $i = 1; | |
2047 $match = ''; | |
2048 $args[] = $item['menu_name']; | |
2049 $p = 'p1'; | |
2050 while ($i <= MENU_MAX_DEPTH && $item[$p]) { | |
2051 $match .= " AND $p = %d"; | |
2052 $args[] = $item[$p]; | |
2053 $p = 'p'. ++$i; | |
2054 } | |
2055 | |
2056 $max_depth = db_result(db_query_range("SELECT depth FROM {menu_links} WHERE menu_name = '%s'". $match ." ORDER BY depth DESC", $args, 0, 1)); | |
2057 | |
2058 return ($max_depth > $item['depth']) ? $max_depth - $item['depth'] : 0; | |
2059 } | |
2060 | |
2061 /** | |
2062 * Update the children of a menu link that's being moved. | |
2063 * | |
2064 * The menu name, parents (p1 - p6), and depth are updated for all children of | |
2065 * the link, and the has_children status of the previous parent is updated. | |
2066 */ | |
2067 function _menu_link_move_children($item, $existing_item) { | |
2068 | |
2069 $args[] = $item['menu_name']; | |
2070 $set[] = "menu_name = '%s'"; | |
2071 | |
2072 $i = 1; | |
2073 while ($i <= $item['depth']) { | |
2074 $p = 'p'. $i++; | |
2075 $set[] = "$p = %d"; | |
2076 $args[] = $item[$p]; | |
2077 } | |
2078 $j = $existing_item['depth'] + 1; | |
2079 while ($i <= MENU_MAX_DEPTH && $j <= MENU_MAX_DEPTH) { | |
2080 $set[] = 'p'. $i++ .' = p'. $j++; | |
2081 } | |
2082 while ($i <= MENU_MAX_DEPTH) { | |
2083 $set[] = 'p'. $i++ .' = 0'; | |
2084 } | |
2085 | |
2086 $shift = $item['depth'] - $existing_item['depth']; | |
2087 if ($shift < 0) { | |
2088 $args[] = -$shift; | |
2089 $set[] = 'depth = depth - %d'; | |
2090 } | |
2091 elseif ($shift > 0) { | |
2092 // The order of $set must be reversed so the new values don't overwrite the | |
2093 // old ones before they can be used because "Single-table UPDATE | |
2094 // assignments are generally evaluated from left to right" | |
2095 // see: http://dev.mysql.com/doc/refman/5.0/en/update.html | |
2096 $set = array_reverse($set); | |
2097 $args = array_reverse($args); | |
2098 | |
2099 $args[] = $shift; | |
2100 $set[] = 'depth = depth + %d'; | |
2101 } | |
2102 $where[] = "menu_name = '%s'"; | |
2103 $args[] = $existing_item['menu_name']; | |
2104 $p = 'p1'; | |
2105 for ($i = 1; $i <= MENU_MAX_DEPTH && $existing_item[$p]; $p = 'p'. ++$i) { | |
2106 $where[] = "$p = %d"; | |
2107 $args[] = $existing_item[$p]; | |
2108 } | |
2109 | |
2110 db_query("UPDATE {menu_links} SET ". implode(', ', $set) ." WHERE ". implode(' AND ', $where), $args); | |
2111 // Check the has_children status of the parent, while excluding this item. | |
2112 _menu_update_parental_status($existing_item, TRUE); | |
2113 } | |
2114 | |
2115 /** | |
2116 * Check and update the has_children status for the parent of a link. | |
2117 */ | |
2118 function _menu_update_parental_status($item, $exclude = FALSE) { | |
2119 // If plid == 0, there is nothing to update. | |
2120 if ($item['plid']) { | |
2121 // We may want to exclude the passed link as a possible child. | |
2122 $where = $exclude ? " AND mlid != %d" : ''; | |
2123 // Check if at least one visible child exists in the table. | |
2124 $parent_has_children = (bool)db_result(db_query_range("SELECT mlid FROM {menu_links} WHERE menu_name = '%s' AND plid = %d AND hidden = 0". $where, $item['menu_name'], $item['plid'], $item['mlid'], 0, 1)); | |
2125 db_query("UPDATE {menu_links} SET has_children = %d WHERE mlid = %d", $parent_has_children, $item['plid']); | |
2126 } | |
2127 } | |
2128 | |
2129 /** | |
2130 * Helper function that sets the p1..p9 values for a menu link being saved. | |
2131 */ | |
2132 function _menu_link_parents_set(&$item, $parent) { | |
2133 $i = 1; | |
2134 while ($i < $item['depth']) { | |
2135 $p = 'p'. $i++; | |
2136 $item[$p] = $parent[$p]; | |
2137 } | |
2138 $p = 'p'. $i++; | |
2139 // The parent (p1 - p9) corresponding to the depth always equals the mlid. | |
2140 $item[$p] = $item['mlid']; | |
2141 while ($i <= MENU_MAX_DEPTH) { | |
2142 $p = 'p'. $i++; | |
2143 $item[$p] = 0; | |
2144 } | |
2145 } | |
2146 | |
2147 /** | |
2148 * Helper function to build the router table based on the data from hook_menu. | |
2149 */ | |
2150 function _menu_router_build($callbacks) { | |
2151 // First pass: separate callbacks from paths, making paths ready for | |
2152 // matching. Calculate fitness, and fill some default values. | |
2153 $menu = array(); | |
2154 foreach ($callbacks as $path => $item) { | |
2155 $load_functions = array(); | |
2156 $to_arg_functions = array(); | |
2157 $fit = 0; | |
2158 $move = FALSE; | |
2159 | |
2160 $parts = explode('/', $path, MENU_MAX_PARTS); | |
2161 $number_parts = count($parts); | |
2162 // We store the highest index of parts here to save some work in the fit | |
2163 // calculation loop. | |
2164 $slashes = $number_parts - 1; | |
2165 // Extract load and to_arg functions. | |
2166 foreach ($parts as $k => $part) { | |
2167 $match = FALSE; | |
2168 if (preg_match('/^%([a-z_]*)$/', $part, $matches)) { | |
2169 if (empty($matches[1])) { | |
2170 $match = TRUE; | |
2171 $load_functions[$k] = NULL; | |
2172 } | |
2173 else { | |
2174 if (function_exists($matches[1] .'_to_arg')) { | |
2175 $to_arg_functions[$k] = $matches[1] .'_to_arg'; | |
2176 $load_functions[$k] = NULL; | |
2177 $match = TRUE; | |
2178 } | |
2179 if (function_exists($matches[1] .'_load')) { | |
2180 $function = $matches[1] .'_load'; | |
2181 // Create an array of arguments that will be passed to the _load | |
2182 // function when this menu path is checked, if 'load arguments' | |
2183 // exists. | |
2184 $load_functions[$k] = isset($item['load arguments']) ? array($function => $item['load arguments']) : $function; | |
2185 $match = TRUE; | |
2186 } | |
2187 } | |
2188 } | |
2189 if ($match) { | |
2190 $parts[$k] = '%'; | |
2191 } | |
2192 else { | |
2193 $fit |= 1 << ($slashes - $k); | |
2194 } | |
2195 } | |
2196 if ($fit) { | |
2197 $move = TRUE; | |
2198 } | |
2199 else { | |
2200 // If there is no %, it fits maximally. | |
2201 $fit = (1 << $number_parts) - 1; | |
2202 } | |
2203 $masks[$fit] = 1; | |
2204 $item['load_functions'] = empty($load_functions) ? '' : serialize($load_functions); | |
2205 $item['to_arg_functions'] = empty($to_arg_functions) ? '' : serialize($to_arg_functions); | |
2206 $item += array( | |
2207 'title' => '', | |
2208 'weight' => 0, | |
2209 'type' => MENU_NORMAL_ITEM, | |
2210 '_number_parts' => $number_parts, | |
2211 '_parts' => $parts, | |
2212 '_fit' => $fit, | |
2213 ); | |
2214 $item += array( | |
2215 '_visible' => (bool)($item['type'] & MENU_VISIBLE_IN_BREADCRUMB), | |
2216 '_tab' => (bool)($item['type'] & MENU_IS_LOCAL_TASK), | |
2217 ); | |
2218 if ($move) { | |
2219 $new_path = implode('/', $item['_parts']); | |
2220 $menu[$new_path] = $item; | |
2221 $sort[$new_path] = $number_parts; | |
2222 } | |
2223 else { | |
2224 $menu[$path] = $item; | |
2225 $sort[$path] = $number_parts; | |
2226 } | |
2227 } | |
2228 array_multisort($sort, SORT_NUMERIC, $menu); | |
2229 | |
2230 // Apply inheritance rules. | |
2231 foreach ($menu as $path => $v) { | |
2232 $item = &$menu[$path]; | |
2233 if (!$item['_tab']) { | |
2234 // Non-tab items. | |
2235 $item['tab_parent'] = ''; | |
2236 $item['tab_root'] = $path; | |
2237 } | |
2238 for ($i = $item['_number_parts'] - 1; $i; $i--) { | |
2239 $parent_path = implode('/', array_slice($item['_parts'], 0, $i)); | |
2240 if (isset($menu[$parent_path])) { | |
2241 | |
2242 $parent = $menu[$parent_path]; | |
2243 | |
2244 if (!isset($item['tab_parent'])) { | |
2245 // Parent stores the parent of the path. | |
2246 $item['tab_parent'] = $parent_path; | |
2247 } | |
2248 if (!isset($item['tab_root']) && !$parent['_tab']) { | |
2249 $item['tab_root'] = $parent_path; | |
2250 } | |
2251 // If a callback is not found, we try to find the first parent that | |
2252 // has a callback. | |
2253 if (!isset($item['access callback']) && isset($parent['access callback'])) { | |
2254 $item['access callback'] = $parent['access callback']; | |
2255 if (!isset($item['access arguments']) && isset($parent['access arguments'])) { | |
2256 $item['access arguments'] = $parent['access arguments']; | |
2257 } | |
2258 } | |
2259 // Same for page callbacks. | |
2260 if (!isset($item['page callback']) && isset($parent['page callback'])) { | |
2261 $item['page callback'] = $parent['page callback']; | |
2262 if (!isset($item['page arguments']) && isset($parent['page arguments'])) { | |
2263 $item['page arguments'] = $parent['page arguments']; | |
2264 } | |
2265 if (!isset($item['file']) && isset($parent['file'])) { | |
2266 $item['file'] = $parent['file']; | |
2267 } | |
2268 if (!isset($item['file path']) && isset($parent['file path'])) { | |
2269 $item['file path'] = $parent['file path']; | |
2270 } | |
2271 } | |
2272 } | |
2273 } | |
2274 if (!isset($item['access callback']) && isset($item['access arguments'])) { | |
2275 // Default callback. | |
2276 $item['access callback'] = 'user_access'; | |
2277 } | |
2278 if (!isset($item['access callback']) || empty($item['page callback'])) { | |
2279 $item['access callback'] = 0; | |
2280 } | |
2281 if (is_bool($item['access callback'])) { | |
2282 $item['access callback'] = intval($item['access callback']); | |
2283 } | |
2284 | |
2285 $item += array( | |
2286 'access arguments' => array(), | |
2287 'access callback' => '', | |
2288 'page arguments' => array(), | |
2289 'page callback' => '', | |
2290 'block callback' => '', | |
2291 'title arguments' => array(), | |
2292 'title callback' => 't', | |
2293 'description' => '', | |
2294 'position' => '', | |
2295 'tab_parent' => '', | |
2296 'tab_root' => $path, | |
2297 'path' => $path, | |
2298 'file' => '', | |
2299 'file path' => '', | |
2300 'include file' => '', | |
2301 ); | |
2302 | |
2303 // Calculate out the file to be included for each callback, if any. | |
2304 if ($item['file']) { | |
2305 $file_path = $item['file path'] ? $item['file path'] : drupal_get_path('module', $item['module']); | |
2306 $item['include file'] = $file_path .'/'. $item['file']; | |
2307 } | |
2308 | |
2309 $title_arguments = $item['title arguments'] ? serialize($item['title arguments']) : ''; | |
2310 db_query("INSERT INTO {menu_router} | |
2311 (path, load_functions, to_arg_functions, access_callback, | |
2312 access_arguments, page_callback, page_arguments, fit, | |
2313 number_parts, tab_parent, tab_root, | |
2314 title, title_callback, title_arguments, | |
2315 type, block_callback, description, position, weight, file) | |
2316 VALUES ('%s', '%s', '%s', '%s', | |
2317 '%s', '%s', '%s', %d, | |
2318 %d, '%s', '%s', | |
2319 '%s', '%s', '%s', | |
2320 %d, '%s', '%s', '%s', %d, '%s')", | |
2321 $path, $item['load_functions'], $item['to_arg_functions'], $item['access callback'], | |
2322 serialize($item['access arguments']), $item['page callback'], serialize($item['page arguments']), $item['_fit'], | |
2323 $item['_number_parts'], $item['tab_parent'], $item['tab_root'], | |
2324 $item['title'], $item['title callback'], $title_arguments, | |
2325 $item['type'], $item['block callback'], $item['description'], $item['position'], $item['weight'], $item['include file']); | |
2326 } | |
2327 // Sort the masks so they are in order of descending fit, and store them. | |
2328 $masks = array_keys($masks); | |
2329 rsort($masks); | |
2330 variable_set('menu_masks', $masks); | |
2331 return $menu; | |
2332 } | |
2333 | |
2334 /** | |
2335 * Returns TRUE if a path is external (e.g. http://example.com). | |
2336 */ | |
2337 function menu_path_is_external($path) { | |
2338 $colonpos = strpos($path, ':'); | |
2339 return $colonpos !== FALSE && !preg_match('![/?#]!', substr($path, 0, $colonpos)) && filter_xss_bad_protocol($path, FALSE) == check_plain($path); | |
2340 } | |
2341 | |
2342 /** | |
2343 * Checks whether the site is off-line for maintenance. | |
2344 * | |
2345 * This function will log the current user out and redirect to front page | |
2346 * if the current user has no 'administer site configuration' permission. | |
2347 * | |
2348 * @return | |
2349 * FALSE if the site is not off-line or its the login page or the user has | |
2350 * 'administer site configuration' permission. | |
2351 * TRUE for anonymous users not on the login page if the site is off-line. | |
2352 */ | |
2353 function _menu_site_is_offline() { | |
2354 // Check if site is set to off-line mode. | |
2355 if (variable_get('site_offline', 0)) { | |
2356 // Check if the user has administration privileges. | |
2357 if (user_access('administer site configuration')) { | |
2358 // Ensure that the off-line message is displayed only once [allowing for | |
2359 // page redirects], and specifically suppress its display on the site | |
2360 // maintenance page. | |
2361 if (drupal_get_normal_path($_GET['q']) != 'admin/settings/site-maintenance') { | |
2362 drupal_set_message(t('Operating in off-line mode.'), 'status', FALSE); | |
2363 } | |
2364 } | |
2365 else { | |
2366 // Anonymous users get a FALSE at the login prompt, TRUE otherwise. | |
2367 if (user_is_anonymous()) { | |
2368 return $_GET['q'] != 'user' && $_GET['q'] != 'user/login'; | |
2369 } | |
2370 // Logged in users are unprivileged here, so they are logged out. | |
2371 require_once drupal_get_path('module', 'user') .'/user.pages.inc'; | |
2372 user_logout(); | |
2373 } | |
2374 } | |
2375 return FALSE; | |
2376 } | |
2377 | |
2378 /** | |
2379 * Validates the path of a menu link being created or edited. | |
2380 * | |
2381 * @return | |
2382 * TRUE if it is a valid path AND the current user has access permission, | |
2383 * FALSE otherwise. | |
2384 */ | |
2385 function menu_valid_path($form_item) { | |
2386 global $menu_admin; | |
2387 $item = array(); | |
2388 $path = $form_item['link_path']; | |
2389 // We indicate that a menu administrator is running the menu access check. | |
2390 $menu_admin = TRUE; | |
2391 if ($path == '<front>' || menu_path_is_external($path)) { | |
2392 $item = array('access' => TRUE); | |
2393 } | |
2394 elseif (preg_match('/\/\%/', $path)) { | |
2395 // Path is dynamic (ie 'user/%'), so check directly against menu_router table. | |
2396 if ($item = db_fetch_array(db_query("SELECT * FROM {menu_router} where path = '%s' ", $path))) { | |
2397 $item['link_path'] = $form_item['link_path']; | |
2398 $item['link_title'] = $form_item['link_title']; | |
2399 $item['external'] = FALSE; | |
2400 $item['options'] = ''; | |
2401 _menu_link_translate($item); | |
2402 } | |
2403 } | |
2404 else { | |
2405 $item = menu_get_item($path); | |
2406 } | |
2407 $menu_admin = FALSE; | |
2408 return $item && $item['access']; | |
2409 } | |
2410 | |
2411 /** | |
2412 * @} End of "defgroup menu". | |
2413 */ |