menu.inc 42 KB
Newer Older
Dries's avatar
 
Dries committed
1
<?php
2
// $Id$
Kjartan's avatar
Kjartan committed
3

4 5 6 7 8
/**
 * @file
 * API for the Drupal menu system.
 */

Dries's avatar
 
Dries committed
9 10 11
/**
 * @defgroup menu Menu system
 * @{
Dries's avatar
 
Dries committed
12
 * Define the navigation menus, and route page requests to code based on URLs.
Dries's avatar
 
Dries committed
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
 *
 * The Drupal menu system drives both the navigation system from a user
 * perspective and the callback system that Drupal uses to respond to URLs
 * passed from the browser. For this reason, a good understanding of the
 * menu system is fundamental to the creation of complex modules.
 *
 * Drupal's menu system follows a simple hierarchy defined by paths.
 * Implementations of hook_menu() define menu items and assign them to
 * paths (which should be unique). The menu system aggregates these items
 * and determines the menu hierarchy from the paths. For example, if the
 * paths defined were a, a/b, e, a/b/c/d, f/g, and a/b/h, the menu system
 * would form the structure:
 * - a
 *   - a/b
 *     - a/b/c/d
 *     - a/b/h
 * - e
 * - f/g
 * Note that the number of elements in the path does not necessarily
 * determine the depth of the menu item in the tree.
 *
 * When responding to a page request, the menu system looks to see if the
 * path requested by the browser is registered as a menu item with a
 * callback. If not, the system searches up the menu tree for the most
 * complete match with a callback it can find. If the path a/b/i is
 * requested in the tree above, the callback for a/b would be used.
 *
Steven Wittens's avatar
Steven Wittens committed
40 41 42 43 44 45
 * The found callback function is called with any arguments specified
 * in the "callback arguments" attribute of its menu item. The
 * attribute must be an array. After these arguments, any remaining
 * components of the path are appended as further arguments. In this
 * way, the callback for a/b above could respond to a request for
 * a/b/i differently than a request for a/b/j.
Dries's avatar
 
Dries committed
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
 *
 * For an illustration of this process, see page_example.module.
 *
 * Access to the callback functions is also protected by the menu system.
 * The "access" attribute of each menu item is checked as the search for a
 * callback proceeds. If this attribute is TRUE, then access is granted; if
 * FALSE, then access is denied. The first found "access" attribute
 * determines the accessibility of the target. Menu items may omit this
 * attribute to use the value provided by an ancestor item.
 *
 * In the default Drupal interface, you will notice many links rendered as
 * tabs. These are known in the menu system as "local tasks", and they are
 * rendered as tabs by default, though other presentations are possible.
 * Local tasks function just as other menu items in most respects. It is
 * convention that the names of these tasks should be short verbs if
 * possible. In addition, a "default" local task should be provided for
 * each set. When visiting a local task's parent menu item, the default
 * local task will be rendered as if it is selected; this provides for a
 * normal tab user experience. This default task is special in that it
 * links not to its provided path, but to its parent item's path instead.
 * The default task's path is only used to place it appropriately in the
 * menu hierarchy.
Dries's avatar
 
Dries committed
68 69
 */

Dries's avatar
 
Dries committed
70
/**
Dries's avatar
 
Dries committed
71
 * @name Menu flags
Dries's avatar
 
Dries committed
72
 * @{
Dries's avatar
 
Dries committed
73 74
 * Flags for use in the "type" attribute of menu items.
 */
Dries's avatar
 
Dries committed
75

Dries's avatar
 
Dries committed
76 77 78 79 80 81 82 83
define('MENU_IS_ROOT', 0x0001);
define('MENU_VISIBLE_IN_TREE', 0x0002);
define('MENU_VISIBLE_IN_BREADCRUMB', 0x0004);
define('MENU_VISIBLE_IF_HAS_CHILDREN', 0x0008);
define('MENU_MODIFIABLE_BY_ADMIN', 0x0010);
define('MENU_MODIFIED_BY_ADMIN', 0x0020);
define('MENU_CREATED_BY_ADMIN', 0x0040);
define('MENU_IS_LOCAL_TASK', 0x0080);
Dries's avatar
 
Dries committed
84
define('MENU_EXPANDED', 0x0100);
Dries's avatar
 
Dries committed
85
define('MENU_LINKS_TO_PARENT', 0x0200);
Dries's avatar
 
Dries committed
86

Dries's avatar
 
Dries committed
87
/**
Dries's avatar
 
Dries committed
88
 * @} End of "Menu flags".
Dries's avatar
 
Dries committed
89 90 91
 */

/**
Dries's avatar
 
Dries committed
92
 * @name Menu item types
Dries's avatar
 
Dries committed
93 94 95 96
 * @{
 * Menu item definitions provide one of these constants, which are shortcuts for
 * combinations of the above flags.
 */
Dries's avatar
 
Dries committed
97

Dries's avatar
 
Dries committed
98 99
/**
 * Normal menu items show up in the menu tree and can be moved/hidden by
Dries's avatar
 
Dries committed
100 101
 * the administrator. Use this for most menu items. It is the default value if
 * no menu item type is specified.
Dries's avatar
 
Dries committed
102 103
 */
define('MENU_NORMAL_ITEM', MENU_VISIBLE_IN_TREE | MENU_VISIBLE_IN_BREADCRUMB | MENU_MODIFIABLE_BY_ADMIN);
104

Dries's avatar
 
Dries committed
105 106
/**
 * Item groupings are used for pages like "node/add" that simply list
Dries's avatar
 
Dries committed
107 108
 * subpages to visit. They are distinguished from other pages in that they will
 * disappear from the menu if no subpages exist.
Dries's avatar
 
Dries committed
109 110
 */
