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

Dries's avatar
   
Dries committed
4
5
6
7
8
9
10
11
12
/**
 * @defgroup menu Menu system
 * @{
 */

define('MENU_SHOW', 0);
define('MENU_HIDE', 1);
define('MENU_HIDE_NOCHILD', 2);

13
14
15
16
17
define('MENU_NORMAL', 0);
define('MENU_MODIFIED', 1);
define('MENU_LOCKED', 2);
define('MENU_CUSTOM', 3);

Dries's avatar
   
Dries committed
18
19
20
21
define('MENU_FALLTHROUGH', 0);
define('MENU_DENIED', 1);
define('MENU_FOUND', 2);

Dries's avatar
   
Dries committed
22
/** @} */
Dries's avatar
 
Dries committed
23

24
/**
Dries's avatar
   
Dries committed
25
 * Register a menu item with the menu system.
Dries's avatar
   
Dries committed
26
27
 *
 * @ingroup menu
Dries's avatar
   
Dries committed
28
 * @param $path Location the menu item refers to. Do not add a trailing slash.
Dries's avatar
   
Dries committed
29
 * @param $title The title of the menu item to show in the rendered menu.
Dries's avatar
   
Dries committed
30
31
32
33
 * @param $callback
 * - string - The function to call when this is the active menu item.
 * - MENU_FALLTHROUGH - Use the callback defined by the menu item's parent.
 * - MENU_DENIED - Deny access to this menu item by this user.
Dries's avatar
   
Dries committed
34
 * @param $weight Heavier menu items sink down the menu.
35
36
37
38
39
40
41
 * @param $visibility
 * - MENU_SHOW - Show the menu item (default).
 * - MENU_HIDE - Hide the menu item, but register a callback.
 * - MENU_HIDE_NOCHILD - Hide the menu item when it has no children.
 * @param $status
 * - MENU_NORMAL - The menu item can be moved (default).
 * - MENU_LOCKED - The administrator may not modify the item.
42
 */
Dries's avatar
   
Dries committed
43
function menu($path, $title, $callback = MENU_FALLTHROUGH, $weight = 0, $visibility = MENU_SHOW, $status = MENU_NORMAL) {
44
  global $_menu;
Dries's avatar
   
Dries committed
45

46
  // add the menu to the flat list of menu items:
47
48
49
50
51
52
  $_menu['list'][$path] = array('title' => $title, 'callback' => $callback, 'weight' => $weight, 'visibility' => $visibility, 'status' => $status);
}

/**
 * Return the menu data structure.
 *
Dries's avatar
   
Dries committed
53
 * @ingroup menu
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
 * 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.
 * - 'path' - The Drupal path to the menu item. A link to a particular item
 *   can thus be constructed with l($item['title'], $item['path']).
 * - '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;
  global $user;

  if (!isset($_menu['items'])) {
79
    menu_build();
80
  }
81

82
  return $_menu;
Dries's avatar
   
Dries committed
83
}
Dries's avatar
 
Dries committed
84

85
/**
Dries's avatar
   
Dries committed
86
 * Returns an array with the menu items that lead to the specified path.
87
 */
Dries's avatar
   
Dries committed
88
function menu_get_trail($path) {
89
  $menu = menu_get_menu();
Dries's avatar
 
Dries committed
90

Dries's avatar
   
Dries committed
91
  $trail = array();
Dries's avatar
   
Dries committed
92

Dries's avatar
   
Dries committed
93
94
95
96
97
98
99
  // Find the ID of the given path.
  while ($path && !$menu['path index'][$path]) {
    $path = substr($path, 0, strrpos($path, '/'));
  }
  $mid = $menu['path index'][$path];

  // Follow the parents up the chain to get the trail.
100
101
102
  while ($mid && $menu['items'][$mid]) {
    array_unshift($trail, $mid);
    $mid = $menu['items'][$mid]['pid'];
Dries's avatar
   
Dries committed
103
104
  }

Dries's avatar
   
Dries committed
105
  return $trail;
Dries's avatar
   
Dries committed
106
107
}

Dries's avatar
   
Dries committed
108
/**
109
 * Returns the ID of the active menu item.
Dries's avatar
   
Dries committed
110
 * @ingroup menu
Dries's avatar
   
Dries committed
111
112
 */
