? 306151-schema-based-install.patch ? 457450-menu-objects-ext.patch ? 457450-menu-objects.patch ? test.patch ? sites/all/modules/admin_menu ? sites/all/modules/coder ? sites/all/modules/devel ? sites/all/modules/devel 2 ? sites/all/modules/devel-7.x-1.x-dev.tar.gz ? sites/default/files Index: includes/menu.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/menu.inc,v retrieving revision 1.328 diff -u -p -r1.328 menu.inc --- includes/menu.inc 10 Jun 2009 21:52:36 -0000 1.328 +++ includes/menu.inc 4 Jul 2009 16:32:50 -0000 @@ -2065,6 +2065,9 @@ function _menu_delete_item($item, $force // Update the has_children status of the parent. _menu_update_parental_status($item); menu_cache_clear($item['menu_name']); + // Notify modules we have deleted the item. + module_invoke_all('menu_item_delete', $item); + // Clear the cache. _menu_clear_page_cache(); } } @@ -2105,7 +2108,10 @@ function menu_link_save(&$item) { 'module' => 'menu', 'customized' => 0, 'updated' => 0, + 'object_type' => '', + 'object_id' => '', ); + $existing_item = FALSE; if (isset($item['mlid'])) { if ($existing_item = db_query("SELECT * FROM {menu_links} WHERE mlid = :mlid", array(':mlid' => $item['mlid']))->fetchAssoc()) { @@ -2176,6 +2182,8 @@ function menu_link_save(&$item) { 'options' => serialize($item['options']), 'customized' => $item['customized'], 'updated' => $item['updated'], + 'object_type' => $item['object_type'], + 'object_id' => $item['object_id'], )) ->execute(); } @@ -2209,11 +2217,16 @@ function menu_link_save(&$item) { if (empty($item['router_path']) || !$existing_item || ($existing_item['link_path'] != $item['link_path'])) { if ($item['external']) { $item['router_path'] = ''; + // External links are marked as objects, but have no IDs. + $item['object_type'] = 'external_link'; + $item['object_id'] = ''; } else { // Find the router path which will serve this path. $item['parts'] = explode('/', $item['link_path'], MENU_MAX_PARTS); $item['router_path'] = _menu_find_router_path($item['link_path']); + // Find the object type of this item. + _menu_find_object_type($item); } } // If every value in $existing_item is the same in the $item, there is no @@ -2247,6 +2260,8 @@ function menu_link_save(&$item) { 'link_title' => $item['link_title'], 'options' => serialize($item['options']), 'customized' => $item['customized'], + 'object_type' => $item['object_type'], + 'object_id' => $item['object_id'], )) ->condition('mlid', $item['mlid']) ->execute(); @@ -2257,8 +2272,16 @@ function menu_link_save(&$item) { menu_cache_clear($existing_item['menu_name']); } + // Notify modules we have acted on a menu item. + $hook = 'menu_item_insert'; + if ($existing_item) { + $hook = 'menu_item_update'; + } + module_invoke_all($hook, $item); + // Now clear the cache. _menu_clear_page_cache(); } + return $item['mlid']; } @@ -2334,6 +2357,43 @@ function _menu_find_router_path($link_pa } /** + * Determine the object type and ID for a menu item. + * + * This function takes the menu $item by reference and attempts + * to deduce the object type and ID based on the router and its + * associated loader functions. + * + * @param &$item + * The menu item to be saved. + */ +function _menu_find_object_type(&$item) { + // If there is no router path, we cannot continue. + if (empty($item['router_path'])) { + return; + } + $loader = unserialize(db_query("SELECT load_functions FROM {menu_router} WHERE path = :path", array(':path' => $item['router_path']))->fetchField()); + if (isset($loader) && is_array($loader)) { + $item['object_type'] = str_replace('_load', '', current($loader)); + $parts = explode('/', $item['router_path']); + $data = explode('/', $item['link_path']); + // The router_path uses path/% and the link_path uses path/ID. + // Find the first % array element and match it to the path. + // This should be sufficient in most cases, since items with multiple + // loaders are not normally in the {menu_links} table. + foreach ($parts as $key => $value) { + // There are items in the {menu_links} table with no IDs. + if ($value == '%' && $data[$key] != '%') { + $item['object_id'] = $data[$key]; + } + } + } + // Remove unknown types. + if (empty($item['object_id']) && $item['object_type'] != 'external_link') { + $item['object_type'] = ''; + } +} + +/** * Insert, update or delete an uncustomized menu link related to a module. * * @param $module @@ -2346,7 +2406,7 @@ function _menu_find_router_path($link_pa * Title of the link to insert or new title to update the link to. * Unused for delete. * @return - * The insert op returns the mlid of the new item. Others op return NULL. + * The insert op returns the mlid of the new item. Other ops return NULL. */ function menu_link_maintain($module, $op, $link_path, $link_title) { switch ($op) { @@ -2359,21 +2419,11 @@ function menu_link_maintain($module, $op return menu_link_save($menu_link); break; case 'update': - db_update('menu_links') - ->fields(array('link_title' => $link_title)) - ->condition('link_path', $link_path) - ->condition('customized', 0) - ->condition('module', $module) - ->execute(); - $result = db_select('menu_links') - ->fields('menu_links', array('menu_name')) - ->condition('link_path', $link_path) - ->condition('customized', 0) - ->condition('module', $module) - ->groupBy('menu_name') - ->execute()->fetchCol(); - foreach ($result as $menu_name) { - menu_cache_clear($menu_name); + $result = db_query("SELECT * FROM {menu_links} WHERE link_path = :link_path AND module = :module AND customized = 0", array(':link_path' => $link_path, ':module' => $module))->fetchAll(PDO::FETCH_ASSOC); + foreach ($result as $item) { + $item['link_title'] = $link_title; + $item['options'] = unserialize($item['options']); + menu_link_save($item); } break; case 'delete': Index: modules/menu/menu.api.php =================================================================== RCS file: /cvs/drupal/drupal/modules/menu/menu.api.php,v retrieving revision 1.8 diff -u -p -r1.8 menu.api.php --- modules/menu/menu.api.php 9 May 2009 18:44:55 -0000 1.8 +++ modules/menu/menu.api.php 4 Jul 2009 16:32:52 -0000 @@ -154,5 +154,76 @@ function hook_translated_menu_link_alter } /** + * Inform modules that a menu link has been created. + * + * This hook is used to notify module that menu items have been + * created. Contributed modules may use the information to perform + * actions based on the information entered into the menu system. + * + * @param $item + * The $item record saved into the {menu_links} table. + * @return + * None. + * + * @see hook_menu_item_update() + * @see hook_menu_item_delete() + */ +function hook_menu_item_insert($item) { + // In our sample case, we track menu items as editing sections + // of the site. These are stored in our table as 'disabled' items. + $record['mlid'] = $item['mlid']; + $record['menu_name'] = $item['menu_name']; + $record['status'] = 0; + drupal_write_record('menu_example', $record); +} + +/** + * Inform modules that a menu link has been updated. + * + * This hook is used to notify module that menu items have been + * updated. Contributed modules may use the information to perform + * actions based on the information entered into the menu system. + * + * @param $item + * The $item record saved into the {menu_links} table. + * @return + * None. + * + * @see hook_menu_item_insert() + * @see hook_menu_item_delete() + */ +function hook_menu_item_update($item) { + // If the parent menu has changed, update our record. + $menu_name = db_result(db_query("SELECT mlid, menu_name, status FROM {menu_example} WHERE mlid = :mlid", array(':mlid' => $item['mlid']))); + if ($menu_name != $item['menu_name']) { + db_update('menu_example') + ->fields(array('menu_name' => $item['menu_name'])) + ->condition('mlid', $item['mlid']) + ->execute(); + } +} + +/** + * Inform modules that a menu link has been deleted. + * + * This hook is used to notify module that menu items have been + * deleted. Contributed modules may use the information to perform + * actions based on the information entered into the menu system. + * + * @param $item + * The $item record saved into the {menu_links} table. + * @return + * None. + * + * @see hook_menu_item_insert() + * @see hook_menu_item_update() + */ +function hook_menu_item_delete($item) { + // Delete the record from our table. + db_delete('menu_example') + ->condition('mlid', $item['mlid']) + ->execute(); +} +/** * @} End of "addtogroup hooks". */ Index: modules/simpletest/tests/menu.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/menu.test,v retrieving revision 1.11 diff -u -p -r1.11 menu.test --- modules/simpletest/tests/menu.test 30 May 2009 11:17:32 -0000 1.11 +++ modules/simpletest/tests/menu.test 4 Jul 2009 16:32:53 -0000 @@ -123,6 +123,39 @@ class MenuIncTestCase extends DrupalWebT $this->assertEqual($compare_item, $item, t('Modified menu item is equal to newly retrieved menu item.'), 'menu'); } + /** + * Test menu object storage. + */ + function testMenuObject() { + $node = $this->drupalCreateNode(array('type' => 'page', 'promote' => 0)); + $mlid = menu_link_maintain('menu_test', 'insert', "node/$node->nid", 'Menu link #1'); + // This should create a menu item with object type of 'node' and an ID of $node->nid. + $data = db_query("SELECT object_type, object_id FROM {menu_links} WHERE mlid = :mlid", array(':mlid' => $mlid))->fetchAssoc(); + $this->assertEqual($data['object_type'], 'node', t('Store a menu object type for a new menu item.')); + $this->assertEqual($data['object_id'], $node->nid, t('Store a menu object ID for a new menu item.')); + // Check to be sure that unknown items are not tracked. + $mlid = menu_link_maintain('menu_test', 'insert', 'menu_test_hierarchy_parent', 'Menu link custom'); + // This should create a menu item with NULL object type and object ID.. + $data = db_query("SELECT object_type, object_id FROM {menu_links} WHERE mlid = :mlid", array(':mlid' => $mlid))->fetchAssoc(); + $this->assertEqual($data['object_type'], '', t('Ignore a menu object type for a new menu item with no loader function.')); + $this->assertEqual($data['object_id'], '', t('Ignore a menu object ID for a new menu item with no loader function.')); + } + + /** + * Test menu maintainance hooks. + */ + function testMenuItemHooks() { + // Create an item. + menu_link_maintain('menu_test', 'insert', 'menu_test_maintain/4', 'Menu link #4'); + $this->assertEqual(menu_test_static_variable(), 'insert', t('hook_menu_item_insert() fired correctly')); + // Update the item. + menu_link_maintain('menu_test', 'update', 'menu_test_maintain/4', 'Menu link updated'); + $this->assertEqual(menu_test_static_variable(), 'update', t('hook_menu_item_update() fired correctly')); + // Delete the item. + menu_link_maintain('menu_test', 'delete', 'menu_test_maintain/4', ''); + $this->assertEqual(menu_test_static_variable(), 'delete', t('hook_menu_item_delete() fired correctly')); + } + } /** Index: modules/simpletest/tests/menu_test.module =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/menu_test.module,v retrieving revision 1.5 diff -u -p -r1.5 menu_test.module --- modules/simpletest/tests/menu_test.module 27 May 2009 18:34:00 -0000 1.5 +++ modules/simpletest/tests/menu_test.module 4 Jul 2009 16:32:53 -0000 @@ -30,7 +30,7 @@ function menu_test_menu() { 'title' => 'Menu maintain test', 'page callback' => 'node_page_default', 'access arguments' => array('access content'), - ); + ); // Hierarchical tests. $items['menu-test/hierarchy/parent'] = array( 'title' => 'Parent menu router', @@ -56,3 +56,49 @@ function menu_test_menu() { function menu_test_callback() { return $this->randomName(); } + +/** + * Implement hook_menu_item_insert(). + * + * @return + * A random string. + */ +function menu_test_menu_item_insert($item) { + menu_test_static_variable('insert'); +} + +/** + * Implement hook_menu_item_update(). + * + * @return + * A random string. + */ +function menu_test_menu_item_update($item) { + menu_test_static_variable('update'); +} + +/** + * Implement hook_menu_item_delete(). + * + * @return + * A random string. + */ +function menu_test_menu_item_delete($item) { + menu_test_static_variable('delete'); +} + +/** + * Static function for testing hook results. + * + * @param $value + * The value to set or NULL to return the current value. + * @return + * A text string for comparison to test assertions. + */ +function menu_test_static_variable($value = NULL) { + static $variable; + if (!empty($value)) { + $variable = $value; + } + return $variable; +} Index: modules/system/system.install =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.install,v retrieving revision 1.353 diff -u -p -r1.353 system.install --- modules/system/system.install 4 Jul 2009 14:45:36 -0000 1.353 +++ modules/system/system.install 4 Jul 2009 16:32:56 -0000 @@ -1148,12 +1148,27 @@ function system_schema() { 'default' => 0, 'size' => 'small', ), + 'object_type' => array( + 'description' => 'An object type identifier for connecting menu items to external systems.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'object_id' => array( + 'description' => 'A foreign key for objects stored in external systems.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), ), 'indexes' => array( 'path_menu' => array(array('link_path', 128), 'menu_name'), 'menu_plid_expand_child' => array('menu_name', 'plid', 'expanded', 'has_children'), 'menu_parents' => array('menu_name', 'p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7', 'p8', 'p9'), 'router_path' => array(array('router_path', 128)), + 'object' => array('object_type', 'object_id'), ), 'primary key' => array('mlid'), ); @@ -2226,6 +2241,25 @@ function system_update_7028() { } /** + * Add foreign object tracking data to {menu_links}. + */ +function system_update_7029() { + $ret = array(); + db_add_field($ret, 'menu_links', 'object_type', array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => '')); + db_add_field($ret, 'menu_links', 'object_id', array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => '')); + db_add_index($ret, 'menu_links', 'object', array('object_type', 'object_id')); + // Update the {menu_links} table, ignoring items with wildcard link_path entries. + $links = db_query("SELECT mlid, link_path, router_path, object_type, object_id FROM {menu_links} WHERE router_path LIKE '%/\%' AND router_path != link_path")->fetchAll(PDO::FETCH_ASSOC); + foreach ($links as $item) { + _menu_find_object_type($item); + if (isset($item['object_type'])) { + $ret[] = update_sql(sprintf("UPDATE {menu_links} SET object_type = '%s', object_id = '%s' WHERE mlid = %d", $item['object_type'], $item['object_id'], $item['mlid'])); + } + } + return $ret; +} + +/** * @} End of "defgroup updates-6.x-to-7.x" * The next series of updates should start at 8000. */