define('MENU_ITEM_GROUPING', MENU_VISIBLE_IF_HAS_CHILDREN | MENU_VISIBLE_IN_BREADCRUMB | MENU_MODIFIABLE_BY_ADMIN);
Dries's avatar
 
Dries committed
111

Dries's avatar
 
Dries committed
112 113
/**
 * Callbacks simply register a path so that the correct function is fired
Dries's avatar
 
Dries committed
114
 * when the URL is accessed. They are not shown in the menu.
Dries's avatar
 
Dries committed
115 116
 */
define('MENU_CALLBACK', MENU_VISIBLE_IN_BREADCRUMB);
Dries's avatar
 
Dries committed
117

118
/**
Dries's avatar
 
Dries committed
119 120 121 122
 * Dynamic menu items change frequently, and so should not be stored in the
 * database for administrative customization.
 */
define('MENU_DYNAMIC_ITEM', MENU_VISIBLE_IN_TREE | MENU_VISIBLE_IN_BREADCRUMB);
Dries's avatar
 
Dries committed
123

Dries's avatar
 
Dries committed
124
/**
Dries's avatar
 
Dries committed
125 126
 * Modules may "suggest" menu items that the administrator may enable. They act
 * just as callbacks do until enabled, at which time they act like normal items.
Dries's avatar
 
Dries committed
127
 */
128
define('MENU_SUGGESTED_ITEM', MENU_MODIFIABLE_BY_ADMIN | MENU_VISIBLE_IN_BREADCRUMB);
Dries's avatar
 
Dries committed
129 130

/**
Dries's avatar
 
Dries committed
131 132 133
 * Local tasks are rendered as tabs by default. Use this for menu items that
 * describe actions to be performed on their parent item. An example is the path
 * "node/52/edit", which performs the "edit" task on "node/52".
Dries's avatar
 
Dries committed
134 135 136
 */
define('MENU_LOCAL_TASK', MENU_IS_LOCAL_TASK);

Dries's avatar
 
Dries committed
137 138 139 140 141 142
/**
 * Every set of local tasks should provide one "default" task, that links to the
 * same path as its parent when clicked.
 */
define('MENU_DEFAULT_LOCAL_TASK', MENU_IS_LOCAL_TASK | MENU_LINKS_TO_PARENT);

Dries's avatar
 
Dries committed
143
/**
Dries's avatar
 
Dries committed
144 145
 * Custom items are those defined by the administrator. Reserved for internal
 * use; do not return from hook_menu() implementations.
Dries's avatar
 
Dries committed
146 147 148 149
 */
define('MENU_CUSTOM_ITEM', MENU_VISIBLE_IN_TREE | MENU_VISIBLE_IN_BREADCRUMB | MENU_CREATED_BY_ADMIN | MENU_MODIFIABLE_BY_ADMIN);

/**
Dries's avatar
 
Dries committed
150 151
 * Custom menus are those defined by the administrator. Reserved for internal
 * use; do not return from hook_menu() implementations.
Dries's avatar
 
Dries committed
152 153 154 155
 */
define('MENU_CUSTOM_MENU', MENU_IS_ROOT | MENU_VISIBLE_IN_TREE | MENU_CREATED_BY_ADMIN | MENU_MODIFIABLE_BY_ADMIN);

/**
Dries's avatar
 
Dries committed
156
 * @} End of "Menu item types".
Dries's avatar
 
Dries committed
157 158 159 160 161
 */

/**
 * @name Menu status codes
 * @{
Dries's avatar
 
Dries committed
162 163
 * Status codes for menu callbacks.
 */
Dries's avatar
 
Dries committed
164

Dries's avatar
 
Dries committed
165 166 167
define('MENU_FOUND', 1);
define('MENU_NOT_FOUND', 2);
define('MENU_ACCESS_DENIED', 3);
168
define('MENU_SITE_OFFLINE', 4);
Dries's avatar
 
Dries committed
169

Dries's avatar
 
Dries committed
170
/**
Dries's avatar
 
Dries committed
171
 * @} End of "Menu status codes".
Dries's avatar
 
Dries committed
172
 */
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188

/**
 * Return the menu data structure.
 *
 * The returned structure contains much information that is useful only
 * internally in the menu system. External modules are likely to need only
 * the ['visible'] element of the returned array. All menu items that are
 * accessible to the current user and not hidden will be present here, so
 * modules and themes can use this structure to build their own representations
 * of the menu.
 *
 * $menu['visible'] will contain an associative array, the keys of which
 * are menu IDs. The values of this array are themselves associative arrays,
 * with the following key-value pairs defined:
 * - 'title' - The displayed title of the menu or menu item. It will already
 *   have been translated by the locale system.
Dries's avatar
 
Dries committed
189 190
 * - 'description' - The description (link title attribute) of the menu item.
 *   It will already have been translated by the locale system.
191
 * - 'path' - The Drupal path to the menu item. A link to a particular item
Dries's avatar
 
Dries committed
192 193
 *   can thus be constructed with
 *   l($item['title'], $item['path'], array('title' => $item['description'])).
194 195 196 197 198 199 200 201
 * - 'children' - A linear list of the menu ID's of this item's children.
 *
 * Menu ID 0 is the "root" of the menu. The children of this item are the
 * menus themselves (they will have no associated path). Menu ID 1 will
 * always be one of these children; it is the default "Navigation" menu.
 */
