? 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 3 Jul 2009 19:29:24 -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,17 @@ 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) { + dsm('tes'); + $hook = 'menu_item_update'; + } + module_invoke_all($hook, $item); + // Now clear the cache. _menu_clear_page_cache(); } + return $item['mlid']; } @@ -2334,6 +2358,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 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 3 Jul 2009 19:29:26 -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 3 Jul 2009 19:29:26 -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.351 diff -u -p -r1.351 system.install --- modules/system/system.install 1 Jul 2009 12:03:35 -0000 1.351 +++ modules/system/system.install 3 Jul 2009 19:29:29 -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'), ); @@ -2261,7 +2276,24 @@ function system_update_7029() { return $ret; } - +/** + * Add foreign object tracking data to {menu_links}. + */ +function system_update_7030() { + $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"