? 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.
  */