function menu_get_menu() {
  global $_menu;
Dries's avatar
 
Dries committed
202
  global $user;
203
  global $locale;
204 205

  if (!isset($_menu['items'])) {
Dries's avatar
 
Dries committed
206 207
    // _menu_build() may indirectly call this function, so prevent infinite loops.
    $_menu['items'] = array();
Dries's avatar
 
Dries committed
208

209 210
    $cid = "$user->uid:$locale";
    if ($cached = cache_get($cid, 'cache_menu')) {
Dries's avatar
 
Dries committed
211 212 213 214 215
      $_menu = unserialize($cached->data);
    }
    else {
      _menu_build();
      // Cache the menu structure for this user, to expire after one day.
216
      cache_set($cid, 'cache_menu', serialize($_menu), time() + (60 * 60 * 24));
Dries's avatar
 
Dries committed
217 218 219 220
    }

    // Make sure items that cannot be cached are added.
    _menu_append_contextual_items();
221 222 223

    // Reset the cached $menu in menu_get_item().
    menu_get_item(NULL, NULL, TRUE);
224
  }
225

Dries's avatar
 
Dries committed
226 227 228 229 230 231 232 233 234 235 236 237 238 239
  return $_menu;
}

/**
 * Return the local task tree.
 *
 * Unlike the rest of the menu structure, the local task tree cannot be cached
 * nor determined too early in the page request, because the user's current
 * location may be changed by a menu_set_location() call, and the tasks shown
 * (just as the breadcrumb trail) need to reflect the changed location.
 */
function menu_get_local_tasks() {
  global $_menu;

Dries's avatar
 
Dries committed
240 241 242 243 244 245
  // Don't cache the local task tree, as it varies by location and tasks are
  // allowed to be dynamically determined.
  if (!isset($_menu['local tasks'])) {
    // _menu_build_local_tasks() may indirectly call this function, so prevent
    // infinite loops.
    $_menu['local tasks'] = array();
Dries's avatar
 
Dries committed
246 247 248 249 250
    $pid = menu_get_active_nontask_item();
    if (!_menu_build_local_tasks($pid)) {
      // If the build returned FALSE, the tasks need not be displayed.
      $_menu['local tasks'][$pid]['children'] = array();
    }
Dries's avatar
 
Dries committed
251 252
  }

Dries's avatar
 
Dries committed
253
  return $_menu['local tasks'];
Dries's avatar
 
Dries committed
254
}
Dries's avatar
 
Dries committed
255

256 257 258 259 260 261 262
/**
 * Retrieves the menu item specified by $mid, or by $path if $mid is not given.
 *
 * @param $mid
 *   The menu ID of the menu item to retrieve.
 * @param $path
 *   The internal path of the menu item to retrieve. Defaults to NULL. Only
263
 *   used if $mid is not set.
264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306
 * @param $reset
 *   Optional flag that resets the static variable cache of the menu tree, if
 *   set to TRUE. Default is FALSE.
 *
 * @return
 *   The menu item found in the site menu, or an empty array if none could be
 *   found.
 */
function menu_get_item($mid, $path = NULL, $reset = FALSE) {
  static $menu;

  if (!isset($menu) || $reset) {
    $menu = menu_get_menu();
  }

  if (isset($mid)) {
    return $menu['items'][$mid];
  }

  if (isset($path)) {
    return $menu['items'][$menu['path index'][$path]];
  }

  return array();
}

/**
 * Retrieves the menu ID and title of all root menus.
 *
 * @return
 *   Array containing all menus (but not menu items), in the form mid => title.
 */
function menu_get_root_menus() {
  $menu = menu_get_menu();
  $root_menus = array();

  foreach ($menu['items'][0]['children'] as $mid) {
    $root_menus[$mid] = $menu['items'][$mid]['title'];
  }

  return $root_menus;
}

307
/**
Dries's avatar
 
Dries committed
308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324
 * Change the current menu location of the user.
 *
 * Frequently, modules may want to make a page or node act as if it were
 * in the menu tree somewhere, even though it was not registered in a
 * hook_menu() implementation. If the administrator has rearranged the menu,
 * the newly set location should respect this in the breadcrumb trail and
 * expanded/collapsed status of menu items in the tree. This function
 * allows this behavior.
 *
 * @param $location
 *   An array specifying a complete or partial breadcrumb trail for the
 *   new location, in the same format as the return value of hook_menu().
 *   The last element of this array should be the new location itself.
 *
 * This function will set the new breadcrumb trail to the passed-in value,
 * but if any elements of this trail are visible in the site tree, the
 * trail will be "spliced in" to the existing site navigation at that point.
325
 */
Dries's avatar
 
Dries committed
326 327 328 329 330
function menu_set_location($location) {
  global $_menu;
  $temp_id = min(array_keys($_menu['items'])) - 1;
  $prev_id = 0;

331 332 333 334
  // Don't allow this function to change the actual current path, just the
  // position in the menu tree.
  $location[count($location) - 1]['path'] = $_GET['q'];

Dries's avatar
 
Dries committed
335 336 337
  foreach (array_reverse($location) as $item) {
    if (isset($_menu['path index'][$item['path']])) {
      $mid = $_menu['path index'][$item['path']];
338
      if (isset($_menu['visible'][$mid])) {
Dries's avatar
 
Dries committed
339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361
        // Splice in the breadcrumb at this location.
        if ($prev_id) {
          $_menu['items'][$prev_id]['pid'] = $mid;
        }
        $prev_id = 0;
        break;
      }
      else {
        // A hidden item; show it, but only temporarily.
        $_menu['items'][$mid]['type'] |= MENU_VISIBLE_IN_BREADCRUMB;
        if ($prev_id) {
          $_menu['items'][$prev_id]['pid'] = $mid;
        }
        $prev_id = $mid;
      }
    }
    else {
      $item['type'] |= MENU_VISIBLE_IN_BREADCRUMB;
      if ($prev_id) {
        $_menu['items'][$prev_id]['pid'] = $temp_id;
      }
      $_menu['items'][$temp_id] = $item;
      $_menu['path index'][$item['path']] = $temp_id;
Dries's avatar
 
Dries committed
362

Dries's avatar
 
Dries committed
363 364 365 366
      $prev_id = $temp_id;
      $temp_id--;
    }
  }
Dries's avatar
 
Dries committed
367

Dries's avatar
 
Dries committed
368 369 370 371 372 373 374 375 376 377 378 379 380
  if ($prev_id) {
    // Didn't find a home, so attach this to the main navigation menu.
    $_menu['items'][$prev_id]['pid'] = 1;
  }

  $final_item = array_pop($location);
  menu_set_active_item($final_item['path']);
}

