menu.module 35.7 KB
Newer Older
1
2
3
<?php
// $Id$

Dries's avatar
   
Dries committed
4
5
6
7
8
/**
 * @file
 * Allows administrators to customize the site navigation menu.
 */

9
10
11
/**
 * Implementation of hook_help().
 */
12
13
function menu_help($path, $arg) {
  switch ($path) {
14
    case 'admin/help#menu':
15
      $output = t('<p>Menus are a collection of links (menu items) used to navigate a website. The menu module provides an interface to control and customize the powerful menu system that comes with Drupal. Menus are primarily displayed as a hierarchical list of links using Drupal\'s highly flexible <a href="@admin-block">blocks</a> feature. Each menu automatically creates a block of the same name. By default, new menu items are placed inside a built-in menu labelled %navigation, but administrators can also create custom menus.</p>
16
<p>Drupal themes generally provide out-of-the-box support for two menus commonly labelled %primary-links and %secondary-links. These are sets of links which are usually displayed in the header or footer of each page (depending on the currently active theme).</p>
17
Menu administration tabs:
18
<ul>
19
20
21
  <li>On the administer menu page, administrators can "edit" to change the title, description, parent or weight of a menu item. Under the "operations" column, click on "enable/disable" to toggle a menu item on or off. Only menu items which are enabled are displayed in the corresponding menu block. Note that the default menu items generated by the menu module cannot be deleted, only disabled.</li>
  <li>Use the "add menu" tab to submit a title for a new custom menu. Once submitted, the menu will appear in a list toward the bottom of the administer menu page underneath the main navigation menu. Under the menu name there will be links to edit or delete the menu, and a link to add new items to the menu.</li>
  <li>Use the "add menu item" tab to create new links in either the navigation or a custom menu (such as a primary/secondary links menu). Select the parent item to place the new link within an existing menu structure. For top level menu items, choose the name of the menu in which the link is to be added.</li>
22
</ul>', array('%navigation' => 'Navigation', '%primary-links' => 'Primary links', '%secondary-links' => 'Secondary links', '@admin-block' => url('admin/build/block'), '@menu-settings' => url('admin/build/menu/settings')));
23
      $output .= '<p>'. t('For more information please read the configuration and customization handbook <a href="@menu">Menu page</a>.', array('@menu' => 'http://drupal.org/handbook/modules/menu/')) .'</p>';
24
      return $output;
25
    case 'admin/build/menu':
26
      return '<p>'. t('Menus are a collection of links (menu items) used to navigate a website. The list(s) below display the currently available menus along with their menu items. Select an operation from the list to manage each menu or menu item.', array('@admin-settings-menus' => url('admin/build/menu/settings'), '@admin-block' => url('admin/build/block'))) .'</p>';
27
    case 'admin/build/menu/add':
28
      return '<p>'. t('Enter the name for your new menu. Remember to enable the newly created block in the <a href="@blocks">blocks administration page</a>.', array('@blocks' => url('admin/build/block'))) .'</p>';
29
    case 'admin/build/menu/item/add':
30
      return '<p>'. t('Enter the title, path, position and the weight for your new menu item.') .'</p>';
31
32
33
  }
}

34
35
36
37
38
39
40
/**
 * Implementation of hook_perm().
 */
function menu_perm() {
  return array('administer menu');
}

41
/**
42
 * Implementation of hook_menu().
43
 */
44
function menu_menu() {
45
  $items['admin/build/menu'] = array(
46
47
    'title' => 'Menus',
    'description' => "Control your site's navigation menu, primary links and secondary links. as well as rename and reorganize menu items.",
48
    'page callback' => 'menu_overview_page',
49
50
51
    'access callback' => 'user_access',
    'access arguments' => array('administer menu'),
  );
52

53
  $items['admin/build/menu/list'] = array(
54
    'title' => 'List menus',
55
    'type' => MENU_DEFAULT_LOCAL_TASK,
56
57
    'weight' => -10);
  $items['admin/build/menu/add'] = array(
58
59
60
61
    'title' => 'Add menu',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('menu_edit_menu', 'add'),
    'type' => MENU_LOCAL_TASK);
62
63
64
65
66
67
68
69
70
71
72
  $items['admin/build/menu/settings'] = array(
    'title' => 'Settings',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('menu_configure'),
    'type' => MENU_LOCAL_TASK,
    'weight' => 5);

  $items['admin/build/menu-customize/%menu'] = array(
    'title' => 'Customize menu',
    'page callback' => 'menu_overview',
    'page arguments' => array(3),
73
74
    'title callback' => 'menu_overview_title',
    'title arguments' => array(3),
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
    'access arguments' => array('administer menu'),
    'type' => MENU_CALLBACK);
  $items['admin/build/menu-customize/%menu/list'] = array(
    'title' => 'List items',
    'weight' => -10,
    'type' => MENU_DEFAULT_LOCAL_TASK);
  $items['admin/build/menu-customize/%menu/add'] = array(
    'title' => 'Add item',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('menu_edit_item', 'add', NULL, 3),
    'type' => MENU_LOCAL_TASK);
  $items['admin/build/menu-customize/%menu/edit'] = array(
    'title' => 'Edit menu',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('menu_edit_menu', 'edit', 3),
    'type' => MENU_LOCAL_TASK);
91
92
93
94
95
  $items['admin/build/menu-customize/%menu/delete'] = array(
    'title' => 'Delete menu',
    'page callback' => 'menu_delete_menu_page',
    'page arguments' => array(3),
    'type' => MENU_CALLBACK);
96
  $items['admin/build/menu/item/%menu_link/disable'] = array(
97
    'title' => 'Disable menu item',
98
    'page callback' => 'menu_flip_item',
99
100
101
    'page arguments' => array(TRUE, 4),
    'type' => MENU_CALLBACK);
  $items['admin/build/menu/item/%menu_link/enable'] = array(
102
    'title' => 'Enable menu item',
103
    'page callback' => 'menu_flip_item',
104
105
106
    'page arguments' => array(FALSE, 4),
    'type' => MENU_CALLBACK);
  $items['admin/build/menu/item/%menu_link/edit'] = array(
107
    'title' => 'Edit menu item',
108
    'page callback' => 'drupal_get_form',
109
    'page arguments' => array('menu_edit_item', 'edit', 4, NULL),
110
    'type' => MENU_CALLBACK);
111
  $items['admin/build/menu/item/%menu_link/reset'] = array(
112
    'title' => 'Reset menu item',
113
    'page callback' => 'drupal_get_form',
114
    'page arguments' => array('menu_reset_item_confirm', 4),
115
    'type' => MENU_CALLBACK);
116
  $items['admin/build/menu/item/%menu_link/delete'] = array(
117
    'title' => 'Delete menu item',
118
119
    'page callback' => 'menu_item_delete_page',
    'page arguments' => array(4),
120
121
    'type' => MENU_CALLBACK);

122
  return $items;
123
}
124

125
126
127
128
129
130
131
132
133
/**
 * Implementation of hook_enable()
 *
 *  Add a link for each custom menu.
 */
function menu_enable() {
  menu_rebuild();
  $result = db_query("SELECT * FROM {menu_custom}");
  $link['module'] = 'menu';
134
  $link['plid'] = db_result(db_query("SELECT mlid FROM {menu_links} WHERE menu_name = '%s' AND link_path = '%s'", 'navigation', 'admin/build/menu'));
135
136
137
138
139
140
141
142
  $link['router_path'] = 'admin/build/menu-customize/%';

  while ($menu = db_fetch_array($result)) {
    $link['mlid'] = 0;
    $link['link_title'] = $menu['title'];
    $link['link_path'] = 'admin/build/menu-customize/'. $menu['menu_name'];
    menu_link_save($link);
  }
143
  menu_cache_clear_all();
144
145
}

146
147
148
149
150
151
152
/**
 * Title callback for the menu overview page and links.
 */
function menu_overview_title($menu) {
  return t('!menu_title (overview)', array('!menu_title' => $menu['title']));
}

153
154
155
156
157
158
159
160
161
162
163
/**
 * Load the data for a single custom menu.
 */
function menu_load($menu_name) {
  return db_fetch_array(db_query("SELECT * FROM {menu_custom} WHERE menu_name = '%s'", $menu_name));
}

/**
 * Menu callback which shows an overview page of all the custom menus and their descriptions.
 */
function menu_overview_page() {
164
  $result = db_query("SELECT * FROM {menu_custom} ORDER BY title");
165
166
167
168
169
170
171
172
  $content = array();
  while ($menu = db_fetch_array($result)) {
    $menu['href'] = 'admin/build/menu-customize/'. $menu['menu_name'];
    $menu['options'] = array();
    $content[] = $menu;
  }
  return theme('admin_block_content', $content);
}
173

174
/**
175
 * Shows for one menu the menu items accessible to the current user and relevant operations.
176
 */
177
178
function menu_overview($menu) {

179
  $header = array(t('Menu item'), t('Expanded'), array('data' => t('Operations'), 'colspan' => '3'));
180
  $sql ="
181
    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, ml.*
182
    FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path
183
184
185
    WHERE ml.menu_name = '%s'
    ORDER BY p1 ASC, p2 ASC, p3 ASC, p4 ASC, p5 ASC, p6 ASC, p7 ASC, p8 ASC, p9 ASC";
  $sql_count = "SELECT COUNT(*) FROM {menu_links} ml WHERE menu_name = '%s'";
186
187
  $result = pager_query($sql, 200, 0, $sql_count, $menu['menu_name']);
  $tree = menu_tree_data($result);
188
189
  $node_links = array();
  menu_tree_collect_node_links($tree, $node_links);
190
  menu_tree_check_access($tree, $node_links);
191
192
193
194
195
  $rows = _menu_overview_tree($tree);
  $output = theme('table', $header, $rows);
  $output .= theme('pager', NULL, 200, 0);
  return $output;
}
196

197
198
199
/**
 * Recursive helper function for menu_overview().
 */
200
201
202
203
function _menu_overview_tree($tree) {
  static $rows = array();
  foreach ($tree as $data) {
    $title = '';
204
205
206
    $item = $data['link'];
    // Don't show callbacks; these have $item['hidden'] < 0.
    if ($item && $item['hidden'] >= 0) {
207
      $title = str_repeat('&nbsp;&nbsp;', $item['depth'] - 1) . ($item['depth'] > 1 ? '-&nbsp;' : '');
208
      $title .= l($item['title'], $item['href'], $item['options']);
209
210
211
      // Populate the operations field.
      $operations = array();
      // Set the edit column.
212
      $operations[] = array('data' => l(t('edit'), 'admin/build/menu/item/'. $item['mlid'] .'/edit'));
213
214
215
      if ($item['hidden']) {
        $title .= ' ('. t('disabled') .')';
        $class = 'menu-disabled';
216
        $operations[] = array('data' => l(t('enable'), 'admin/build/menu/item/'. $item['mlid'] .'/enable'));
217
218
219
      }
      else {
        $class = 'menu-enabled';
220
        $operations[] = array('data' => l(t('disable'), 'admin/build/menu/item/'. $item['mlid'] .'/disable'));
221
222
223
      }
      // Only items created by the menu module can be deleted.
      if ($item['module'] == 'menu') {
224
        $operations[] = array('data' => l(t('delete'), 'admin/build/menu/item/'. $item['mlid'] .'/delete'));
225
226
      }
      // Set the reset column.
227
228
      elseif ($item['module'] == 'system' && $item['customized']) {
        $operations[] = array('data' => l(t('reset'), 'admin/build/menu/item/'. $item['mlid'] .'/reset'));
229
230
231
232
233
234
235
236
237
238
239
240
241
      }
      else {
        $operations[] = array('data' => '');
      }
      $row = array(
        array('data' => $title, 'class' => $class),
        array('data' => ($item['has_children'] ? (($item['expanded']) ? t('Yes') : t('No')) : ''), 'class' => $class),
      );
      foreach ($operations as $operation) {
        $operation['class'] = $class;
        $row[] = $operation;
      }
      $rows[] = $row;
242
    }
243
244
    if ($data['below']) {
      _menu_overview_tree($data['below']);
245
246
    }
  }
247
  return $rows;
248
}
249

250
/**
251
 * Menu callback; enable/disable a menu link.
252
 *
253
254
 * @param $hide
 *   TRUE to not show in the menu tree. FALSE to make the item and its children
255
 *   reappear in menu tree.
256
257
 * @param $item
 *    The menu item.
258
 */
259
260
function menu_flip_item($hide, $item) {

261
  $item['hidden'] = (bool)$hide;
262
  $item['customized'] = 1;
263
264
  menu_link_save($item);
  drupal_set_message($hide ? t('The menu item has been disabled.') : t('The menu item has been enabled.'));
265
  drupal_goto('admin/build/menu-customize/'. $item['menu_name']);
266
267
268
}

/**
269
 * Menu callback; Build the menu link editing form.
270
 */
271
272
function menu_edit_item(&$form_state, $type, $item, $menu) {

273
274
275
276
277
278
279
280
  $form['menu'] = array(
    '#type' => 'fieldset',
    '#title' => t('Menu settings'),
    '#collapsible' => FALSE,
    '#tree' => TRUE,
    '#weight' => -2,
    '#attributes' => array('class' => 'menu-item-form'),
  );
281
282
283
  if ($type == 'add' || empty($item)) {
    // This is an add form, initialize the menu link.
    $item = array('link_title' => '', 'mlid' => 0, 'plid' => 0, 'menu_name' => $menu['menu_name'], 'weight' => 0,  'link_path' => '', 'options' => array(), 'module' => 'menu', 'expanded' => 0, 'hidden' => 0, 'has_children' => 0);
284
  }
285
  foreach (array('link_path', 'mlid', 'module', 'hidden', 'has_children', 'options') as $key) {
286
    $form['menu'][$key] = array('#type' => 'value', '#value' => $item[$key]);
287
  }
288
  // Any item created or edited via this interface is considered "customized".
289
290
  $form['menu']['customized'] = array('#type' => 'value', '#value' => 1);
  $form['menu']['original_item'] = array('#type' => 'value', '#value' => $item);
Dries's avatar
   
Dries committed
291

292
  if ($item['module'] == 'menu') {
293
    $form['menu']['link_path'] = array(
294
      '#type' => 'textfield',
295
      '#title' => t('Path'),
296
      '#default_value' => $item['link_path'],
297
      '#description' => t('The path this menu item links to. This can be an internal Drupal path such as %add-node or an external URL such as %drupal. Enter %front to link to the front page.', array('%front' => '<front>', '%add-node' => 'node/add', '%drupal' => 'http://drupal.org')),
298
      '#required' => TRUE,
299
    );
300
301
302
303
304
305
306
    $form['delete'] = array(
      '#type' => 'submit',
      '#value' => t('Delete'),
      '#access' => $item['mlid'],
      '#submit' => array('menu_item_delete_submit'),
      '#weight' => 10,
    );
307
308
  }
  else {
309
    $form['menu']['_path'] = array(
310
      '#type' => 'item',
311
      '#title' => t('Path'),
312
      '#description' => l($item['link_title'], $item['href'], $item['options']),
313
    );
314
  }
315
  $form['menu']['link_title'] = array('#type' => 'textfield',
316
317
318
319
320
    '#title' => t('Menu link title'),
    '#default_value' => $item['link_title'],
    '#description' => t('The link text corresponding to this item that should appear in the menu.'),
    '#required' => TRUE,
  );
321
  $form['menu']['description'] = array(
322
323
324
325
326
327
    '#type' => 'textarea',
    '#title' => t('Description'),
    '#default_value' => isset($item['options']['attributes']['title']) ? $item['options']['attributes']['title'] : '',
    '#rows' => 1,
    '#description' => t('The description displayed when hovering over a menu item.'),
  );
328
  $form['menu']['expanded'] = array(
329
    '#type' => 'checkbox',
330
    '#title' => t('Expanded'),
331
    '#default_value' => $item['expanded'],
332
333
    '#description' => t('If selected and this menu item has children, the menu will always appear expanded.'),
  );
334

335
  // Generate a list of possible parents (not including this item or descendants).
336
337
338
339
340
  $options = menu_parent_options(menu_get_menus(), $item);
  $default = $item['menu_name'] .':'. $item['plid'];
  if (!isset($options[$default])) {
    $default = 'navigation:0';
  }
341
  $form['menu']['parent'] = array(
342
    '#type' => 'select',
343
    '#title' => t('Parent item'),
344
    '#default_value' => $default,
345
    '#options' => $options,
346
347
    '#description' => t('The maximum depth for an item and all its children is fixed at !maxdepth.  Some menu items may not be available as parents if selecting them would exceed this limit.', array('!maxdepth' => MENU_MAX_DEPTH)),
    '#attributes' => array('class' => 'menu-title-select'),
348
  );
349
  $form['menu']['weight'] = array(
350
    '#type' => 'weight',
351
    '#title' => t('Weight'),
352
    '#default_value' => $item['weight'],
353
354
    '#description' => t('Optional. In the menu, the heavier items will sink and the lighter items will be positioned nearer the top.'),
  );
355
  $form['submit'] = array('#type' => 'submit', '#value' => t('Save'));
356

357

358
  return $form;
359
360
}

361
362
363
/**
 * Validate form values for a menu link being added or edited.
 */
364
function menu_edit_item_validate($form, &$form_state) {
365
  $item = $form_state['values']['menu'];
366
367
  if (!trim($item['link_path']) || !menu_valid_path($item)) {
    form_set_error('link_path', t("The path '@link_path' is either invalid or you do not have access to it.", array('@link_path' => $item['link_path'])));
368
369
  }
}
370

371
372
373
374
/**
 * Submit function for the delete button on the menu item editing form.
 */
function menu_item_delete_submit($form, &$form_state) {
375
  $form_state['redirect'] = 'admin/build/menu/item/'. $form_state['values']['menu']['mlid'] .'/delete';
376
377
}

Dries's avatar
   
Dries committed
378
/**
379
 * Process menu and menu item add/edit form submissions.
Dries's avatar
   
Dries committed
380
 */
381
function menu_edit_item_submit($form, &$form_state) {
382
383
384
385
  $item = $form_state['values']['menu'];
  $item['options']['attributes']['title'] = $item['description'];
  list($item['menu_name'], $item['plid']) = explode(':', $item['parent']);
  if (!menu_link_save($item)) {
386
387
    drupal_set_message(t('There was an error saving the menu link.'), 'error');
  }
388
  $form_state['redirect'] = 'admin/build/menu-customize/'. $item['menu_name'];
Dries's avatar
   
Dries committed
389
390
}

391
/**
392
 * Return a list of menu items that are valid possible parents for the given menu item.
393
 *
394
395
 * @param $menus
 *   An array of menu names and titles, such as from menu_get_menus().
396
397
 * @param $item
 *   The menu item for which to generate a list of parents.
398
 *   If $item['mlid'] == 0 then the complete tree is returned.
399
 * @return
400
401
 *   An array of menu link titles keyed on the a string containing the menu name
 *   and mlid. The list excludes the given item and its children.
402
 */
403
function menu_parent_options($menus, $item) {
404

405
  // If the item has children, there is an added limit to the depth of valid parents.
406
407
408
409
410
411
  if (isset($item['parent_depth_limit'])) {
    $limit = $item['parent_depth_limit'];
  }
  else {
    $limit = _menu_parent_depth_limit($item);
  }
412

413
  foreach ($menus as $menu_name => $title) {
414
    $tree = menu_tree_all_data($menu_name, NULL);
415
    $options[$menu_name .':0'] = '<'. $title .'>';
416
    _menu_parents_recurse($tree, $menu_name, '--', $options, $item['mlid'], $limit);
417
  }
418
  return $options;
419
420
421
}

/**
422
 * Recursive helper function for menu_parent_options().
423
 */
424
function _menu_parents_recurse($tree, $menu_name, $indent, &$options, $exclude, $depth_limit) {
425
  foreach ($tree as $data) {
426
427
428
429
    if ($data['link']['depth'] > $depth_limit) {
      // Don't iterate through any links on this level.
      break;
    }
430
    if ($data['link']['mlid'] != $exclude && $data['link']['hidden'] >= 0) {
431
432
433
434
      $title = $indent .' '. truncate_utf8($data['link']['title'], 30, TRUE, FALSE);
      if ($data['link']['hidden']) {
        $title .= ' ('. t('disabled') .')';
      }
435
      $options[$menu_name .':'. $data['link']['mlid']] = $title;
436
437
      if ($data['below']) {
        _menu_parents_recurse($data['below'], $menu_name, $indent .'--', $options, $exclude, $depth_limit);
438
439
440
      }
    }
  }
441
442
443
}

/**
444
 * Menu callback; Build the form that handles the adding/editing of a custom menu.
445
 */
446
function menu_edit_menu(&$form_state, $type, $menu = array()) {
447
  if ($type == 'edit') {
448
    $form['menu_name'] = array('#type' => 'value', '#value' => $menu['menu_name']);
449
    $form['#insert'] = FALSE;
450
451
452
453
454
455
456
    $form['delete'] = array(
      '#type' => 'submit',
      '#value' => t('Delete'),
      '#access' => !in_array($menu['menu_name'], menu_list_system_menus()),
      '#submit' => array('menu_custom_delete_submit'),
      '#weight' => 10,
    );
457
458
  }
  else {
459
460
461
462
    $menu = array('menu_name' => '', 'title' => '', 'description' => '');
    $form['menu_name'] = array(
      '#type' => 'textfield',
      '#title' => t('Menu name'),
463
      '#description' => t('The machine-readable name of this menu. This text will be used for constructing the URL of the <em>menu overview</em> page for this menu. This name may consist of only of lowercase letters, numbers, and hyphens, and must be unique.'),
464
465
466
      '#required' => TRUE,
    );
    $form['#insert'] = TRUE;
467
  }
468
469
470
  $form['#title'] = $menu['title'];
  $form['title'] = array(
    '#type' => 'textfield',
471
    '#title' => t('Title'),
472
    '#default_value' => $menu['title'],
473
    '#required' => TRUE,
474
475
  );
  $form['description'] = array(
476
477
478
479
480
481
    '#type' => 'textarea',
    '#title' => t('Description'),
    '#default_value' => $menu['description'],
  );
  $form['submit'] = array(
    '#type' => 'submit',
482
    '#value' => t('Save'),
483
484
485
486
487
  );

  return $form;
}

488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
/**
 * Submit function for the 'Delete' button on the menu editing form.
 */
function menu_custom_delete_submit($form, &$form_state) {
  $form_state['redirect'] = 'admin/build/menu-customize/'. $form_state['values']['menu_name'] .'/delete';
}

/**
 * Menu callback; check access and get a confirm form for deletion of a custom menu.
 */
function menu_delete_menu_page($menu) {
  // System-defined menus may not be deleted.
  if (in_array($menu['menu_name'], menu_list_system_menus())) {
    drupal_access_denied();
    return;
  }
  return drupal_get_form('menu_delete_menu_confirm', $menu);
}

/**
 * Build a confirm form for deletion of a custom menu.
 */
function menu_delete_menu_confirm(&$form_state, $menu) {
  $form['#menu'] = $menu;
  $caption = '';
  $num_links = db_result(db_query("SELECT COUNT(*) FROM {menu_links} WHERE menu_name = '%s'", $menu['menu_name']));
  if ($num_links) {
    $caption .= '<p>'. format_plural($num_links, '<strong>Warning:</strong> There is currently 1 menu item in %title. It will be deleted (system-defined items will be reset).', '<strong>Warning:</strong> There are currently @count menu items in %title. They will be deleted (system-defined items will be reset).', array('%title' => $menu['title'])) .'</p>';
  }
517
  $caption .= '<p>'. t('This action cannot be undone.') .'</p>';
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
  return confirm_form($form, t('Are you sure you want to delete the custom menu %title?', array('%title' => $menu['title'])), 'admin/build/menu-customize/'. $menu['menu_name'], $caption, t('Delete'));
}

/**
 * Delete a custom menu and all items in it.
 */
function menu_delete_menu_confirm_submit($form, &$form_state) {
  $menu = $form['#menu'];
  $form_state['redirect'] = 'admin/build/menu';
  // System-defined menus may not be deleted - only menus defined by this module.
  if (in_array($menu['menu_name'], menu_list_system_menus())  || !db_result(db_query("SELECT COUNT(*) FROM {menu_custom} WHERE menu_name = '%s'", $menu['menu_name']))) {
    return;
  }
  // Reset all the menu links defined by the system via hook_menu.
  $result = db_query("SELECT * FROM {menu_links} ml INNER JOIN {menu_router} m ON ml.router_path = m.path WHERE ml.menu_name = '%s' AND ml.module = 'system' ORDER BY m.number_parts ASC", $menu['menu_name']);
  while ($item = db_fetch_array($result)) {
    menu_reset_item($item);
  }
  // Delete all links to the overview page for this menu.
  $result = db_query("SELECT mlid FROM {menu_links} ml WHERE ml.link_path = '%s'", 'admin/build/menu-customize/'. $menu['menu_name']);
  while ($m = db_fetch_array($result)) {
539
    menu_link_delete($m['mlid']);
540
541
542
543
544
545
546
547
548
549
550
551
552
553
  }
  // Delete all the links in the menu and the menu from the list of custom menus.
  db_query("DELETE FROM {menu_links} WHERE menu_name = '%s'", $menu['menu_name']);
  db_query("DELETE FROM {menu_custom} WHERE menu_name = '%s'", $menu['menu_name']);
  // Delete all the blocks for this menu.
  db_query("DELETE FROM {blocks} WHERE module = 'menu' AND delta = '%s'", $menu['menu_name']);
  db_query("DELETE FROM {blocks_roles} WHERE module = 'menu' AND delta = '%s'", $menu['menu_name']);
  menu_cache_clear_all();
  cache_clear_all();
  $t_args = array('%title' => $menu['title']);
  drupal_set_message(t('The custom menu %title has been deleted.', $t_args));
  watchdog('menu', 'Deleted custom menu %title and all its menu items.', $t_args, WATCHDOG_NOTICE);
}

554
555
556
/**
 * Validates the human and machine-readable names when adding or editing a menu.
 */
557
558
559
function menu_edit_menu_validate($form, &$form_state) {
  $item = $form_state['values'];
  if (preg_match('/[^a-z0-9-]/', $item['menu_name'])) {
560
    form_set_error('menu_name', t('Menu name may consist only of lowercase letters, numbers, and hyphens.'));
561
  }
562
563
  if ($form['#insert']) {
    // We will add 'menu-' to the menu name to help avoid name-space conflicts.
564
    $item['menu_name'] = 'menu-'. $item['menu_name'];
565
    if (db_result(db_query("SELECT menu_name FROM {menu_custom} WHERE menu_name = '%s'", $item['menu_name'])) ||
566
      db_result(db_query_range("SELECT menu_name FROM {menu_links} WHERE menu_name = '%s'", $item['menu_name'], 0, 1))) {
567
568
      form_set_error('menu_name', t('Menu already exists'));
    }
569
570
571
  }
}

572
573
574
/**
 * Submit function for adding or editing a custom menu.
 */
575
576
function menu_edit_menu_submit($form, &$form_state) {
  $menu = $form_state['values'];
577
  $path = 'admin/build/menu-customize/';
578
  if ($form['#insert']) {
579
580
581
582
583
584
    // Add 'menu-' to the menu name to help avoid name-space conflicts.
    $menu['menu_name'] = 'menu-'. $menu['menu_name'];
    $link['link_title'] = $menu['title'];
    $link['link_path'] = $path . $menu['menu_name'];
    $link['router_path'] = $path .'%';
    $link['module'] = 'menu';
585
    $link['plid'] = db_result(db_query("SELECT mlid FROM {menu_links} WHERE menu_name = '%s' AND link_path = '%s'", 'navigation', 'admin/build/menu'));
586
    menu_link_save($link);
587
588
589
590
    db_query("INSERT INTO {menu_custom} (menu_name, title, description) VALUES ('%s', '%s', '%s')", $menu['menu_name'], $menu['title'], $menu['description']);
  }
  else {
    db_query("UPDATE {menu_custom} SET title = '%s', description = '%s' WHERE menu_name = '%s'", $menu['title'], $menu['description'], $menu['menu_name']);
591
    $result = db_query("SELECT mlid FROM {menu_links} WHERE link_path = '%s'", $path . $menu['menu_name']);
592
593
594
595
596
    while ($m = db_fetch_array($result)) {
      $link = menu_link_load($m['mlid']);
      $link['link_title'] = $menu['title'];
      menu_link_save($link);
    }
597
  }
598
  $form_state['redirect'] = $path . $menu['menu_name'];
599
600
}

601
/**
602
 * Menu callback; Check access and present a confirm form for deleting a menu link.
603
 */
604
605
function menu_item_delete_page($item) {
  // Links defined via hook_menu may not be deleted.
606
607
  if ($item['module'] == 'system') {
    drupal_access_denied();
608
    return;
609
  }
610
611
612
613
614
615
616
  return drupal_get_form('menu_item_delete_form', $item);
}

/**
 * Build a confirm form for deletion of a single menu link.
 */
function menu_item_delete_form(&$form_state, $item) {
617
  $form['#item'] = $item;
618
  return confirm_form($form, t('Are you sure you want to delete the custom menu item %item?', array('%item' => $item['link_title'])), 'admin/build/menu-customize/'. $item['menu_name']);
619
}
620

621
622
623
/**
 * Process menu delete form submissions.
 */
624
function menu_item_delete_form_submit($form, &$form_state) {
625
626
627
628
629
  $item = $form['#item'];
  menu_link_delete($item['mlid']);
  $t_args = array('%title' => $item['link_title']);
  drupal_set_message(t('The menu item %title has been deleted.', $t_args));
  watchdog('menu', 'Deleted menu item %title.', $t_args, WATCHDOG_NOTICE);
630
  $form_state['redirect'] = 'admin/build/menu-customize/'. $item['menu_name'];
631
632
}

633
634
635
/**
 * Menu callback; reset a single modified item.
 */
636
function menu_reset_item_confirm(&$form_state, $item) {
637
  $form['item'] = array('#type' => 'value', '#value' => $item);
638
  return confirm_form($form, t('Are you sure you want to reset the item %item to its default values?', array('%item' => $item['link_title'])), 'admin/build/menu-customize/'. $item['menu_name'], t('Any customizations will be lost. This action cannot be undone.'), t('Reset'));
639
640
641
}

/**
642
 * Process menu reset item form submissions.
643
 */
644
function menu_reset_item_confirm_submit($form, &$form_state) {
645
  $item = $form_state['values']['item'];
646
647
648
649
650
651
652
653
654
  $new_item = menu_reset_item($item);
  drupal_set_message(t('The menu item was reset to its default settings.'));
  $form_state['redirect'] = 'admin/build/menu-customize/'. $new_item['menu_name'];
}

/**
 * Reset a system-defined menu item.
 */
function menu_reset_item($item) {
655
656
657
658
659
660
  $router = menu_router_build();
  $new_item = _menu_link_build($router[$item['router_path']]);
  foreach (array('mlid', 'has_children') as $key) {
    $new_item[$key] = $item[$key];
  }
  menu_link_save($new_item);
661
  return $new_item;
662
663
}

664
/**
665
 * Implementation of hook_block().
666
 */
667
function menu_block($op = 'list', $delta = 0) {
668
  $custom_menus = menu_get_menus(FALSE);
669
670
  if ($op == 'list') {
    $blocks = array();
671
    foreach ($custom_menus as $name => $title) {
672
      // Default "Navigation" block is handled by user.module.
673
      $blocks[$name]['info'] = check_plain($title);
674
675
676
      // Menu blocks can't be cached because each menu item can have
      // a custom access callback. menu.inc manages its own caching.
      $blocks[$name]['cache'] = BLOCK_NO_CACHE;
677
    }
678
    return $blocks;
679
  }
680
  else if ($op == 'view') {
681
682
    $data['subject'] = check_plain($custom_menus[$delta]);
    $data['content'] = menu_tree($delta);
683
684
685
    return $data;
  }
}
686

687
688
689
/**
 * Implementation of hook_nodeapi().
 */
690
691
692
693
694
695
696
697
function menu_nodeapi(&$node, $op) {
  switch ($op) {
    case 'insert':
    case 'update':
      if (isset($node->menu)) {
        $item = $node->menu;
        if (!empty($item['delete'])) {
          menu_link_delete($item['mlid']);
698
        }
699
700
        elseif (trim($item['link_title'])) {
          $item['link_title'] = trim($item['link_title']);
701
          $item['link_path'] = "node/$node->nid";
702
703
704
          if (!$item['customized']) {
            $item['options']['attributes']['title'] = trim($node->title);
          }
705
706
707
          if (!menu_link_save($item)) {
            drupal_set_message(t('There was an error saving the menu link.'), 'error');
          }
708
        }
709
710
711
712
713
714
715
716
717
718
719
720
      }
      break;
    case 'delete':
      // Delete all menu module links that point to this node.
      $result = db_query("SELECT mlid FROM {menu_links} WHERE link_path = 'node/%d' AND module = 'menu'", $node->nid);
      while ($m = db_fetch_array($result)) {
        menu_link_delete($m['mlid']);
      }
      break;
    case 'prepare':
      if (empty($node->menu)) {
        // Prepare the node for the edit form so that $node->menu always exists.
721
        $menu_name = variable_get('menu_default_node_menu', 'navigation');
722
723
        $item = array();
        if (isset($node->nid)) {
724
725
726
727
728
729
          // Give priority to the default menu
          $mlid = db_result(db_query_range("SELECT mlid FROM {menu_links} WHERE link_path = 'node/%d' AND menu_name = '%s' AND module = 'menu' ORDER BY mlid ASC", $node->nid, $menu_name, 0, 1));
          // Check all menus if a link does not exist in the default menu.
          if (!$mlid) {
            $mlid = db_result(db_query_range("SELECT mlid FROM {menu_links} WHERE link_path = 'node/%d' AND module = 'menu' ORDER BY mlid ASC", $node->nid, 0, 1));
          }
730
731
732
733
734
735
736
          if ($mlid) {
            $item = menu_link_load($mlid);
          }
        }
        // Set default values.
        $node->menu = $item + array('link_title' => '', 'mlid' => 0, 'plid' => 0, 'menu_name' => $menu_name, 'weight' => 0, 'options' => array(), 'module' => 'menu', 'expanded' => 0, 'hidden' => 0, 'has_children' => 0, 'customized' => 0);
      }
737
738
739
740
      // Find the depth limit for the parent select.
      if (!isset($node->menu['parent_depth_limit'])) {
        $node->menu['parent_depth_limit'] = _menu_parent_depth_limit($node->menu);
      }
741
      break;
742
743
744
  }
}

745
746
747
748
749
750
751
/**
 * Find the depth limit for items in the parent select.
 */
function _menu_parent_depth_limit($item) {
  return MENU_MAX_DEPTH - 1 - (($item['mlid'] && $item['has_children']) ? menu_link_children_relative_depth($item) : 0);
}

752
/**
753
 * Implementation of hook_form_alter(). Adds menu item fields to the node form.
754
 */
755
function menu_form_alter(&$form, $form_state, $form_id) {
756
757
758
759
  if (isset($form['#node']) && $form['#node']->type .'_node_form' == $form_id) {
    // Note - doing this to make sure the delete checkbox stays in the form.
    $form['#cache'] = TRUE;

760
761
    $form['menu'] = array(
      '#type' => 'fieldset',
762
763
764
      '#title' => t('Menu settings'),
      '#access' => user_access('administer menu'),
      '#collapsible' => TRUE,
765
      '#collapsed' => FALSE,
766
      '#tree' => TRUE,
767
      '#weight' => -2,
768
      '#attributes' => array('class' => 'menu-item-form'),
769
    );
770
771
772
    $item = $form['#node']->menu;

    if ($item['mlid']) {
773
      // There is an existing link.
774
      $form['menu']['delete'] = array(
775
        '#type' => 'checkbox',
776
        '#title' => t('Check to remove this item from the menu.'),
777
      );
778
    }
779
    if (!$item['link_title']) {
780
781
      $form['menu']['#collapsed'] = TRUE;
    }
782

783
    foreach (array('mlid', 'module', 'hidden', 'has_children', 'customized', 'options', 'expanded', 'hidden', 'parent_depth_limit') as $key) {
784
785
786
787
788
789
790
791
792
793
      $form['menu'][$key] = array('#type' => 'value', '#value' => $item[$key]);
    }

    $form['menu']['link_title'] = array('#type' => 'textfield',
      '#title' => t('Menu link title'),
      '#default_value' => $item['link_title'],
      '#description' => t('The link text corresponding to this item that should appear in the menu. Leave blank if you do not wish to add this post to the menu.'),
      '#required' => FALSE,
    );
    // Generate a list of possible parents (not including this item or descendants).
794
795
796
797
798
799
    $options = menu_parent_options(menu_get_menus(), $item);
    $default = $item['menu_name'] .':'. $item['plid'];
    if (!isset($options[$default])) {
      $default = 'navigation:0';
    }
    $form['menu']['parent'] = array(
800
801
      '#type' => 'select',
      '#title' => t('Parent item'),
802
      '#default_value' => $default,
803
      '#options' => $options,
804
805
      '#description' => t('The maximum depth for an item and all its children is fixed at !maxdepth.  Some menu items may not be available as parents if selecting them would exceed this limit.', array('!maxdepth' => MENU_MAX_DEPTH)),
      '#attributes' => array('class' => 'menu-title-select'),
806
    );
807
808
    $form['#submit'][] = 'menu_node_form_submit';

809
810
811
812
813
814
    $form['menu']['weight'] = array(
      '#type' => 'weight',
      '#title' => t('Weight'),
      '#default_value' => $item['weight'],
      '#description' => t('Optional. In the menu, the heavier items will sink and the lighter items will be positioned nearer the top.'),
    );
815
816
817
  }
}

818
819
820
821
822
823
824
/**
 * Decompose the selected menu parent option into the menu_name and plid.
 */
function menu_node_form_submit($form, &$form_state) {
  list($form_state['values']['menu']['menu_name'], $form_state['values']['menu']['plid']) = explode(':', $form_state['values']['menu']['parent']);
}

825
826
827
828
829
830
831
832
833
834
/**
 * Return an associative array of the custom menus names.
 *
 * @param $all
 *   If FALSE return only user-added menus, or if TRUE also include
 *   the menus defined by the system.
 * @return
 *   An array with the machine-readable names as the keys, and human-readable
 *   titles as the values.
 */
835
function menu_get_menus($all = TRUE) {
836
837
838
839
840
841
842
  $system_menus = menu_list_system_menus();
  $sql = 'SELECT * FROM {menu_custom}';
  if (!$all) {
    $sql .= ' WHERE menu_name NOT IN ('. implode(',', array_fill(0, count($system_menus), "'%s'")) .')';
  }
  $sql .= ' ORDER BY title';
  $result = db_query($sql, $system_menus);
843
844
845
846
847
  $rows = array();
  while ($r = db_fetch_array($result)) {
    $rows[$r['menu_name']] = $r['title'];
  }
  return $rows;
848
849
}

850
/**
851
 * Menu callback; Build the form presenting menu configuration options.
852
 */
853
function menu_configure() {
854
855
  $form['intro'] = array(
    '#type' => 'item',
856
    '#value' => t('The menu module allows on-the-fly creation of menu links in the content authoring forms. The following option sets the default menu in which a new link will be added.'),
857
858
  );

859
860
861
862
  $authoring_options = menu_get_menus();
  $form['menu_default_node_menu'] = array('#type' => 'select',
    '#title' => t('Default menu for content'),
    '#default_value' => variable_get('menu_default_node_menu', 'navigation'),
863
    '#options' => $authoring_options,
864
    '#description' => t('Choose the menu to be the default in the menu options in the content authoring form.'),
865
  );
866
867

  return system_settings_form($form);
868
}
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897

/**
 * Validates the path of a menu link being created or edited.
 *
 * @return
 *   TRUE if it is a valid path AND the current user has access permission,
 *   FALSE otherwise.
 */
function menu_valid_path($form_item) {
  $item = array();
  $path = $form_item['link_path'];
  if ($path == '<front>' || menu_path_is_external($path)) {
    $item = array('access' => TRUE);
  }
  elseif (preg_match('/\/\%/', $path)) {
    // Path is dynamic (ie 'user/%'), so check directly against menu_router table.
    if ($item = db_fetch_array(db_query("SELECT * FROM {menu_router} where path = '%s' ", $path))) {
      $item['link_path']  = $form_item['link_path'];
      $item['link_title'] = $form_item['link_title'];
      $item['external']   = FALSE;
      $item['options'] = '';
      _menu_link_translate($item);
    }
  }
  else {
    $item = menu_get_item($path);
  }
  return $item && $item['access'];
}