In function _menu_link_build($item) this code is sub-optimal:

...

  $placeholders = implode(', ', array_fill(0, count($menu), "'%s'"));
  // Remove items if their router path does not exist any more.
  db_query('DELETE FROM {menu_links} WHERE router_path NOT IN ('. $placeholders .')', array_keys($menu));

since it doesn't use the API function, it's possible we'll have orphaned links. It should probably be something like:

...

  $placeholders = implode(', ', array_fill(0, count($menu), "'%s'"));
  // Remove items if their router path does not exist any more.
  $result = db_query('SELECT mlid FROM {menu_links} WHERE router_path NOT IN ('. $placeholders .')', array_keys($menu));
  while ($m = db_fetch_array($result)) {
    menu_link_delete($m['mlid']);
  }

but a patch needs to wait for: http://drupal.org/node/151583

Comments

pwolanin’s picture

Priority: Normal » Critical

A related bug whose fix should be included with this patch: http://drupal.org/node/166336

Links to and addresses outside of drupal are given a router_path of '' (empty string) in the menu_links table. Each time the menu is rebuilt, _menu_navigation_links_rebuild checks to make sure that all of the links belong to an existing router_path. '' (empty string) is not in this list, so all links to <front> or addresses outside of drupal are removed every time the menu is rebuilt. The removal is done at this line:

db_query('DELETE FROM {menu_links} WHERE router_path NOT IN ('. $placeholders .')', array_keys($menu));

The issue can be fixed by adding '' to the list of valid router_paths, like this:

db_query('DELETE FROM {menu_links} WHERE router_path NOT IN (\'\', '. $placeholders .')', array_keys($menu));

It could also be fixed by checking to make sure router_path is not empty. I'm not sure which way is more consistent with the way things are done in drupal core, I'll leave that up to the reviewer.

pwolanin’s picture

Status: Active » Needs review
StatusFileSize
new1.1 KB

perhaps this LEFT JOIN will be faster?

pwolanin’s picture

StatusFileSize
new1.07 KB

Here's a patch back to the original form of the query. It's not really clear to me which is better (either works): here are the MySQL explain results (the 202 unique router paths omitted for clarity) :

EXPLAIN SELECT mlid FROM menu_links WHERE router_path NOT IN (...) AND external = 0 ORDER BY depth DESC
+-------------+------------+------+---------------+------+---------+------+------+-----------------------------+
| select_type | table      | type | possible_keys | key  | key_len | ref  | rows | Extra                       |
+-------------+------------+------+---------------+------+---------+------+------+-----------------------------+
| SIMPLE      | menu_links | ALL  | router_path   | NULL | NULL    | NULL |  339 | Using where; Using filesort | 
+-------------+------------+------+---------------+------+---------+------+------+-----------------------------+
EXPLAIN SELECT mlid FROM menu_links ml LEFT JOIN menu_router m ON m.path  = ml.router_path WHERE ml.external = 0 AND m.path IS NULL ORDER BY ml.depth DESC; 

+-------------+-------+--------+---------------+---------+---------+----------------+------+--------------------------------------+
| select_type | table | type   | possible_keys | key     | key_len | ref            | rows | Extra    +-------------+-------+--------+---------------+---------+---------+----------------+------+--------------------------------------+
| SIMPLE      | ml    | ALL    | NULL          | NULL    | NULL    | NULL           |  339 | Using where; Using filesort 
| SIMPLE      | m     | eq_ref | PRIMARY       | PRIMARY | 767     | ml.router_path |    1 | Using where; Using index; Not exists
+-------------+-------+--------+---------------+---------+---------+----------------+------+--------------------------------------+

I'm guessing that the first (the one used in this patch) may be faster since it has the possibility of using a key (though it doesn't in this case) and it only operates on a single table. Since this is a rare operation, speed isn't critical.

chx’s picture

Status: Needs review » Reviewed & tested by the community

Code is OK and does what it needs to do. I think it's good to go.

dries’s picture

Status: Reviewed & tested by the community » Fixed

Committed to CVS HEAD. Thanks.

pwolanin’s picture

Status: Fixed » Needs review
StatusFileSize
new2 KB

crud - apparently I am too smart for myself and didn't test this thoroughly enough.

We have as a safety check in function _menu_delete_item():

  // System-created items get automatically deleted, but only on menu rebuild.
  if ($item && $item['module'] != 'system') {

So, our safety check defeats our intent to use the API function and the code code in menu.inc leaves behind any system-created links.

Further fix attached - adds a "rebuild" flag to function _menu_delete_item() to indicate that we are doing a menu rebuild and should bypass this safety check (and also bypass clearing the menu cache, since it's already cleared).

webernet’s picture

Status: Needs review » Reviewed & tested by the community

Tested and looks OK.

gábor hojtsy’s picture

Status: Reviewed & tested by the community » Fixed

Committed to Drupal 6, thanks.

Anonymous’s picture

Status: Fixed » Closed (fixed)