/**
 * Execute the handler associated with the active menu item.
 *
 * This is called early in the page request. The active menu item is at
381
 * this point determined exclusively by the URL. The handler that is called
Dries's avatar
 
Dries committed
382 383 384 385 386
 * here may, as a side effect, change the active menu item so that later
 * menu functions (that display the menus and breadcrumbs, for example)
 * act as if the user were in a different location on the site.
 */
function menu_execute_active_handler() {
387 388 389 390
  if (_menu_site_is_offline()) {
    return MENU_SITE_OFFLINE;
  }

Dries's avatar
 
Dries committed
391 392 393 394
  $menu = menu_get_menu();

  // Determine the menu item containing the callback.
  $path = $_GET['q'];
395
  while ($path && !isset($menu['callbacks'][$path])) {
Dries's avatar
 
Dries committed
396 397
    $path = substr($path, 0, strrpos($path, '/'));
  }
398

399
  if (!isset($menu['callbacks'][$path])) {
Dries's avatar
 
Dries committed
400
    return MENU_NOT_FOUND;
Dries's avatar
 
Dries committed
401 402
  }

403 404 405 406
  if (!function_exists($menu['callbacks'][$path]['callback'])) {
    return MENU_NOT_FOUND;
  }

Dries's avatar
 
Dries committed
407 408 409 410 411
  if (!_menu_item_is_accessible(menu_get_active_item())) {
    return MENU_ACCESS_DENIED;
  }

  // We found one, and are allowed to execute it.
412 413
  $arguments = isset($menu['callbacks'][$path]['callback arguments']) ? $menu['callbacks'][$path]['callback arguments'] : array();
  $arg = substr($_GET['q'], strlen($path) + 1);
Dries's avatar
 
Dries committed
414
  if (strlen($arg)) {
415
    $arguments = array_merge($arguments, explode('/', $arg));
Dries's avatar
 
Dries committed
416
  }
417

418
  return call_user_func_array($menu['callbacks'][$path]['callback'], $arguments);
Dries's avatar
 
Dries committed
419 420
}

Dries's avatar
 
Dries committed
421
/**
422
 * Returns the ID of the active menu item.
Dries's avatar
 
Dries committed
423 424
 */
function menu_get_active_item() {
Dries's avatar
 
Dries committed
425 426 427
  return menu_set_active_item();
}

Dries's avatar
 
Dries committed
428 429 430
/**
 * Sets the path of the active menu item.
 */
Dries's avatar
 
Dries committed
431
function menu_set_active_item($path = NULL) {
432
  static $stored_mid;
Dries's avatar
 
Dries committed
433

434 435
  if (!isset($stored_mid) || isset($path)) {
    if (!isset($path)) {
436
      $path = $_GET['q'];
Dries's avatar
 
Dries committed
437 438 439 440
    }
    else {
      $_GET['q'] = $path;
    }
441
    $menu = menu_get_menu();
Dries's avatar
 
Dries committed
442

443
    while ($path && !isset($menu['path index'][$path])) {
444
      $path = substr($path, 0, strrpos($path, '/'));
Dries's avatar
 
Dries committed
445
    }
446
    $stored_mid = isset($menu['path index'][$path]) ? $menu['path index'][$path] : 0;
Dries's avatar
 
Dries committed
447 448 449 450 451

    // Search for default local tasks to activate instead of this item.
    $continue = TRUE;
    while ($continue) {
      $continue = FALSE;
452
      if (isset($menu['items'][$stored_mid]['children'])) {
Dries's avatar
 
Dries committed
453 454 455 456 457 458 459 460
        foreach ($menu['items'][$stored_mid]['children'] as $cid) {
          if ($menu['items'][$cid]['type'] & MENU_LINKS_TO_PARENT) {
            $stored_mid = $cid;
            $continue = TRUE;
          }
        }
      }
    }
461 462 463

    // Reset the cached $menu in menu_get_item().
    menu_get_item(NULL, NULL, TRUE);
Dries's avatar
 
Dries committed
464 465
  }

466
  return $stored_mid;
Dries's avatar
 
Dries committed
467 468
}

Dries's avatar
 
Dries committed
469 470 471 472 473 474 475 476
/**
 * Returns the ID of the current menu item or, if the current item is a
 * local task, the menu item to which this task is attached.
 */
function menu_get_active_nontask_item() {
  $mid = menu_get_active_item();

  // Find the first non-task item:
477 478 479 480 481 482
  while ($mid) {
    $item = menu_get_item($mid);

    if (!($item['type'] & MENU_IS_LOCAL_TASK)) {
      return $mid;
    }
Dries's avatar
 
Dries committed
483

484
    $mid = $item['pid'];
Dries's avatar
 
Dries committed
485 486 487
  }
}