function menu_get_active_item() {
Dries's avatar
   
Dries committed
113
114
115
  return menu_set_active_item();
}

Dries's avatar
   
Dries committed
116
117
118
119
/**
 * Sets the path of the active menu item.
 * @ingroup menu
 */
Dries's avatar
   
Dries committed
120
function menu_set_active_item($path = NULL) {
121
122
  static $stored_mid;
  $menu = menu_get_menu();
Dries's avatar
 
Dries committed
123

124
  if (is_null($stored_mid) || !empty($path)) {
Dries's avatar
   
Dries committed
125
    if (empty($path)) {
126
      $path = $_GET['q'];
Dries's avatar
   
Dries committed
127
128
129
130
    }
    else {
      $_GET['q'] = $path;
    }
Dries's avatar
 
Dries committed
131

132
133
    while ($path && !$menu['path index'][$path]) {
      $path = substr($path, 0, strrpos($path, '/'));
Dries's avatar
   
Dries committed
134
    }
135
    $stored_mid = $menu['path index'][$path];
Dries's avatar
 
Dries committed
136
137
  }

138
  return $stored_mid;
Dries's avatar
 
Dries committed
139
140
}

Dries's avatar
   
Dries committed
141
/**
Dries's avatar
   
Dries committed
142
143
 * Returns the title of the active menu item.
 */
144
function menu_get_active_title() {
145
  $menu = menu_get_menu();
Dries's avatar
 
Dries committed
146

147
148
  if ($mid = menu_get_active_item()) {
    return ucfirst($menu['items'][$mid]['title']);
149
150
  }
}
Dries's avatar
 
Dries committed
151

Dries's avatar
   
Dries committed
152
/**
Dries's avatar
   
Dries committed
153
154
 * Returns the help associated with the active menu item.
 */
155
function menu_get_active_help() {
Dries's avatar
 
Dries committed
156

Dries's avatar
   
Dries committed
157
  if (menu_active_handler_exists()) {
158
159
    $path = $_GET['q'];
    $output = '';
Dries's avatar
   
Dries committed
160

161
    $return = module_invoke_all('help', $path);
Dries's avatar
   
Dries committed
162
163
164
165
    foreach ($return as $item) {
      if (!empty($item)) {
        $output .= $item ."\n";
      }
Dries's avatar
   
Dries committed
166
    }
Dries's avatar
   
Dries committed
167
    return $output;
Dries's avatar
 
Dries committed
168
  }
169
170
}

Dries's avatar
   
Dries committed
171
172
173
174
/**
 * Returns an array of rendered menu items in the active breadcrumb trail.
 */
function menu_get_active_breadcrumb() {
175
  $menu = menu_get_menu();
Dries's avatar
 
Dries committed
176

177
  $links[] = l(t('Home'), '');
178

179
  $trail = menu_get_trail(drupal_get_path_alias($_GET['q']));
Dries's avatar
   
Dries committed
180
181
182
183

  // The last item in the trail is the page title; don't display it here.
  array_pop($trail);

184
  foreach ($trail as $mid) {
Dries's avatar
   
Dries committed
185
186
    // Don't show hidden menu items or items without valid link targets.
    if (isset($menu['visible'][$mid]) && $menu['items'][$mid]['path'] != '') {
187
188
      $links[] = _menu_render_item($mid);
    }
Dries's avatar
 
Dries committed
189
190
  }

Dries's avatar
   
Dries committed
191
  return $links;
192
193
}

Dries's avatar
   
Dries committed
194
195
196
197
/**
 * Execute the handler associated with the active menu item.
 */
