Admin Menu Editor - Version 1.9.7

Version Description

  • Fixed a conflict with Elementor 3.0.0-beta that caused the "Theme Builder" menu item to have the wrong URL.
  • Minor performance optimization.
Download this release

Release Info

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

Code changes from version 1.9.6 to 1.9.7

Files changed (3) hide show
  1. includes/menu-editor-core.php +160 -45
  2. menu-editor.php +1 -1
  3. readme.txt +6 -2
includes/menu-editor-core.php CHANGED
@@ -81,6 +81,12 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
81
  */
82
  private $cached_user_roles = array();
83
 
 
 
 
 
 
 
84
  /**
85
  * @var array An index of URLs relative to /wp-admin/. Any menus that match the index will be ignored.
86
  */
@@ -335,11 +341,15 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
335
  //Reset plugin access if the only allowed user gets deleted or their ID changes.
336
  add_action('wp_login', array($this, 'maybe_reset_plugin_access'), 10, 2);
337
 
338
- //Workaround for buggy plugins that unintentionally remove user roles.
339
- /** @see WPMenuEditor::get_user_roles */
 
 
 
340
  add_action('set_current_user', array($this, 'update_current_user_cache'), 1, 0); //Run before most plugins.
341
- add_action('updated_user_meta', array($this, 'clear_user_role_cache'), 10, 2);
342
- add_action('deleted_user_meta', array($this, 'clear_user_role_cache'), 10, 2);
 
343
  //There's also a "set_user_role" hook, but it's only called by WP_User::set_role and not WP_User::add_role.
344
  //It's also redundant - WP_User::set_role updates user meta, so the above hooks already cover it.
345
 
@@ -656,7 +666,7 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
656
  $submenu = $this->custom_wp_submenu;
657
 
658
  $this->user_cap_cache_enabled = true;
659
- list($menu, $submenu) = $this->filter_menu($menu, $submenu);
660
  $this->user_cap_cache_enabled = false;
661
 
662
  do_action('admin_menu_editor-menu_replaced');
@@ -686,11 +696,13 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
686
  *
687
  * - Adds position-dependent CSS classes.
688
  *
689
- * @param array $menu
690
- * @param array $submenu
691
- * @return array An array with two items - the filtered menu and submenu.
 
692
  */
693
- private function filter_menu($menu, $submenu) {
 
694
  global $_wp_menu_nopriv; //Caution: Modifying this array could lead to unexpected consequences.
695
 
696
  //Remove sub-menus which the user shouldn't be able to access,
@@ -783,8 +795,6 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
783
 
784
  //Add display-specific classes like "menu-top-first" and others.
785
  $menu = add_menu_classes($menu);
786
-
787
- return array($menu, $submenu);
788
  }
789
 
790
  public function register_base_dependencies() {
@@ -1652,7 +1662,7 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
1652
  }
1653
  }
1654
 