Dries's avatar
 
Dries committed
488
/**
Dries's avatar
 
Dries committed
489 490
 * Returns the title of the active menu item.
 */
491
function menu_get_active_title() {
Dries's avatar
 
Dries committed
492
  if ($mid = menu_get_active_nontask_item()) {
493 494
    $item = menu_get_item($mid);
    return $item['title'];
495 496
  }
}
Dries's avatar
 
Dries committed
497

Dries's avatar
 
Dries committed
498
/**
Dries's avatar
 
Dries committed
499 500
 * Returns the help associated with the active menu item.
 */
501
function menu_get_active_help() {
Dries's avatar
 
Dries committed
502 503
  $path = $_GET['q'];
  $output = '';
Dries's avatar
 
Dries committed
504

Dries's avatar
 
Dries committed
505 506 507 508
  if (!_menu_item_is_accessible(menu_get_active_item())) {
    // Don't return help text for areas the user cannot access.
    return;
  }
Dries's avatar
 
Dries committed
509

Dries's avatar
 
Dries committed
510 511 512 513 514
  foreach (module_list() as $name) {
    if (module_hook($name, 'help')) {
      if ($temp = module_invoke($name, 'help', $path)) {
        $output .= $temp . "\n";
      }
515
      if (module_hook('help', 'page')) {
516
        if (arg(0) == "admin") {
517 518
          if (module_invoke($name, 'help', 'admin/help#'. arg(2)) && !empty($output)) {
            $output .= theme("more_help_link", url('admin/help/'. arg(2)));
519
          }
Dries's avatar
 
Dries committed
520 521
        }
      }
Dries's avatar
 
Dries committed
522
    }
Dries's avatar
 
Dries committed
523
  }
Dries's avatar
 
Dries committed
524
  return $output;
525 526
}

Dries's avatar
 
Dries committed
527 528 529 530
/**
 * Returns an array of rendered menu items in the active breadcrumb trail.
 */
function menu_get_active_breadcrumb() {
531 532 533 534 535 536

  // No breadcrumb for the front page.
  if (drupal_is_front_page()) {
    return array();
  }

537 538
  // We do *not* want to use "variable_get('site_frontpage', 'node)" here
  // as that will create the link '/node'. This is unsightly and creates
539
  // a second URL for the homepage ('/' *and* '/node').
540
  $links[] = l(t('Home'), '');
541

Dries's avatar
 
Dries committed
542
  $trail = _menu_get_active_trail();
543
  foreach ($trail as $mid) {
544 545
    $item = menu_get_item($mid);
    if ($item['type'] & MENU_VISIBLE_IN_BREADCRUMB) {
546
      $links[] = menu_item_link($mid);
547
    }
Dries's avatar
 
Dries committed
548 549
  }

Dries's avatar
 
Dries committed
550 551 552
  // The last item in the trail is the page title; don't display it here.
  array_pop($links);

Dries's avatar
 
Dries committed
553
  return $links;
554 555
}

Dries's avatar
 
Dries committed
556
/**
557
 * Returns TRUE when the menu item is in the active trail.
Dries's avatar
 
Dries committed
558
 */
Dries's avatar
 
Dries committed
559
function menu_in_active_trail($mid) {
Dries's avatar
 
Dries committed
560
  $trail = _menu_get_active_trail();
Dries's avatar
 
Dries committed
561

Dries's avatar
 
Dries committed
562
  return in_array($mid, $trail);
Dries's avatar
 
Dries committed
563 564
}

565
/**
566
 * Returns TRUE when the menu item is in the active trail within a
567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583
 * specific subsection of the menu tree.
 *
 * @param $mid
 *   The menu item being considered.
 * @param $pid
 *   The root of the subsection of the menu tree in which to look.
 */
function menu_in_active_trail_in_submenu($mid, $pid) {
  $trail = _menu_get_active_trail_in_submenu($pid);

  if (!$trail) {
    return FALSE;
  }

  return in_array($mid, $trail);
}

584
/**
Dries's avatar
 
Dries committed
585 586 587
 * Populate the database representation of the menu.
 *
 * This need only be called at the start of pages that modify the menu.
588
 */
Dries's avatar
 
Dries committed
589
function menu_rebuild() {
Dries's avatar
 
Dries committed
590
  // Clear the page cache, so that changed menus are reflected for anonymous users.
591
  cache_clear_all('*', 'cache_page', TRUE);
Dries's avatar
 
Dries committed
592
  // Also clear the menu cache.
593
  cache_clear_all('*', 'cache_menu', TRUE);
Dries's avatar
 
Dries committed
594

595 596
  _menu_build();

597
  if (module_exists('menu')) {
Dries's avatar
 
Dries committed
598
    $menu = menu_get_menu();
Dries's avatar
 
Dries committed
599

600
    // Fill a queue of new menu items which are modifiable.
Dries's avatar
 
Dries committed
601 602 603
    $new_items = array();
    foreach ($menu['items'] as $mid => $item) {
      if ($mid < 0 && ($item['type'] & MENU_MODIFIABLE_BY_ADMIN)) {
604
        $new_items[$mid] = $item;
605 606 607
      }
    }

608
    $old_count = -1;
609
    // Save the new items updating the pids in each iteration
610 611
    while (($c = count($new_items)) && ($c != $old_count)) {
      $old_count = count($new_items);
612 613 614 615 616 617 618 619 620 621 622
      foreach($new_items as $mid => $item) {
        // If the item has a valid parent, save it
        if ($item['pid'] >= 0) {
          // The new menu ID gets passed back by reference as $item['mid']
          menu_save_item($item);
          // Fix parent IDs for the children of the menu item just saved
          if ($item['children']) {
            foreach ($item['children'] as $child) {
              if (isset($new_items[$child])) {
                $new_items[$child]['pid'] = $item['mid'];
              }
Dries's avatar
 
Dries committed
623
            }
Dries's avatar
 
Dries committed
624
          }
625 626
          // remove the item
          unset($new_items[$mid]);
Dries's avatar
 
Dries committed
627
        }
628
      }
Dries's avatar
 
Dries committed
629
    }
630 631
    // Rebuild the menu to account for the changes.
    _menu_build();
Dries's avatar
 
Dries committed
632
  }
633 634 635 636

  // Reset the cached $menu in menu_get_item().
  menu_get_item(NULL, NULL, TRUE);

Dries's avatar
 
Dries committed
637 638
}

