diff --git a/includes/menu.inc b/includes/menu.inc index a9511af9724d11fc2509039c2f354a9db9214646..eba2a90362bcd6d111519e06faafb9baa6bffbb0 100644 --- a/includes/menu.inc +++ b/includes/menu.inc @@ -1464,7 +1464,13 @@ function menu_get_names() { * Return an array containing the names of system-defined (default) menus. */ function menu_list_system_menus() { - return array('navigation' => 'Navigation', 'management' => 'Management', 'user-menu' => 'User menu', 'main-menu' => 'Main menu', 'secondary-menu' => 'Secondary menu'); + return array( + 'navigation' => 'Navigation', + 'management' => 'Management', + 'user-menu' => 'User menu', + 'main-menu' => 'Main menu', + 'secondary-menu' => 'Secondary menu', + ); } /** diff --git a/modules/block/block.module b/modules/block/block.module index 2e84ab94b25448670b3fba112832de8b7a3c0bc8..8db09eb81348459c3f0cb11a0574e1dab3eda4d4 100644 --- a/modules/block/block.module +++ b/modules/block/block.module @@ -851,6 +851,20 @@ function block_filter_format_delete($format, $fallback) { ->execute(); } +/** + * Implement hook_menu_delete(). + */ +function block_menu_delete($menu) { + db_delete('block') + ->condition('module', 'menu') + ->condition('delta', $menu['menu_name']) + ->execute(); + db_delete('block_role') + ->condition('module', 'menu') + ->condition('delta', $menu['menu_name']) + ->execute(); +} + /** * Implement hook_form_FORM_ID_alter(). */ diff --git a/modules/menu/menu.admin.inc b/modules/menu/menu.admin.inc index cc9b873d68b464dc30a6a45ed0792418707fd3ed..8841fa3d638f37bba47668a093fd5fc3a926f764 100644 --- a/modules/menu/menu.admin.inc +++ b/modules/menu/menu.admin.inc @@ -408,7 +408,11 @@ function menu_edit_item_submit($form, &$form_state) { */ function menu_edit_menu($form, &$form_state, $type, $menu = array()) { $system_menus = menu_list_system_menus(); - $menu += array('menu_name' => '', 'title' => '', 'description' => ''); + $menu += array('menu_name' => '', 'old_name' => '', 'title' => '', 'description' => ''); + if (!empty($menu['menu_name'])) { + $menu['old_name'] = $menu['menu_name']; + } + $form['old_name'] = array('#type' => 'value', '#value' => $menu['old_name']); // The title of a system menu cannot be altered. if (isset($system_menus[$menu['menu_name']])) { @@ -425,7 +429,7 @@ function menu_edit_menu($form, &$form_state, $type, $menu = array()) { } // The internal menu name can only be defined during initial menu creation. - if ($type == 'edit') { + if (!empty($menu['old_name'])) { $form['#insert'] = FALSE; $form['menu_name'] = array('#type' => 'value', '#value' => $menu['menu_name']); } @@ -516,41 +520,28 @@ function menu_delete_menu_confirm($form, &$form_state, $menu) { function menu_delete_menu_confirm_submit($form, &$form_state) { $menu = $form['#menu']; $form_state['redirect'] = 'admin/structure/menu'; + // System-defined menus may not be deleted - only menus defined by this module. $system_menus = menu_list_system_menus(); if (isset($system_menus[$menu['menu_name']]) || !(db_query("SELECT 1 FROM {menu_custom} WHERE menu_name = :menu", array(':menu' => $menu['menu_name']))->fetchField())) { return; } - // Reset all the menu links defined by the system via hook_menu. + + // 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 = :menu AND ml.module = 'system' ORDER BY m.number_parts ASC", array(':menu' => $menu['menu_name']), array('fetch' => PDO::FETCH_ASSOC)); - foreach ($result as $item) { - menu_reset_item($item); + foreach ($result as $link) { + menu_reset_item($link); } + // Delete all links to the overview page for this menu. $result = db_query("SELECT mlid FROM {menu_links} ml WHERE ml.link_path = :link", array(':link' => 'admin/structure/menu/manage/' . $menu['menu_name']), array('fetch' => PDO::FETCH_ASSOC)); - foreach ($result as $m) { - menu_link_delete($m['mlid']); - } - // Delete all the links in the menu and the menu from the list of custom menus. - db_delete('menu_links') - ->condition('menu_name', $menu['menu_name']) - ->execute(); - db_delete('menu_custom') - ->condition('menu_name', $menu['menu_name']) - ->execute(); - // Delete all the blocks for this menu. - if (module_exists('block')) { - db_delete('block') - ->condition('module', 'menu') - ->condition('delta', $menu['menu_name']) - ->execute(); - db_delete('block_role') - ->condition('module', 'menu') - ->condition('delta', $menu['menu_name']) - ->execute(); - } - menu_cache_clear_all(); - cache_clear_all(); + foreach ($result as $link) { + menu_link_delete($link['mlid']); + } + + // Delete the custom menu and all its menu links. + menu_delete($menu); + $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 links.', $t_args, WATCHDOG_NOTICE); @@ -571,8 +562,8 @@ function menu_edit_menu_validate($form, &$form_state) { // We will add 'menu-' to the menu name to help avoid name-space conflicts. $item['menu_name'] = 'menu-' . $item['menu_name']; - $custom_exists = db_query('SELECT menu_name FROM {menu_custom} WHERE menu_name = :menu', array(':menu' => $item['menu_name']))->fetchField(); - $link_exists = db_query_range("SELECT menu_name FROM {menu_links} WHERE menu_name = :menu", 0, 1, array(':menu' => $item['menu_name']))->fetchField(); + $custom_exists = db_query_range('SELECT 1 FROM {menu_custom} WHERE menu_name = :menu', 0, 1, array(':menu' => $item['menu_name']))->fetchField(); + $link_exists = db_query_range("SELECT 1 FROM {menu_links} WHERE menu_name = :menu", 0, 1, array(':menu' => $item['menu_name']))->fetchField(); if ($custom_exists || $link_exists) { form_set_error('menu_name', t('The menu already exists.')); } @@ -599,22 +590,10 @@ function menu_edit_menu_submit($form, &$form_state) { ->fetchField(); menu_link_save($link); - db_insert('menu_custom') - ->fields(array( - 'menu_name' => $menu['menu_name'], - 'title' => $menu['title'], - 'description' => $menu['description'], - )) - ->execute(); + menu_save($menu); } else { - db_update('menu_custom') - ->fields(array( - 'title' => $menu['title'], - 'description' => $menu['description'], - )) - ->condition('menu_name', $menu['menu_name']) - ->execute(); + menu_save($menu); $result = db_query("SELECT mlid FROM {menu_links} WHERE link_path = :path", array(':path' => $path . $menu['menu_name']), array('fetch' => PDO::FETCH_ASSOC)); foreach ($result as $m) { $link = menu_link_load($m['mlid']); diff --git a/modules/menu/menu.api.php b/modules/menu/menu.api.php index 80cd58df4219f035a55b31b5399a5b523ba845c6..599a4a124d4e0715388f1ce454bd1c80ff6a4084 100644 --- a/modules/menu/menu.api.php +++ b/modules/menu/menu.api.php @@ -291,7 +291,7 @@ function hook_translated_menu_link_alter(&$item, $map) { } } - /** +/** * Inform modules that a menu link has been created. * * This hook is used to notify modules that menu items have been @@ -357,6 +357,78 @@ function hook_menu_link_delete($link) { ->execute(); } +/** + * Informs modules that a custom menu was created. + * + * This hook is used to notify modules that a custom menu has been created. + * Contributed modules may use the information to perform actions based on the + * information entered into the menu system. + * + * @param $menu + * An array representing a custom menu: + * - menu_name: The unique name of the custom menu. + * - title: The human readable menu title. + * - description: The custom menu description. + * + * @see hook_menu_update() + * @see hook_menu_delete() + */ +function hook_menu_insert($menu) { + // For example, we track available menus in a variable. + $my_menus = variable_get('my_module_menus', array()); + $my_menus[$menu['menu_name']] = $menu['menu_name']; + variable_set('my_module_menus', $my_menus); +} + +/** + * Informs modules that a custom menu was updated. + * + * This hook is used to notify modules that a custom menu has been updated. + * Contributed modules may use the information to perform actions based on the + * information entered into the menu system. + * + * @param $menu + * An array representing a custom menu: + * - menu_name: The unique name of the custom menu. + * - title: The human readable menu title. + * - description: The custom menu description. + * - old_name: The current 'menu_name'. Note that internal menu names cannot + * be changed after initial creation. + * + * @see hook_menu_insert() + * @see hook_menu_delete() + */ +function hook_menu_update($menu) { + // For example, we track available menus in a variable. + $my_menus = variable_get('my_module_menus', array()); + $my_menus[$menu['menu_name']] = $menu['menu_name']; + variable_set('my_module_menus', $my_menus); +} + +/** + * Informs modules that a custom menu was deleted. + * + * This hook is used to notify modules that a custom menu along with all links + * contained in it (if any) has been deleted. Contributed modules may use the + * information to perform actions based on the information entered into the menu + * system. + * + * @param $link + * An array representing a custom menu: + * - menu_name: The unique name of the custom menu. + * - title: The human readable menu title. + * - description: The custom menu description. + * + * @see hook_menu_insert() + * @see hook_menu_update() + */ +function hook_menu_delete($menu) { + // Delete the record from our variable. + $my_menus = variable_get('my_module_menus', array()); + unset($my_menus[$menu['menu_name']]); + variable_set('my_module_menus', $my_menus); +} + /** * @} End of "addtogroup hooks". */ diff --git a/modules/menu/menu.install b/modules/menu/menu.install index aca5835ee14346b5eacae8c2a2e5c06bacab3ead..6514ef2bacce9e71d07b74a948bfc135a3adce04 100644 --- a/modules/menu/menu.install +++ b/modules/menu/menu.install @@ -11,17 +11,21 @@ */ function menu_install() { $system_menus = menu_list_system_menus(); + $t = get_t(); $descriptions = array( - 'navigation' => 'The Navigation menu contains links such as Recent posts (if the Tracker module is enabled). Non-administrative links are added to this menu by default by modules.', - 'user-menu' => "The User menu contains links related to the user's account, as well as the 'Log out' link.", - 'management' => 'The Management menu contains links for content creation, structure, user management, and similar site activities.', - 'main-menu' => 'The Main menu is the default source for the Main links which are often used by themes to show the major sections of a site.', - 'secondary-menu' => 'The Secondary menu is the default source for the Secondary links which are often used for legal notices, contact details, and other navigation items that play a lesser role than the Main links.', + 'navigation' => $t('The Navigation menu contains links such as Recent posts (if the Tracker module is enabled). Non-administrative links are added to this menu by default by modules.'), + 'user-menu' => $t("The User menu contains links related to the user's account, as well as the 'Log out' link."), + 'management' => $t('The Management menu contains links for content creation, structure, user management, and similar site activities.'), + 'main-menu' => $t('The Main menu is the default source for the Main links which are often used by themes to show the major sections of a site.'), + 'secondary-menu' => $t('The Secondary menu is the default source for the Secondary links which are often used for legal notices, contact details, and other navigation items that play a lesser role than the Main links.'), ); - $t = get_t(); - $query = db_insert('menu_custom')->fields(array('menu_name', 'title', 'description')); foreach ($system_menus as $menu_name => $title) { - $query->values(array('menu_name' => $menu_name, 'title' => $t($title), 'description' => $t($descriptions[$menu_name])))->execute(); + $menu = array( + 'menu_name' => $menu_name, + 'title' => $t($title), + 'description' => $descriptions[$menu_name], + ); + menu_save($menu); } } diff --git a/modules/menu/menu.module b/modules/menu/menu.module index d338f877f4ce490f69cff439a3c5f8de272f8717..ca77e56875f28460cf46a8d54e377cee08ece0cf 100644 --- a/modules/menu/menu.module +++ b/modules/menu/menu.module @@ -198,11 +198,97 @@ function menu_overview_title($menu) { /** * Load the data for a single custom menu. + * + * @param $menu_name + * The unique name of a custom menu to load. */ function menu_load($menu_name) { return db_query("SELECT * FROM {menu_custom} WHERE menu_name = :menu", array(':menu' => $menu_name))->fetchAssoc(); } +/** + * Save a custom menu. + * + * @param $menu + * An array representing a custom menu: + * - menu_name: The unique name of the custom menu. + * - title: The human readable menu title. + * - description: The custom menu description. + * - old_name: For existing menus, the current 'menu_name', otherwise empty. + * Decides whether hook_menu_insert() or hook_menu_update() will be invoked. + * + * Modules should always pass a fully populated $menu when saving a custom + * menu, so other modules are able to output proper status or watchdog messages. + * + * @see menu_load() + */ +function menu_save($menu) { + db_merge('menu_custom') + ->key(array('menu_name' => $menu['menu_name'])) + ->fields(array( + 'title' => $menu['title'], + 'description' => $menu['description'], + )) + ->execute(); + + // Since custom menus are keyed by name and their machine-name cannot be + // changed, there is no real differentiation between inserting and updating a + // menu. To overcome this, we define the existing menu_name as 'old_name' in + // menu_edit_menu(). + // @todo Replace this condition when db_merge() returns the proper query + // result (insert/update). + if (!empty($menu['old_name'])) { + module_invoke_all('menu_update', $menu); + } + else { + module_invoke_all('menu_insert', $menu); + } +} + +/** + * Delete a custom menu and all contained links. + * + * Note that this function deletes all menu links in a custom menu. While menu + * links derived from router paths may be restored by rebuilding the menu, all + * customized and custom links will be irreversibly gone. Therefore, this + * function should usually be called from a user interface (form submit) handler + * only, which allows the user to confirm the action. + * + * @param $menu + * An array representing a custom menu: + * - menu_name: The unique name of the custom menu. + * - title: The human readable menu title. + * - description: The custom menu description. + * + * Modules should always pass a fully populated $menu when deleting a custom + * menu, so other modules are able to output proper status or watchdog messages. + * + * @see menu_load() + * + * _menu_delete_item() will take care of clearing the page cache. Other modules + * should take care of their menu-related data by implementing + * hook_menu_delete(). + */ +function menu_delete($menu) { + // Delete all links from the menu. + $links = db_query("SELECT * FROM {menu_links} WHERE menu_name = :menu_name", array(':menu_name' => $menu['menu_name'])); + foreach ($links as $link) { + // To speed up the deletion process, we reset some link properties that + // would trigger re-parenting logic in _menu_delete_item() and + // _menu_update_parental_status(). + $link['has_children'] = FALSE; + $link['plid'] = 0; + _menu_delete_item($link); + } + + // Delete the custom menu. + db_delete('menu_custom') + ->condition('menu_name', $menu['menu_name']) + ->execute(); + + module_invoke_all('menu_delete', $menu); +} + /** * Return a list of menu items that are valid possible parents for the given menu item. * @@ -496,3 +582,4 @@ function menu_get_menus($all = TRUE) { return $query->execute()->fetchAllKeyed(); } + diff --git a/modules/menu/menu.test b/modules/menu/menu.test index c8b29b13c0b2c768ed79883da0e8d8d8291af500..796b90f57f8487639b4b47b64b843e3b1fda234a 100644 --- a/modules/menu/menu.test +++ b/modules/menu/menu.test @@ -92,11 +92,38 @@ class MenuTestCase extends DrupalWebTestCase { $this->menu = $this->addCustomMenu(); $this->doMenuTests($this->menu['menu_name']); $this->addInvalidMenuLink($this->menu['menu_name']); + $this->addCustomMenuCRUD(); + } + + /** + * Add custom menu using CRUD functions. + */ + function addCustomMenuCRUD() { + // Add a new custom menu. + $menu_name = substr(md5($this->randomName(16)), 0, MENU_MAX_MENU_NAME_LENGTH_UI); + $title = $this->randomName(16); + + $menu = array( + 'menu_name' => $menu_name, + 'title' => $title, + 'description' => 'Description text', + ); + menu_save($menu); + + // Assert the new menu. + $this->drupalGet('admin/structure/menu/manage/' . $menu_name . '/edit'); + $this->assertText($title, t('Custom menu was added.')); + + // Edit the menu. + $new_title = $this->randomName(16); + $menu['title'] = $new_title; + menu_save($menu); + $this->drupalGet('admin/structure/menu/manage/' . $menu_name . '/edit'); + $this->assertText($new_title, t('Custom menu was edited.')); } /** * Add custom menu. - * */ function addCustomMenu() { // Add custom menu. @@ -152,6 +179,9 @@ class MenuTestCase extends DrupalWebTestCase { $this->assertResponse(200); $this->assertRaw(t('The custom menu %title has been deleted.', array('%title' => $title)), t('Custom menu was deleted')); $this->assertFalse(menu_load($menu_name), 'Custom menu was deleted'); + // Test if all menu links associated to the menu were removed from database. + $result = db_query("SELECT menu_name FROM {menu_links} WHERE menu_name = :menu_name", array(':menu_name' => $menu_name))->fetchField(); + $this->assertFalse($result, t('All menu links associated to the custom menu were deleted.')); } /** diff --git a/modules/toolbar/toolbar.install b/modules/toolbar/toolbar.install index 2e4cf0ba5b7671d398df5020d2bebbb7dc4b4375..e2539d785240571c7cceac5f9311fe44b1d8a5cd 100644 --- a/modules/toolbar/toolbar.install +++ b/modules/toolbar/toolbar.install @@ -14,13 +14,12 @@ */ function toolbar_install() { $t = get_t(); - $query = db_insert('menu_custom') - ->fields(array( - 'menu_name' => 'admin_shortcuts', - 'title' => $t('Administration shortcuts'), - 'description' => $t('The Admininstration shortcuts menu contains commonly used links for administrative tasks.') - )) - ->execute(); + $menu = array( + 'menu_name' => 'admin_shortcuts', + 'title' => $t('Administration shortcuts'), + 'description' => $t('The Administration shortcuts menu contains commonly used links for administrative tasks.'), + ); + menu_save($menu); // Add starter convenience shortcuts. menu_rebuild();