Admin Menu Editor - Version 1.4.1

Version Description

  • Fixed "Appearance -> Customize" always showing up as "new" and ignoring custom settings.
  • Fixed a WooCommerce 2.2.1+ compatibility issue that caused a superfluous "WooCommerce -> WooCommerce" submenu item to show up. Normally this item is invisible.
  • Fixed a bug where the plugin would fail to determine the current menu if the user tries to add a new item of a custom post type that doesn't have an "Add New" menu. Now it highlights the CPT parent menu instead.
  • Fixed a very obscure bug where certain old versions of PHP would crash if another plugin created a menu item using an absolute file name as the slug while AME was active. The crash was due to a known bug in PHP and only affected Windows systems with open_basedir enabled.
  • Added more debugging information for situations where the plugin can't save menu settings due to server configuration problems.
  • Other minor fixes.
Download this release

Release Info

Developer whiteshadow
Plugin Icon 128x128 Admin Menu Editor
Version 1.4.1
Comparing to
See all releases

Code changes from version 1.4 to 1.4.1

includes/menu-editor-core.php CHANGED
@@ -75,6 +75,7 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
75
  //Our personal copy of the request vars, without any "magic quotes".
76
  private $post = array();
77
  private $get = array();
 
78
 
79
  function init(){
80
  $this->sitewide_options = true;
@@ -267,6 +268,8 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
267
 
268
  //Compatibility fix for bbPress.
269
  $this->apply_bbpress_compat_fix();
 
 
270
  //Compatibility fix for WordPress Mu Domain Mapping.
271
  $this->apply_wpmu_domain_mapping_fix();
272
 
@@ -390,6 +393,23 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
390
  }
391
  }
392
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
393
  //Remove menus that have no accessible sub-menus and require privileges that the user does not have.
394
  //Ensure the rest are visible. Run re-parent loop again.
395
  foreach ( $menu as $id => $data ) {
@@ -1030,6 +1050,7 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
1030
  $priority--;
1031
  }
1032
 
 
1033
  $this->page_access_lookup[$item['url']][$priority] = $item['access_level'];
1034
  }
1035
 
@@ -1088,7 +1109,7 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
1088
  $items = $topmenu['items'];
1089
  //Sort by position
1090
  uasort($items, 'ameMenuItem::compare_position');
1091
-
1092
  foreach ($items as $item) {
1093
  //Skip missing and hidden items
1094
  if ( !empty($item['missing']) || !empty($item['hidden']) ) {
@@ -1441,9 +1462,24 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
1441
  try {
1442
  $menu = ameMenu::load_json($post['data'], true);
1443
  } catch (InvalidMenuException $ex) {
1444
- //Or redirect & display the error message
1445
- wp_redirect( add_query_arg('message', 2, $url) );
1446
- die();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1447
  }
1448
 
1449
  //Save the custom menu
@@ -1915,6 +1951,14 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
1915
 
1916
  $current_url = $this->parse_url($current_url);
1917
 
 
 
 
 
 
 
 
 
1918
  //Hook-based submenu pages can be accessed via both "parent-page.php?page=foo" and "admin.php?page=foo".
1919
  //WP has a private API function for determining the canonical parent page for the current request.
1920
  if ( $this->endsWith($current_url['path'], '/admin.php') && is_callable('get_admin_page_parent') ) {
@@ -1955,6 +1999,13 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
1955
  }
1956
  }
1957
 
 
 
 
 
 
 
 
1958
  //The current URL must match all query parameters of the item URL.
1959
  $different_params = array_diff_assoc($item_url['params'], $current_url['params']);
1960
 
@@ -1967,6 +2018,19 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
1967
  }
1968
  }
1969
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1970
  $cached_item = $best_item;
1971
  return $best_item;
1972
  }
@@ -2107,7 +2171,7 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
2107
  * @return void
2108
  */
2109
  function capture_request_vars(){
2110
- $this->post = $_POST;
2111
  $this->get = $_GET;
2112
 
2113
  if ( function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc() ) {
@@ -2324,6 +2388,36 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
2324
  }
2325
  }