Dries's avatar
 
Dries committed
639
/**
640 641 642 643
 * Generate the HTML for a menu tree.
 *
 * @param $pid
 *   The parent id of the menu.
Dries's avatar
 
Dries committed
644 645
 *
 * @ingroup themeable
Dries's avatar
 
Dries committed
646
 */
647 648
function theme_menu_tree($pid = 1) {
  if ($tree = menu_tree($pid)) {
649
    return "\n<ul class=\"menu\">\n". $tree ."\n</ul>\n";
650 651 652 653 654 655 656 657 658 659
  }
}

/**
 * Returns a rendered menu tree.
 *
 * @param $pid
 *   The parent id of the menu.
 */
function menu_tree($pid = 1) {
660 661
  $menu = menu_get_menu();
  $output = '';
Dries's avatar
 
Dries committed
662

663 664
  if (isset($menu['visible'][$pid]) && $menu['visible'][$pid]['children']) {
    foreach ($menu['visible'][$pid]['children'] as $mid) {
665 666 667
      $type = isset($menu['visible'][$mid]['type']) ? $menu['visible'][$mid]['type'] : NULL;
      $children = isset($menu['visible'][$mid]['children']) ? $menu['visible'][$mid]['children'] : NULL;
      $output .= theme('menu_item', $mid, menu_in_active_trail($mid) || ($type & MENU_EXPANDED) ? theme('menu_tree', $mid) : '', count($children) == 0);
Dries's avatar
 
Dries committed
668 669 670
    }
  }

671
  return $output;
Dries's avatar
 
Dries committed
672 673
}

Dries's avatar
 
Dries committed
674
/**
675
 * Generate the HTML output for a single menu item.
Dries's avatar
 
Dries committed
676 677
 *
 * @param $mid
678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694
 *   The menu id of the item.
 * @param $children
 *   A string containing any rendered child items of this menu.
 * @param $leaf
 *   A boolean indicating whether this menu item is a leaf.
 *
 * @ingroup themeable
 */
function theme_menu_item($mid, $children = '', $leaf = TRUE) {
  return '<li class="'. ($leaf ? 'leaf' : ($children ? 'expanded' : 'collapsed')) .'">'. menu_item_link($mid) . $children ."</li>\n";
}

/**
 * Generate the HTML representing a given menu item ID.
 *
 * @param $item
 *   The menu item to render.
695
 * @param $link_item
696
 *   The menu item which should be used to find the correct path.
Dries's avatar
 
Dries committed
697 698
 *
 * @ingroup themeable
Dries's avatar
 
Dries committed
699
 */
700
function theme_menu_item_link($item, $link_item) {
701
  return l($item['title'], $link_item['path'], !empty($item['description']) ? array('title' => $item['description']) : array(), isset($item['query']) ? $item['query'] : NULL);
702 703 704 705 706 707 708
}

/**
 * Returns the rendered link to a menu item.
 *
 * @param $mid
 *   The menu item id to render.
709 710
 * @param $theme
 *   Whether to return a themed link or the link as an array
711
 */
712
function menu_item_link($mid, $theme = TRUE) {
713 714
  $item = menu_get_item($mid);
  $link_item = $item;
715
  $link = '';
Dries's avatar
 
Dries committed
716

717 718
  while ($link_item['type'] & MENU_LINKS_TO_PARENT) {
    $link_item = menu_get_item($link_item['pid']);
Dries's avatar
 
Dries committed
719 720
  }

721 722 723 724 725
  if ($theme) {
    $link = theme('menu_item_link', $item, $link_item);
  }
  else {
    $link = array(
726 727
      'title' => $item['title'],
      'href' => $link_item['path'],
728
      'attributes' => !empty($item['description']) ? array('title' => $item['description']) : array()
729 730 731 732
    );
  }

  return $link;
Dries's avatar
 
Dries committed
733 734 735 736 737
}

/**
 * Returns the rendered local tasks. The default implementation renders
 * them as tabs.
Dries's avatar
 
Dries committed
738 739
 *
 * @ingroup themeable
Dries's avatar
 
Dries committed
740 741
 */
function theme_menu_local_tasks() {
742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757
  $output = '';

  if ($primary = menu_primary_local_tasks()) {
    $output .= "<ul class=\"tabs primary\">\n". $primary ."</ul>\n";
  }
  if ($secondary = menu_secondary_local_tasks()) {
    $output .= "<ul class=\"tabs secondary\">\n". $secondary ."</ul>\n";
  }

  return $output;
}

/**
 * Returns the rendered HTML of the primary local tasks.
 */