1655
- if (is_array($topmenu['items'])) {
1656
  //Iterate over submenu items
1657
  foreach ($topmenu['items'] as &$item){
1658
  if ( !ameMenuItem::get($item, 'custom') ) {
@@ -2040,7 +2050,7 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
2040
  // menu or the top level. We'll need to adjust the file field to point to the correct URL.
2041
  // This is required because WP identifies plugin pages using *both* the plugin file
2042
  // and the parent file.
2043
- if ( $item['template_id'] !== '' && !$item['separator'] ) {
2044
  $template = $this->item_templates[$item['template_id']];
2045
  if ( $template['defaults']['is_plugin_page'] ) {
2046
  $default_parent = $template['defaults']['parent'];
@@ -2112,7 +2122,7 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
2112
 
2113
  //Menus without a custom icon image should have it set to "none" (or "div" in older WP versions).
2114
  //See /wp-admin/menu-header.php for details on how this works.
2115
- if ( $item['icon_url'] === '' ) {
2116
  $item['icon_url'] = 'none';
2117
  }
2118
 
@@ -2157,7 +2167,7 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
2157
 
2158
  //WPML support: Use translated menu titles where available.
2159
  if (
2160
- !$item['separator'] && $hasCustomMenuTitle && function_exists('icl_t')
2161
  && !empty($this->options['wpml_support_enabled'])
2162
  ) {
2163
  $item['menu_title'] = icl_t(
@@ -2985,6 +2995,7 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
2985
  $this->cached_virtual_caps = null;
2986
  $this->cached_user_caps = array();
2987
  $this->cached_user_roles = array();
 
2988
 
2989
  if ($this->options['menu_config_scope'] === 'site') {
2990
  $this->cached_custom_menu = null;
@@ -2992,27 +3003,6 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
2992
  }
2993
  }
2994
 
2995
- /**
2996
- * Create a virtual 'super_admin' capability that only super admins have.
2997
- * This function accomplishes that by by filtering 'user_has_cap' calls.
2998
- *
2999
- * @param array $allcaps All capabilities belonging to the current user, cap => true/false.
3000
- * @param array $required_caps The required capabilities.
3001
- * @param array $args The capability passed to current_user_can, the current user's ID, and other args.
3002
- * @return array Filtered version of $allcaps
3003
- */
3004
- function hook_user_has_cap($allcaps, /** @noinspection PhpUnusedParameterInspection */ $required_caps, $args){
3005
- //Be careful not to overwrite a super_admin cap added by other plugins
3006
- //For example, Advanced Access Manager also adds this capability.
3007
- if ( is_array($allcaps) && !isset($allcaps['super_admin']) ){
3008
- $user_id = intval($args[1]);
3009
- if ( $user_id != 0 ) {
3010
- $allcaps['super_admin'] = is_multisite() && is_super_admin($user_id);
3011
- }
3012
- }
3013
- return $allcaps;
3014
- }
3015
-
3016
  /**
3017
  * AJAX callback for saving screen options (whether to show or to hide advanced menu options).
3018
  *
@@ -3367,8 +3357,16 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
3367
  * @return array
3368
  */
3369
  private function parse_url($url) {
3370
- $url_defaults = array_fill_keys(array('scheme', 'host', 'port', 'user', 'pass', 'path', 'query', 'fragment'), '');
3371
- $url_defaults['port'] = '80';
 
 
 
 
 
 
 
 
3372
 
3373
  $parsed = @parse_url($url);
3374
  if ( !is_array($parsed) ) {
@@ -3377,7 +3375,7 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
3377
  $parsed = array_merge($url_defaults, $parsed);
3378
 
3379
  $params = array();
3380
- if (!empty($parsed['query'])) {
3381
  wp_parse_str($parsed['query'], $params);
3382
  };
3383
  $parsed['params'] = $params;
@@ -3458,7 +3456,11 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
3458
  if ( $len == 0 ) {
3459
  return true;
3460
  }
3461
- return substr($string, -$len) === $suffix;
 
 
 
 
3462
  }
3463
 
3464
  public function castValuesToBool($capabilities) {
@@ -4038,14 +4040,95 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
4038
  }
4039
 
4040
  /**
4041
- * The current user has changed; cache their roles.
4042
  */
4043
  public function update_current_user_cache() {
4044
  $user = wp_get_current_user();
4045
  if ( empty($user) || !$user->exists() ) {
4046
  return;
4047
  }
 
 
 
4048
  $this->cached_user_roles[$user->ID] = $this->extract_user_roles($user);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4049
  }
4050
 
4051
  /**
@@ -4069,19 +4152,51 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
4069
  }
4070
 
4071
  /**
4072
- * User metadata was updated or deleted; invalidate the role cache.
4073
  *
4074
- * Not all metadata updates are related to role changes, but filtering them is non-trivial (meta keys change)
4075
- * and not really necessary for our purposes.
4076
  *
4077
  * @param int|array $unused_meta_id
4078
  * @param int $user_id
 
 
4079
  */
4080
- public function clear_user_role_cache(/** @noinspection PhpUnusedParameterInspection */$unused_meta_id, $user_id) {
4081
  if ( empty($user_id) || !is_numeric($user_id) ) {
4082
  return;
4083
  }
 
4084
  unset($this->cached_user_roles[$user_id]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4085
  }
4086
 
4087
  /**
81
  */
82
  private $cached_user_roles = array();
83
 
84
+ private $cached_virtual_user_caps = array();
85
+ private $virtual_caps_for_this_call = array();
86
+
87
+ public $disable_virtual_caps = false;
88
+ public $virtual_cap_mode = 3; //self::ALL_VIRTUAL_CAPS
89
+
90
  /**
91
  * @var array An index of URLs relative to /wp-admin/. Any menus that match the index will be ignored.
92
  */
341
  //Reset plugin access if the only allowed user gets deleted or their ID changes.
342
  add_action('wp_login', array($this, 'maybe_reset_plugin_access'), 10, 2);
343
 
344
+ //Grant virtual capabilities like "super_user" to users.
345
+ add_filter('user_has_cap', array($this, 'grant_virtual_caps_to_user'), 9, 3);
346
+ add_filter('user_has_cap', array($this, 'regrant_virtual_caps_to_user'), 200, 1);
347
+
348
+ //Update caches when the current user changes.
349
  add_action('set_current_user', array($this, 'update_current_user_cache'), 1, 0); //Run before most plugins.
350
+ //Clear or refresh per-user caches when the user's roles or capabilities change.
351
+ add_action('updated_user_meta', array($this, 'on_user_metadata_changed'), 10, 3);
352
+ add_action('deleted_user_meta', array($this, 'on_user_metadata_changed'), 10, 3);
353
  //There's also a "set_user_role" hook, but it's only called by WP_User::set_role and not WP_User::add_role.
354
  //It's also redundant - WP_User::set_role updates user meta, so the above hooks already cover it.
355
 
666
  $submenu = $this->custom_wp_submenu;
667
 
668
  $this->user_cap_cache_enabled = true;
669
+ $this->filter_global_menu();
670
  $this->user_cap_cache_enabled = false;
671
 
672
  do_action('admin_menu_editor-menu_replaced');
696
  *
697
  * - Adds position-dependent CSS classes.
698
  *
699
+ * @global array $menu
700
+ * @global array $submenu
701
+ *
702
+ * @return void
703
  */
704
+ private function filter_global_menu() {
705
+ global $menu, $submenu;
706
  global $_wp_menu_nopriv; //Caution: Modifying this array could lead to unexpected consequences.
707
 
708
  //Remove sub-menus which the user shouldn't be able to access,
795
 
796
  //Add display-specific classes like "menu-top-first" and others.
797
  $menu = add_menu_classes($menu);
 
 
798
  }
799
 
800
  public function register_base_dependencies() {
1662
  }
1663
  }
1664
 
1665
+ if (!empty($topmenu['items'])) {
1666
  //Iterate over submenu items
1667
  foreach ($topmenu['items'] as &$item){
1668
  if ( !ameMenuItem::get($item, 'custom') ) {
2050
  // menu or the top level. We'll need to adjust the file field to point to the correct URL.
2051
  // This is required because WP identifies plugin pages using *both* the plugin file
2052
  // and the parent file.
2053
+ if ( $item['template_id'] !== '' && empty($item['separator']) ) {
2054
  $template = $this->item_templates[$item['template_id']];
2055
  if ( $template['defaults']['is_plugin_page'] ) {
2056
  $default_parent = $template['defaults']['parent'];
2122
 
2123
  //Menus without a custom icon image should have it set to "none" (or "div" in older WP versions).
2124
  //See /wp-admin/menu-header.php for details on how this works.
2125
+ if ( !isset($item['icon_url']) || ($item['icon_url'] === '') ) {
2126
  $item['icon_url'] = 'none';
2127
  }
2128
 
2167
 
2168
  //WPML support: Use translated menu titles where available.
2169
  if (
2170
+ empty($item['separator']) && $hasCustomMenuTitle && function_exists('icl_t')
2171
  && !empty($this->options['wpml_support_enabled'])
2172
  ) {
2173
  $item['menu_title'] = icl_t(
2995
  $this->cached_virtual_caps = null;
2996
  $this->cached_user_caps = array();
2997
  $this->cached_user_roles = array();
2998
+ $this->cached_virtual_user_caps = array();
2999
 
3000
  if ($this->options['menu_config_scope'] === 'site') {
3001
  $this->cached_custom_menu = null;
3003
  }
3004
  }
3005
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3006
  /**
3007
  * AJAX callback for saving screen options (whether to show or to hide advanced menu options).
3008
  *
3357
  * @return array
3358
  */
3359
  private function parse_url($url) {
3360
+ static $url_defaults = array(
3361
+ 'scheme' => '',
3362
+ 'host' => '',
3363
+ 'port' => '80',
3364
+ 'user' => '',
3365
+ 'pass' => '',
3366
+ 'path' => '',
3367
+ 'query' => '',
3368
+ 'fragment' => '',
3369
+ );
3370
 
3371
  $parsed = @parse_url($url);
3372
  if ( !is_array($parsed) ) {
3375
  $parsed = array_merge($url_defaults, $parsed);
3376
 
3377
  $params = array();
3378
+ if ( !empty($parsed['query']) ) {
3379
  wp_parse_str($parsed['query'], $params);
3380
  };
3381
  $parsed['params'] = $params;
3456
  if ( $len == 0 ) {
3457
  return true;
3458
  }
3459
+ $inputLen = strlen($string);
3460
+ if ( $len > $inputLen ) {
3461
+ return false;
3462
+ }
3463
+ return substr_compare($string, $suffix, $inputLen - $len) === 0;
3464
  }
3465
 
3466
  public function castValuesToBool($capabilities) {
4040
  }
4041
 
4042
  /**
4043
+ * The current user has changed; update role and capability caches.
4044
  */
4045
  public function update_current_user_cache() {
4046
  $user = wp_get_current_user();
4047
  if ( empty($user) || !$user->exists() ) {
4048
  return;
4049
  }
4050
+
4051
+ //Workaround for buggy plugins that unintentionally remove user roles.
4052
+ /** @see WPMenuEditor::get_user_roles */
4053
  $this->cached_user_roles[$user->ID] = $this->extract_user_roles($user);
4054
+
4055
+ $this->update_virtual_cap_cache($user);
4056
+ }
4057
+
4058
+ /**
4059
+ * @param WP_User $user
4060
+ */
4061
+ private function update_virtual_cap_cache($user) {
4062
+ if ( $user === null ) {
4063
+ return;
4064
+ }
4065
+
4066
+ $virtual_caps = array(
4067
+ self::ALL_VIRTUAL_CAPS => array(),
4068
+ self::DIRECTLY_GRANTED_VIRTUAL_CAPS => array(),
4069
+ );
4070
+
4071
+ //Create a virtual 'super_admin' capability that only super admins have. Be careful not to overwrite
4072
+ //the same cap added by other plugins. For example, Advanced Access Manager also adds this capability.
4073
+ if ( !isset($user->allcaps['super_admin']) ) {
4074
+ $virtual_caps[self::ALL_VIRTUAL_CAPS]['super_admin'] = is_multisite() && is_super_admin($user->ID);
4075
+ }
4076
+
4077
+ $virtual_caps = apply_filters('admin_menu_editor-virtual_caps', $virtual_caps, $user);
4078
+ $this->cached_virtual_user_caps[$user->ID] = $virtual_caps;
4079
+ }
4080
+
4081
+ /**
4082
+ * Grant virtual caps to the user.
4083
+ *
4084
+ * @param array $capabilities All capabilities belonging to the specified user, cap => true/false.
4085
+ * @param array $required_caps The required capabilities.
4086
+ * @param array $args The capability passed to current_user_can, the user's ID, and other args.
4087
+ * @return array Filtered list of capabilities.
4088
+ */
4089
+ function grant_virtual_caps_to_user($capabilities, /** @noinspection PhpUnusedParameterInspection */ $required_caps, $args){
4090
+ $this->virtual_caps_for_this_call = array();
4091
+
4092
+ if ( $this->disable_virtual_caps ) {
4093
+ return $capabilities;
4094
+ }
4095
+
4096
+ //The second entry of the $args array should be the user ID
4097
+ if ( count($args) < 2 ) {
4098
+ return $capabilities;
4099
+ }
4100
+ $user_id = intval($args[1]);
4101
+
4102
+ if ( !isset($this->cached_virtual_user_caps[$user_id]) ) {
4103
+ $this->update_virtual_cap_cache($this->get_user_by_id($user_id));
4104
+ }
4105
+
4106
+ if ( empty($this->cached_virtual_user_caps[$user_id][$this->virtual_cap_mode]) ) {
4107
+ return $capabilities;
4108
+ }
4109
+
4110
+ $this->virtual_caps_for_this_call = $this->cached_virtual_user_caps[$user_id][$this->virtual_cap_mode];
4111
+
4112
+ $capabilities = array_merge($capabilities, $this->virtual_caps_for_this_call);
4113
+ return $capabilities;
4114
+ }
4115
+
4116
+ /**
4117
+ * Set the capabilities that were already set by grant_virtual_caps_to_user() again.
4118
+ *
4119
+ * The goal of granting the same capabilities twice at different hook priorities is to:
4120
+ * 1) Make sure meta caps that rely on the granted caps are enabled.
4121
+ * 2) Reduce the risk that the granted caps will be overridden by other plugins.
4122
+ *
4123
+ * @param array $capabilities
4124
+ * @return array
4125
+ */
4126
+ public function regrant_virtual_caps_to_user($capabilities) {
4127
+ if ( !empty($this->virtual_caps_for_this_call) ) {
4128
+ $capabilities = array_merge($capabilities, $this->virtual_caps_for_this_call);
4129
+ $this->virtual_caps_for_this_call = array();
4130
+ }
4131
+ return $capabilities;
4132
  }
4133
 
4134
  /**
4152
  }
4153
 
4154
  /**
4155
+ * User metadata was updated or deleted; refresh or invalidate the associated role/capability caches.
4156
  *
4157
+ * Not all metadata updates are related to role changes, but filtering them is non-trivial (meta keys change).
 
4158
  *
4159
  * @param int|array $unused_meta_id
4160
  * @param int $user_id
4161
+ * @param string $meta_key
4162
+ * @noinspection PhpUnusedParameterInspection
4163
  */
4164
+ public function on_user_metadata_changed($unused_meta_id, $user_id, $meta_key) {
4165
  if ( empty($user_id) || !is_numeric($user_id) ) {
4166
  return;
4167
  }
4168
+ //Clear the user role cache.
4169
  unset($this->cached_user_roles[$user_id]);
4170
+
4171
+ $this->virtual_caps_for_this_call = array();
4172
+
4173
+ //Did this update change user capabilities or roles? If so, refresh virtual caps.
4174
+ $user = $this->get_user_by_id($user_id);
4175
+ if ( $meta_key === $user->cap_key ) {
4176
+ $this->update_virtual_cap_cache($user);
4177
+ }
4178
+ }
4179
+
4180
+ /**
4181
+ * Get the user object based on a user ID.
4182
+ *
4183
+ * In most cases, when this plugin needs to retrieve a user, it is the current user. This method
4184
+ * attempts to make that common case faster.
4185
+ *
4186
+ * @param int $user_id
4187
+ * @return WP_User|null
4188
+ */
4189
+ private function get_user_by_id($user_id) {
4190
+ $current_user = wp_get_current_user();
4191
+ if ( $current_user->ID == $user_id ) {
4192
+ $user = $current_user;
4193
+ } else {
4194
+ $user = get_user_by('id', $user_id);
4195
+ if ( $user === false ) {
4196
+ return null;
4197
+ }
4198
+ }
4199
+ return $user;
4200
  }
4201
 
4202
  /**
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.9.6
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.9.7
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: 4.1
6
- Tested up to: 5.5
7
- Stable tag: 1.9.6
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,10 @@ Plugins installed in the `mu-plugins` directory are treated as "always on", so y
63
 
64
  == Changelog ==
65
 
 
 
 
 
66
  = 1.9.6 =
67
  * Added an option to disable WPML support.
68
  * Fixed a minor WP 5.5 compatibility issue where some of the boxes shown on the menu settings page were displayed incorrectly.
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: 4.1
6
+ Tested up to: 5.6
7
+ Stable tag: 1.9.7
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.9.7 =
67
+ * Fixed a conflict with Elementor 3.0.0-beta that caused the "Theme Builder" menu item to have the wrong URL.
68
+ * Minor performance optimization.
69
+
70
  = 1.9.6 =
71
  * Added an option to disable WPML support.
72
  * Fixed a minor WP 5.5 compatibility issue where some of the boxes shown on the menu settings page were displayed incorrectly.