2326
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2327
  /**
2328
  * Compatibility fix for WordPress Mu Domain Mapping 0.5.4.3.
2329
  *
75
  //Our personal copy of the request vars, without any "magic quotes".
76
  private $post = array();
77
  private $get = array();
78
+ private $originalPost = array();
79
 
80
  function init(){
81
  $this->sitewide_options = true;
268
 
269
  //Compatibility fix for bbPress.
270
  $this->apply_bbpress_compat_fix();
271
+ //Compatibility fix for WooCommerce (woo).
272
+ $this->apply_woocommerce_compat_fix();
273
  //Compatibility fix for WordPress Mu Domain Mapping.
274
  $this->apply_wpmu_domain_mapping_fix();
275
 
393
  }
394
  }
395
 
396
+ //Remove consecutive submenu separators. This can happen if there are separators around a menu item
397
+ //that is not accessible to the current user.
398
+ foreach ($submenu as $parent => $items) {
399
+ $found_separator = false;
400
+ foreach ($items as $index => $item) {
401
+ //Separator have a dummy #anchor as a URL. See wsMenuEditorExtras::create_submenu_separator().
402
+ if (strpos($item[2], '#submenu-separator-') === 0) {
403
+ if ( $found_separator ) {
404
+ unset($submenu[$parent][$index]);
405
+ }
406
+ $found_separator = true;
407
+ } else {
408
+ $found_separator = false;
409
+ }
410
+ }
411
+ }
412
+
413
  //Remove menus that have no accessible sub-menus and require privileges that the user does not have.
414
  //Ensure the rest are visible. Run re-parent loop again.
415
  foreach ( $menu as $id => $data ) {
1050
  $priority--;
1051
  }
1052
 
1053
+ //TODO: Include more details like menu title and template ID for debugging purposes (log output).
1054
  $this->page_access_lookup[$item['url']][$priority] = $item['access_level'];
1055
  }
1056
 
1109
  $items = $topmenu['items'];
1110
  //Sort by position
1111
  uasort($items, 'ameMenuItem::compare_position');
1112
+
1113
  foreach ($items as $item) {
1114
  //Skip missing and hidden items
1115
  if ( !empty($item['missing']) || !empty($item['hidden']) ) {
1462
  try {
1463
  $menu = ameMenu::load_json($post['data'], true);
1464
  } catch (InvalidMenuException $ex) {
1465
+ $debugData = '';
1466
+ $debugData .= "Exception:\n" . $ex->getMessage() . "\n\n";
1467
+ $debugData .= "Used POST data:\n" . print_r($this->post, true) . "\n\n";
1468
+ $debugData .= "Original POST:\n" . print_r($this->originalPost, true) . "\n\n";
1469
+ $debugData .= "\$_POST global:\n" . print_r($_POST, true);
1470
+
1471
+ $debugData = sprintf(
1472
+ "<textarea rows=\"30\" cols=\"100\">%s</textarea>",
1473
+ htmlentities($debugData)
1474
+ );
1475
+
1476
+ wp_die(
1477
+ "Error: Failed to decode menu data!<br><br>\n"
1478
+ . "Please send this debugging information to the developer: <br>"
1479
+ . $debugData
1480
+ );
1481
+
1482
+ return;
1483
  }
1484
 
1485
  //Save the custom menu
1951
 
1952
  $current_url = $this->parse_url($current_url);
1953
 
1954
+ //Special case: if post_type is not specified for edit.php and post-new.php,
1955
+ //WordPress assumes it is "post". Here we make this explicit.
1956
+ if ( $this->endsWith($current_url['path'], '/wp-admin/edit.php') || $this->endsWith($current_url['path'], '/wp-admin/post-new.php') ) {
1957
+ if ( !isset($current_url['params']['post_type']) ) {
1958
+ $current_url['params']['post_type'] = 'post';
1959
+ }
1960
+ }
1961
+
1962
  //Hook-based submenu pages can be accessed via both "parent-page.php?page=foo" and "admin.php?page=foo".
1963
  //WP has a private API function for determining the canonical parent page for the current request.
1964
  if ( $this->endsWith($current_url['path'], '/admin.php') && is_callable('get_admin_page_parent') ) {
1999
  }
2000
  }
2001
 
2002
+ //Same as above - default post type is "post".
2003
+ if ( $this->endsWith($item_url['path'], '/wp-admin/edit.php') || $this->endsWith($item_url['path'], '/wp-admin/post-new.php') ) {
2004
+ if ( !isset($item_url['params']['post_type']) ) {
2005
+ $item_url['params']['post_type'] = 'post';
2006
+ }
2007
+ }
2008
+
2009
  //The current URL must match all query parameters of the item URL.
2010
  $different_params = array_diff_assoc($item_url['params'], $current_url['params']);
2011
 
2018
  }
2019
  }
2020
 
2021
+ //Special case for CPTs: When the "Add New" menu is disabled by CPT settings (show_ui, etc), and someone goes
2022
+ //to add a new item, WordPress highlights the "$CPT-Name" item as the current one. Lets do the same for
2023
+ //consistency. See also: /wp-admin/post-new.php, lines #20 to #40.
2024
+ if (
2025
+ ($best_item === null)
2026
+ && isset($current_url['params']['post_type'])
2027
+ && (!empty($current_url['params']['post_type']))
2028
+ && $this->endsWith($current_url['path'], '/wp-admin/post-new.php')
2029
+ && isset($this->reverse_item_lookup['edit.php?post_type=' . $current_url['params']['post_type']])
2030
+ ) {
2031
+ $best_item = $this->reverse_item_lookup['edit.php?post_type=' . $current_url['params']['post_type']];
2032
+ }
2033
+
2034
  $cached_item = $best_item;
2035
  return $best_item;
2036
  }
2171
  * @return void
2172
  */