function menu_primary_local_tasks() {
Dries's avatar
 
Dries committed
758
  $local_tasks = menu_get_local_tasks();
Dries's avatar
 
Dries committed
759
  $pid = menu_get_active_nontask_item();
Dries's avatar
 
Dries committed
760
  $output = '';
Dries's avatar
 
Dries committed
761

Dries's avatar
 
Dries committed
762 763
  if (count($local_tasks[$pid]['children'])) {
    foreach ($local_tasks[$pid]['children'] as $mid) {
764
      $output .= theme('menu_local_task', $mid, menu_in_active_trail($mid), TRUE);
Dries's avatar
 
Dries committed
765
    }
766 767 768 769 770 771 772 773 774 775 776 777
  }

  return $output;
}

/**
 * Returns the rendered HTML of the secondary local tasks.
 */
function menu_secondary_local_tasks() {
  $local_tasks = menu_get_local_tasks();
  $pid = menu_get_active_nontask_item();
  $output = '';
Dries's avatar
 
Dries committed
778

779
  if (count($local_tasks[$pid]['children'])) {
Dries's avatar
 
Dries committed
780
    foreach ($local_tasks[$pid]['children'] as $mid) {
Dries's avatar
 
Dries committed
781
      if (menu_in_active_trail($mid) && count($local_tasks[$mid]['children']) > 1) {
Dries's avatar
 
Dries committed
782
        foreach ($local_tasks[$mid]['children'] as $cid) {
783
          $output .= theme('menu_local_task', $cid, menu_in_active_trail($cid), FALSE);
Dries's avatar
 
Dries committed
784 785 786 787
        }
      }
    }
  }
788

Dries's avatar
 
Dries committed
789 790 791 792
  return $output;
}

/**
Dries's avatar
 
Dries committed
793
 * Generate the HTML representing a given menu item ID as a tab.
Dries's avatar
 
Dries committed
794 795 796 797 798
 *
 * @param $mid
 *   The menu ID to render.
 * @param $active
 *   Whether this tab or a subtab is the active menu item.
799 800
 * @param $primary
 *   Whether this tab is a primary tab or a subtab.
Dries's avatar
 
Dries committed
801 802
 *
 * @ingroup themeable
Dries's avatar
 
Dries committed
803
 */
804
function theme_menu_local_task($mid, $active, $primary) {
Dries's avatar
 
Dries committed
805
  if ($active) {
806
    return '<li class="active">'. menu_item_link($mid) ."</li>\n";
Dries's avatar
 
Dries committed
807 808
  }
  else {
809
    return '<li>'. menu_item_link($mid) ."</li>\n";
Dries's avatar
 
Dries committed
810 811 812
  }
}

813 814 815 816 817 818 819 820 821 822 823 824 825
/**
 * Returns an array containing the primary links.
 * Can optionally descend from the root of the Primary links menu towards the
 * current node for a specified number of levels and return that submenu.
 * Used to generate a primary/secondary menu from different levels of one menu.
 *
 * @param $start_level
 *   This optional parameter can be used to retrieve a context-sensitive array
 *   of links at $start_level levels deep into the Primary links menu.
 *   The default is to return the top-level links.
 * @param $pid
 *   The parent menu ID from which to search for children. Defaults to the
 *   menu_primary_menu setting.
826
 * @return A nested array of links and their properties. The keys of
827 828 829 830 831 832 833
 *   the array contain some extra encoded information about the results.
 *   The format of the key is {level}-{num}{-active}.
 *   level is the depth within the menu tree of this list.
 *   num is the number within this array, used only to make the key unique.
 *   -active is appended if this element is in the active trail.
 */
function menu_primary_links($start_level = 1, $pid = 0) {
834
  if (!module_exists('menu')) {
835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858
    return NULL;
  }
  if (!$pid) {
    $pid = variable_get('menu_primary_menu', 0);
  }
  if (!$pid) {
    return NULL;
  }

  if ($start_level < 1) {
    $start_level = 1;
  }

  if ($start_level > 1) {
    $trail = _menu_get_active_trail_in_submenu($pid);
    if (!$trail) {
      return NULL;
    }
    else {
      $pid = $trail[$start_level - 1];
    }
  }

  $menu = menu_get_menu();
859
  $links = array();
860
  if ($pid && is_array($menu['visible'][$pid]) && isset($menu['visible'][$pid]['children'])) {
861 862
    $count = 1;
    foreach ($menu['visible'][$pid]['children'] as $cid) {
863
      $index = "menu-$start_level-$count-$pid";
864 865 866
      if (menu_in_active_trail_in_submenu($cid, $pid)) {
        $index .= "-active";
      }
867
      $links[$index] = menu_item_link($cid, FALSE);
868 869 870 871
      $count++;
    }
  }

872
  // Special case - provide link to admin/build/menu if primary links is empty.
873
  if (empty($links) && $start_level == 1 && $pid == variable_get('menu_primary_menu', 0) && user_access('administer menu')) {
874
    $links['1-1'] = array(
Steven Wittens's avatar
Steven Wittens committed
875
      'title' => t('Edit primary links'),
876
      'href' => 'admin/build/menu'
877
    );
878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920
  }

  return $links;
}

/**
 * Returns an array containing the secondary links.
 * Secondary links can be either a second level of the Primary links
 * menu or generated from their own menu.
 */
function menu_secondary_links() {
  $msm = variable_get('menu_secondary_menu', 0);
  if ($msm == 0) {
    return NULL;
  }

  if ($msm == variable_get('menu_primary_menu', 0)) {
    return menu_primary_links(2, $msm);
  }

  return menu_primary_links(1, $msm);
}