function menu_execute_active_handler() {
198
  $menu = menu_get_menu();
199

200
  $path = $_GET['q'];
Dries's avatar
   
Dries committed
201
  while ($path && (!$menu['path index'][$path] || $menu['items'][$menu['path index'][$path]]['callback'] === MENU_FALLTHROUGH)) {
202
203
204
    $path = substr($path, 0, strrpos($path, '/'));
  }
  $mid = $menu['path index'][$path];
Dries's avatar
   
Dries committed
205
206
207
  if ($menu['items'][$mid]['callback'] === MENU_DENIED) {
    return MENU_DENIED;
  }
208

Dries's avatar
   
Dries committed
209
  if (is_string($menu['items'][$mid]['callback'])) {
210
    $arg = substr($_GET['q'], strlen($menu['items'][$mid]['path']) + 1);
Dries's avatar
   
Dries committed
211
    if (strlen($arg)) {
Dries's avatar
   
Dries committed
212
      call_user_func_array($menu['items'][$mid]['callback'], explode('/', $arg));
Dries's avatar
   
Dries committed
213
214
    }
    else {
Dries's avatar
   
Dries committed
215
      call_user_func($menu['items'][$mid]['callback']);
Dries's avatar
   
Dries committed
216
    }
Dries's avatar
   
Dries committed
217
    return MENU_FOUND;
Dries's avatar
   
Dries committed
218
  }
Dries's avatar
   
Dries committed
219
220

  return MENU_FALLTHROUGH;
Dries's avatar
 
Dries committed
221
222
}

223
224
225
/**
 * Return true if a valid callback can be called from the current path.
 */
Dries's avatar
   
Dries committed
226
function menu_active_handler_exists() {
227
  $menu = menu_get_menu();
Dries's avatar
   
Dries committed
228

229
  $path = $_GET['q'];
Dries's avatar
   
Dries committed
230
  while ($path && (!$menu['path index'][$path] || $menu['items'][$menu['path index'][$path]]['callback'] === MENU_FALLTHROUGH)) {
231
232
233
    $path = substr($path, 0, strrpos($path, '/'));
  }
  $mid = $menu['path index'][$path];
Dries's avatar
   
Dries committed
234

Dries's avatar
   
Dries committed
235
236
237
238
239
240
241
  if ($menu['items'][$mid]['callback'] === MENU_FALLTHROUGH) {
    return FALSE;
  }
  if ($menu['items'][$mid]['callback'] === MENU_DENIED) {
    return FALSE;
  }

242
  return function_exists($menu['items'][$mid]['callback']);
Dries's avatar
   
Dries committed
243
244
}

Dries's avatar
   
Dries committed
245
246
247
/**
 * Returns true when the path is in the active trail.
 */
248
function menu_in_active_trail($mid) {
Dries's avatar
   
Dries committed
249
  static $trail;
250

Dries's avatar
   
Dries committed
251
  if (empty($trail)) {
252
    $trail = menu_get_trail(drupal_get_path_alias($_GET['q']));
Dries's avatar
   
Dries committed
253
  }
254

255
  return in_array($mid, $trail);
Dries's avatar
   
Dries committed
256
}
Dries's avatar
 
Dries committed
257

Dries's avatar
   
Dries committed
258
/**
259
 * Returns a rendered menu tree.
Dries's avatar
   
Dries committed
260
 */
261
262
263
264
function menu_tree($pid = 1) {
  static $trail;
  $menu = menu_get_menu();
  $output = '';
Dries's avatar
   
Dries committed
265

266
267
268
269
270
271
272
273
274
275
276
277
  if (empty($trail)) {
    $trail = menu_get_trail($_GET['q']);
  }

  if (isset($menu['visible'][$pid]) && $menu['visible'][$pid]['children']) {

    foreach ($menu['visible'][$pid]['children'] as $mid) {
      $style = (count($menu['visible'][$mid]['children']) ? (menu_in_active_trail($mid)  ? 'expanded' : 'collapsed') : 'leaf');
      $output .= "<li class=\"$style\">";
      $output .= _menu_render_item($mid);
      if (menu_in_active_trail($mid)) {
        $output .= menu_tree($mid);
Dries's avatar
   
Dries committed
278
      }
279
280
281
282
283
      $output .= "</li>\n";
    }

    if ($output != '') {
      $output  = "\n<ul>\n$output\n</ul>\n";
Dries's avatar
   
Dries committed
284
285
286
    }
  }

287
  return $output;
Dries's avatar
   
Dries committed
288
289
}

Dries's avatar
   
Dries committed
290
/**
291
 * Build the menu by querying both modules and the database.
Dries's avatar
   
Dries committed
292
 */