2173
  function capture_request_vars(){
2174
+ $this->post = $this->originalPost = $_POST;
2175
  $this->get = $_GET;
2176
 
2177
  if ( function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc() ) {
2388
  }
2389
  }
2390
 
2391
+ /**
2392
+ * Compatibility fix for WooCommerce 2.2.1+.
2393
+ * Summary: When AME is active, an unusable WooCommerce -> WooCommerce menu item shows up. Here we remove it.
2394
+ *
2395
+ * WooCommerce creates a top level "WooCommerce" menu with no callback. By default, WordPress automatically adds
2396
+ * a submenu item with the same name. However, since the item doesn't have a callback, it is unusable and clicking
2397
+ * it just triggers a "Cannot load woocommerce" error. So WooCommerce removes this item in an admin_head hook to
2398
+ * hide it. With AME active, the item shows up anyway, and users get confused by the error.
2399
+ *
2400
+ * Fix it by removing the problematic menu item.
2401
+ *
2402
+ * Caution: If the user hides all WooCommerce submenus but not the top level menu, the WooCommerce menu will still
2403
+ * show up but be inaccessible. This may be slightly counter-intuitive, but seems reasonable.
2404
+ */
2405
+ private function apply_woocommerce_compat_fix() {
2406
+ if ( !isset($this->default_wp_submenu, $this->default_wp_submenu['woocommerce']) ) {
2407
+ return;
2408
+ }
2409
+
2410
+ $badSubmenuExists = isset($this->default_wp_submenu['woocommerce'][0])
2411
+ && isset($this->default_wp_submenu['woocommerce'][0][2])
2412
+ && ($this->default_wp_submenu['woocommerce'][0][2] === 'woocommerce');
2413
+ $anotherSubmenuExists = isset($this->default_wp_submenu['woocommerce'][1]);
2414
+
2415
+ if ( $badSubmenuExists && $anotherSubmenuExists ) {
2416
+ $this->default_wp_submenu['woocommerce'][0] = $this->default_wp_submenu['woocommerce'][1];
2417
+ unset($this->default_wp_submenu['woocommerce'][1]);
2418
+ }
2419
+ }
2420
+
2421
  /**
2422
  * Compatibility fix for WordPress Mu Domain Mapping 0.5.4.3.
2423
  *
includes/menu-item.php CHANGED
@@ -223,6 +223,14 @@ abstract class ameMenuItem {
223
  $parent_file = 'users.php';
224
  }
225
 
 
 
 
 
 
 
 
 
226
  return $parent_file . '>' . $item_file;
227
  }
228
 
@@ -379,6 +387,11 @@ abstract class ameMenuItem {
379
  $menu_url = is_array($item_slug) ? self::get($item_slug, 'file') : $item_slug;
380
  $parent_url = !empty($parent_slug) ? $parent_slug : 'admin.php';
381
 
 
 
 
 
 
382
  if ( strpos($menu_url, '://') !== false ) {
383
  return $menu_url;
384
  }
@@ -398,13 +411,35 @@ abstract class ameMenuItem {
398
  }
399
  $pageFile = self::remove_query_from($page_url);
400
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
401
  //Check our hard-coded list of admin pages first. It's measurably faster than
402
  //hitting the disk with is_file().
403
  if ( isset(self::$known_wp_admin_files[$pageFile]) ) {
404
  return false;
405
  }
 
406
  //Now actually check the filesystem.
407
- $adminFileExists = is_file(ABSPATH . '/wp-admin/' . $pageFile);
408
  if ( $adminFileExists ) {
409
  return false;
410
  }
@@ -414,7 +449,10 @@ abstract class ameMenuItem {
414
  return true;
415
  }
416
 
417
- $pluginFileExists = ($page_url != 'index.php') && is_file(WP_PLUGIN_DIR . '/' . $pageFile);
 
 
 
418
  if ( $pluginFileExists ) {
419
  return true;
420
  }
223
  $parent_file = 'users.php';
224
  }
225
 
226
+ //Special case: In WP 4.0+ the URL of the "Appearance -> Customize" item is different on every admin page.
227
+ //This is because the URL includes a "return" parameter that contains the current page's URL. It also makes
228
+ //the template ID different on every page, so it's impossible to identify the menu. To fix that, lets remove
229
+ //the "return" parameter from the ID.
230
+ if ( ($parent_file === 'themes.php') && (strpos($item_file, 'customize.php?') === 0) ) {
231
+ $item_file = remove_query_arg('return', $item_file);
232
+ }
233
+
234
  return $parent_file . '>' . $item_file;
235
  }
236
 
387
  $menu_url = is_array($item_slug) ? self::get($item_slug, 'file') : $item_slug;
388
  $parent_url = !empty($parent_slug) ? $parent_slug : 'admin.php';
389
 
390
+ //Workaround for WooCommerce 2.1.12: For some reason, it uses "&amp;" instead of a plain "&" to separate
391
+ //query parameters. We need a plain URL, not a HTML-entity-encoded one.
392
+ //It is theoretically possible that another plugin might want to use a literal "&amp;", but its very unlikely.
393
+ $menu_url = str_replace('&amp;', '&', $menu_url);
394
+
395
  if ( strpos($menu_url, '://') !== false ) {
396
  return $menu_url;
397
  }
411
  }
412
  $pageFile = self::remove_query_from($page_url);
413
 
414
+ /*
415
+ * Special case: Absolute paths.
416
+ *
417
+ * - add_submenu_page() applies plugin_basename() to the menu slug, so we don't need to worry about plugin
418
+ * paths. However, absolute paths that *don't* point point to the plugins directory can be a problem.
419
+ *
420
+ * - If we blindly append $pageFile to another path, we'll get something like "C:\a\b/wp-admin/C:\c\d.php".
421
+ * PHP 5.2.5 has a known bug where calling file_exists() on that kind of an invalid filename will cause
422
+ * a timeout and a crash in some configurations. See: https://bugs.php.net/bug.php?id=44412
423
+ *
424
+ * - WP 3.9.2 and 4.0+ unintentionally break menu URLs like "foo.php?page=c:\a\b.php" because esc_url()
425
+ * interprets the part before the colon as an invalid protocol. As a result, such links have an empty URL
426
+ * on Windows (but they might still work on other OS).
427
+ *
428
+ * - Recent versions of WP won't let you load a PHP file from outside the plugins and mu-plugins directories
429
+ * with "admin.php?page=filename". See the validate_file() call in /wp-admin/admin.php. However, such filenames
430
+ * can still be used as unique slugs for menus with hook callbacks, so we shouldn't reject them outright.
431
+ * Related: https://core.trac.wordpress.org/ticket/10011
432
+ */
433
+ $allowPathConcatenation = (substr($pageFile, 1, 1) !== ':'); //Reject "C:\whatever" and similar.
434
+
435
  //Check our hard-coded list of admin pages first. It's measurably faster than