/**
 * Returns the themed HTML for primary and secondary links.
 * Note that this function is overridden by most core themes because
 * those themes display links in "link | link" format, not from a list.
 * Also note that by default links rendered with this function are
 * displayed with the same CSS as is used for the local tasks.
 * If a theme wishes to render links from a ul it is expected that
 * the theme will provide suitable CSS.
 *
 * @param $links
 *   An array containing links to render.
 * @return
 *   A string containing the themed links.
 *
 * @ingroup themeable
 */
function theme_menu_links($links) {
  if (!count($links)) {
    return '';
  }
921
  $level_tmp = explode('-', key($links));
922 923 924 925 926 927 928
  $level = $level_tmp[0];
  $output = "<ul class=\"links-$level\">\n";
  foreach ($links as $index => $link) {
    $output .= '<li';
    if (stristr($index, 'active')) {
      $output .= ' class="active"';
    }
929
    $output .= ">". l($link['title'], $link['href'], $link['attributes'], $link['query'], $link['fragment']) ."</li>\n";
930 931 932 933 934 935
  }
  $output .= '</ul>';

  return $output;
}

936
/**
Dries's avatar
 
Dries committed
937
 * @} End of "defgroup menu".
938
 */
Dries's avatar
 
Dries committed
939 940

/**
Dries's avatar
 
Dries committed
941
 * Returns an array with the menu items that lead to the current menu item.
Dries's avatar
 
Dries committed
942
 */
Dries's avatar
 
Dries committed
943 944
function _menu_get_active_trail() {
  static $trail;
Dries's avatar
 
Dries committed
945

Dries's avatar
 
Dries committed
946 947
  if (!isset($trail)) {
    $trail = array();
Dries's avatar
 
Dries committed
948

Dries's avatar
 
Dries committed
949
    $mid = menu_get_active_item();
Dries's avatar
 
Dries committed
950

Dries's avatar
 
Dries committed
951
    // Follow the parents up the chain to get the trail.
952
    while ($mid && ($item = menu_get_item($mid))) {
Dries's avatar
 
Dries committed
953
      array_unshift($trail, $mid);
954
      $mid = $item['pid'];
Dries's avatar
 
Dries committed
955 956
    }
  }
Dries's avatar
 
Dries committed
957 958 959
  return $trail;
}

960 961 962 963 964 965 966 967 968
/**
 * Find the active trail through a specific subsection of the menu tree.
 *
 * @param $pid
 *   The root item from which the active trail must descend.
 */
function _menu_get_active_trail_in_submenu($pid) {
  static $trails;

969
  if (!isset($trails)) {
970 971
    // Find all menu items which point to the current node and for each
    // follow the parents up the chain to build an active trail.
972
    $trails = array();
973 974 975 976 977
    $menu = menu_get_menu();
    $path = $_GET['q'];
    $count = 0;
    while ($path && !$count) {
      foreach ($menu['items'] as $key => $item) {
978
        if (isset($item['path']) && $item['path'] == $path) {
979 980 981 982 983 984
          $trails[$count] = array();
          $mid = $key;
          while ($mid && $menu['items'][$mid]) {
            array_unshift($trails[$count], $mid);
            $mid = $menu['items'][$mid]['pid'];
          }
985
          $count ++;
986 987 988 989 990 991 992
        }
      }
      $path = substr($path, 0, strrpos($path, '/'));
    }
  }

  if ($trails) {
993 994 995
    foreach ($trails as $trail) {
      $count_trail = count($trail);
      for ($i = 0; $i < $count_trail; $i++) {
996
        if ($trail[$i] == $pid) {
997 998 999 1000
          // Return a trail from $pid down to the current page inclusive.
          for ( ; $i < $count_trail; $i++) {
            $subtrail[] = $trail[$i];
          }
1001 1002 1003 1004 1005 1006 1007 1008 1009
          return $subtrail;
        }
      }
    }
  }

  return NULL;
}

Dries's avatar
 
Dries committed
1010 1011 1012 1013 1014 1015 1016 1017 1018
/**
 * Comparator routine for use in sorting menu items.
 */
function _menu_sort($a, $b) {
  $menu = menu_get_menu();

  $a = &$menu['items'][$a];
  $b = &$menu['items'][$b];

1019 1020 1021 1022 1023 1024
  if ($a['weight'] < $b['weight']) {
    return -1;
  }
  elseif ($a['weight'] > $b['weight']) {
    return 1;
  }
1025 1026
  elseif (isset($a['title']) && isset($b['title'])) {
    return strnatcasecmp($a['title'], $b['title']);
1027 1028 1029 1030
  }
  else {
    return 1;
  }
Dries's avatar
 
Dries committed
1031 1032
}

Dries's avatar
 
Dries committed
1033
/**
1034
 * Build the menu by querying both modules and the database.
Dries's avatar
 
Dries committed
1035
 */
Dries's avatar
 
Dries committed
1036
function _menu_build() {
1037 1038
  global $_menu;
  global $user;
Dries's avatar
 
Dries committed
1039

1040 1041 1042 1043 1044
  // Start from a clean slate.
  $_menu = array();

  $_menu['path index'] = array();
  // Set up items array, including default "Navigation" menu.
Dries's avatar
 
Dries committed
1045
  $_menu['items'] = array(
Dries's avatar
 
Dries committed
1046 1047
    0 => array('path' => '', 'title' => '', 'type' => MENU_IS_ROOT),
    1 => array('pid' => 0, 'path' => '', 'title' => t('Navigation'), 'weight' => -50, 'access' => TRUE, 'type' => MENU_IS_ROOT | MENU_VISIBLE_IN_TREE)
Dries's avatar
 
Dries committed
1048
    );