293
294
295
function menu_build() {
  global $_menu;
  global $user;
Dries's avatar
   
Dries committed
296

297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
  // Start from a clean slate.
  $_menu = array();

  // Build a sequential list of all menu items.
  module_invoke_all('link', 'system');

  $_menu['path index'] = array();
  // Set up items array, including default "Navigation" menu.
  $_menu['items'] = array(0 => array(), 1 => array('pid' => 0, 'title' => t('Navigation'), 'weight' => -50, 'visibility' => MENU_SHOW, 'status' => MENU_LOCKED));

  // Menu items not in the DB get temporary negative IDs.
  $temp_mid = -1;

  foreach ($_menu['list'] as $path => $data) {
    $mid = $temp_mid;
    $_menu['items'][$mid] = array('path' => $path, 'title' => $data['title'], 'callback' => $data['callback'], 'weight' => $data['weight'], 'visibility' => $data['visibility'], 'status' => $data['status']);
    $_menu['path index'][$path] = $mid;

    $temp_mid--;
316
317
  }

318
319
320
321
322
323
  // Now fetch items from the DB, reassigning menu IDs as needed.
  if (module_exist('menu')) {
    $result = db_query('SELECT * FROM {menu}');
    while ($item = db_fetch_object($result)) {
      // First, add any custom items added by the administrator.
      if ($item->status == MENU_CUSTOM) {
Dries's avatar
   
Dries committed
324
        $_menu['items'][$item->mid] = array('pid' => $item->pid, 'path' => $item->path, 'title' => $item->title, 'callback' => MENU_FALLTHROUGH, 'weight' => $item->weight, 'visibility' => MENU_SHOW, 'status' => MENU_CUSTOM);
325
326
327
328
329
330
331
332
333
334
335
336
337
338
        $_menu['path index'][$item->path] = $item->mid;
      }
      // Don't display non-custom menu items if no module declared them.
      else if ($old_mid = $_menu['path index'][$item->path]) {
        $_menu['items'][$item->mid] = $_menu['items'][$old_mid];
        unset($_menu['items'][$old_mid]);
        $_menu['path index'][$item->path] = $item->mid;
        // If administrator has changed item position, reflect the change.
        if ($item->status == MENU_MODIFIED) {
          $_menu['items'][$item->mid]['title'] = $item->title;
          $_menu['items'][$item->mid]['pid'] = $item->pid;
          $_menu['items'][$item->mid]['weight'] = $item->weight;
          $_menu['items'][$item->mid]['visibility'] = $item->visibility;
          $_menu['items'][$item->mid]['status'] = $item->status;
Dries's avatar
   
Dries committed
339
340
        }
      }
341
342
343
344
345
346
347
348
349
350
    }
  }

  // Establish parent-child relationships.
  foreach ($_menu['items'] as $mid => $item) {
    if (!isset($item['pid'])) {
      // Parent's location has not been customized, so figure it out using the path.
      $parent = $item['path'];
      do {
        $parent = substr($parent, 0, strrpos($parent, '/'));
Dries's avatar
   
Dries committed
351
      }
352
353
354
355
356
357
358
      while ($parent && !$_menu['path index'][$parent]);

      $pid = $parent ? $_menu['path index'][$parent] : 1;
      $_menu['items'][$mid]['pid'] = $pid;
    }
    else {
      $pid = $item['pid'];
Dries's avatar
   
Dries committed
359
    }
360

361
362
363
364
365
366
367
368
369
370
    // Don't make root a child of itself.
    if ($mid) {
      if (isset ($_menu['items'][$pid])) {
        $_menu['items'][$pid]['children'][] = $mid;
      }
      else {
        // If parent is missing, it is a menu item that used to be defined
        // but is no longer. Default to a root-level "Navigation" menu item.
        $_menu['items'][1]['children'][] = $mid;
      }
371
    }
Dries's avatar
 
Dries committed
372
373
  }

374
375
  // Prepare to display trees to the user as required.
  menu_build_visible_tree();
Dries's avatar
 
Dries committed
376
377
}

Dries's avatar
   
Dries committed
378
/**
379
380
381
382
 * Find all visible items in the menu tree, for ease in displaying to user.
 *
 * Since this is only for display, we only need title, path, and children
 * for each item.
Dries's avatar
   
Dries committed
383
 */