436
  //hitting the disk with is_file().
437
  if ( isset(self::$known_wp_admin_files[$pageFile]) ) {
438
  return false;
439
  }
440
+
441
  //Now actually check the filesystem.
442
+ $adminFileExists = $allowPathConcatenation && is_file(ABSPATH . 'wp-admin/' . $pageFile);
443
  if ( $adminFileExists ) {
444
  return false;
445
  }
449
  return true;
450
  }
451
 
452
+ //Note: We don't need to call plugin_basename() on $pageFile because add_submenu_page() already did that.
453
+ $pluginFileExists = $allowPathConcatenation
454
+ && ($page_url != 'index.php')
455
+ && is_file(WP_PLUGIN_DIR . '/' . $pageFile);
456
  if ( $pluginFileExists ) {
457
  return true;
458
  }
menu-editor.php CHANGED
@@ -3,7 +3,7 @@
3
  Plugin Name: Admin Menu Editor
4
  Plugin URI: http://w-shadow.com/blog/2008/12/20/admin-menu-editor-for-wordpress/
5
  Description: Lets you directly edit the WordPress admin menu. You can re-order, hide or rename existing menus, add custom menus and more.
6
- Version: 1.4
7
  Author: Janis Elsts
8
  Author URI: http://w-shadow.com/blog/
9
  */
3
  Plugin Name: Admin Menu Editor
4
  Plugin URI: http://w-shadow.com/blog/2008/12/20/admin-menu-editor-for-wordpress/
5
  Description: Lets you directly edit the WordPress admin menu. You can re-order, hide or rename existing menus, add custom menus and more.
6
+ Version: 1.4.1
7
  Author: Janis Elsts
8
  Author URI: http://w-shadow.com/blog/
9
  */
readme.txt CHANGED
@@ -3,8 +3,8 @@ Contributors: whiteshadow
3
  Donate link: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A6P9S6CE3SRSW
4
  Tags: admin, dashboard, menu, security, wpmu
5
  Requires at least: 3.8
6
- Tested up to: 4.0-beta2
7
- Stable tag: 1.4
8
 
9
  Lets you edit the WordPress admin menu. You can re-order, hide or rename menus, add custom menus and more.
10
 
@@ -63,6 +63,14 @@ Plugins installed in the `mu-plugins` directory are treated as "always on", so y
63
 
64
  == Changelog ==
65
 
 
 
 
 
 
 
 
 
66
  = 1.4 =
67
  * Added a special target page option: "< None >". It makes the selected menu item unclickable. This could be useful for creating menu headers and so on.
68
  * Added a new menu editor colour scheme that's similar to the default WordPress admin colour scheme. Click the "Settings" button next to the menu editor page title to switch colour schemes.
3
  Donate link: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A6P9S6CE3SRSW
4
  Tags: admin, dashboard, menu, security, wpmu
5
  Requires at least: 3.8
6
+ Tested up to: 4.0
7
+ Stable tag: 1.4.1
8
 
9
  Lets you edit the WordPress admin menu. You can re-order, hide or rename menus, add custom menus and more.
10
 
63
 
64
  == Changelog ==
65
 
66
+ = 1.4.1 =
67
+ * Fixed "Appearance -> Customize" always showing up as "new" and ignoring custom settings.
68
+ * Fixed a WooCommerce 2.2.1+ compatibility issue that caused a superfluous "WooCommerce -> WooCommerce" submenu item to show up. Normally this item is invisible.
69
+ * Fixed a bug where the plugin would fail to determine the current menu if the user tries to add a new item of a custom post type that doesn't have an "Add New" menu. Now it highlights the CPT parent menu instead.
70
+ * Fixed a very obscure bug where certain old versions of PHP would crash if another plugin created a menu item using an absolute file name as the slug while AME was active. The crash was due to a known bug in PHP and only affected Windows systems with open_basedir enabled.
71
+ * Added more debugging information for situations where the plugin can't save menu settings due to server configuration problems.
72
+ * Other minor fixes.
73
+
74
  = 1.4 =
75
  * Added a special target page option: "< None >". It makes the selected menu item unclickable. This could be useful for creating menu headers and so on.
76
  * Added a new menu editor colour scheme that's similar to the default WordPress admin colour scheme. Click the "Settings" button next to the menu editor page title to switch colour schemes.