384
385
function menu_build_visible_tree($pid = 0) {
  global $_menu;
Dries's avatar
   
Dries committed
386

387
388
  if (isset($_menu['items'][$pid])) {
    $parent = $_menu['items'][$pid];
389

390
391
392
393
394
395
396
    $children = array();
    if ($parent['children']) {
      usort($parent['children'], '_menu_sort');
      foreach ($parent['children'] as $mid) {
        $children = array_merge($children, menu_build_visible_tree($mid));
      }
    }
Dries's avatar
   
Dries committed
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
    $visible = ($parent['visibility'] == MENU_SHOW) ||
      ($parent['visibility'] == MENU_HIDE_NOCHILD && count($children) > 0);

    if ($parent['callback'] === MENU_FALLTHROUGH) {
      // Follow the path up to find the actual callback.
      $path = $parent['path'];
      while ($path && (!$_menu['path index'][$path] || $_menu['items'][$_menu['path index'][$path]]['callback'] === MENU_FALLTHROUGH)) {
        $path = substr($path, 0, strrpos($path, '/'));
      }
      $callback_mid = $_menu['path index'][$path];
      $allowed = $_menu['items'][$callback_mid]['callback'] !== MENU_DENIED;
    }
    else {
      $allowed = $parent['callback'] !== MENU_DENIED;
    }

    if ($visible && $allowed) {
414
      $_menu['visible'][$pid] = array('title' => $parent['title'], 'path' => $parent['path'], 'children' => $children);
Dries's avatar
   
Dries committed
415
416
417
      foreach ($children as $mid) {
        $_menu['visible'][$mid]['pid'] = $pid;
      }
418
419
420
421
422
423
      return array($pid);
    }
    else {
      return $children;
    }
  }
424

425
426
  return array();
}
427

428
429
430
/**
 * Populate the database representation of the menu.
 *
Dries's avatar
   
Dries committed
431
 * @ingroup menu
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
 * This need only be called at the start of pages that modify the menu.
 */
function menu_rebuild() {
  cache_clear_all();
  menu_build();
  $menu = menu_get_menu();

  $new_items = array();
  foreach ($menu['items'] as $mid => $item) {
    if ($mid < 0 && ($item->status != MENU_LOCKED)) {
      $new_mid = db_next_id('menu_mid');
      if (isset($new_items[$item['pid']])) {
        $new_pid = $new_items[$item['pid']]['mid'];
      }
      else {
        $new_pid = $item['pid'];
      }
449

450
451
452
453
454
455
456
457
      // Fix parent IDs for menu items already added.
      if ($item['children']) {
        foreach ($item['children'] as $child) {
          if (isset($new_items[$child])) {
            $new_items[$child]['pid'] = $new_mid;
          }
        }
      }
458

459
      $new_items[$mid] = array('mid' => $new_mid, 'pid' => $new_pid, 'path' => $item['path'], 'title' => $item['title'], 'weight' => $item['weight'], 'visibility' => $item['visibility'], 'status' => $item['status']);
460
461
    }
  }
462
463
464
465
466
467
468

  foreach ($new_items as $item) {
    db_query('INSERT INTO {menu} (mid, pid, path, title, weight, visibility, status) VALUES (%d, %d, \'%s\', \'%s\', %d, %d, %d)', $item['mid'], $item['pid'], $item['path'], $item['title'], $item['weight'], $item['visibility'], $item['status']);
  }

  // Rebuild the menu to account for any changes.
  menu_build();
Dries's avatar
   
Dries committed
469
470
}

471
472
473
/**
 * Comparator routine for use in sorting menu items.
 */
Dries's avatar
   
Dries committed
474
function _menu_sort($a, $b) {
475
  $menu = menu_get_menu();
Dries's avatar
   
Dries committed
476

477
478
  $a = &$menu['items'][$a];
  $b = &$menu['items'][$b];
Dries's avatar
   
Dries committed
479

480
  return $a['weight'] < $b['weight'] ? -1 : ($a['weight'] > $b['weight'] ? 1 : ($a['title'] < $b['title'] ? -1 : 1));
Dries's avatar
   
Dries committed
481
482
}

483
484
function _menu_render_item($mid) {
  $menu = menu_get_menu();
Dries's avatar
   
Dries committed
485

486
  return l($menu['items'][$mid]['title'], $menu['items'][$mid]['path']);
Dries's avatar
   
Dries committed
487
488
489
}


Dries's avatar
   
Dries committed
490
?>