Version Description
- Trying to delete a non-custom menu item will now trigger a warning dialog that offers to hide the item instead. In general, it's impossible to permanently delete menus created by WordPress itself or other plugins (without editing their source code, that is).
- Added a workaround for a bug in W3 Total Cache 0.9.4.1 that could cause menu permissions to stop working properly when the CDN or New Relic modules were activated.
- Fixed a plugin conflict where certain menu items didn't show up in the editor because the plugin that created them used a very low priority.
- Signigicantly improved sanitization of menu properties.
- Renamed the "Choose Icon" button to "Media Library".
- Minor compatibility improvements.
Download this release
Release Info
Developer | whiteshadow |
Plugin | Admin Menu Editor |
Version | 1.4.3 |
Comparing to | |
See all releases |
Code changes from version 1.4.2 to 1.4.3
- includes/editor-page.php +7 -1
- includes/menu-editor-core.php +88 -2
- includes/menu-item.php +62 -2
- includes/menu.php +162 -4
- js/menu-editor.js +61 -22
- menu-editor.php +1 -1
- readme.txt +10 -2
includes/editor-page.php
CHANGED
@@ -256,7 +256,7 @@ if ( apply_filters('admin_menu_editor_is_pro', false) ) {
|
|
256 |
<input type="button" class="button"
|
257 |
id="ws_choose_icon_from_media"
|
258 |
title="Upload an image or choose one from your media library"
|
259 |
-
value="
|
260 |
<div class="clear"></div>
|
261 |
<?php
|
262 |
endif;
|
@@ -435,6 +435,12 @@ if ( apply_filters('admin_menu_editor_is_pro', false) ) {
|
|
435 |
'ws_hide_menu_except_current_user',
|
436 |
false
|
437 |
);
|
|
|
|
|
|
|
|
|
|
|
|
|
438 |
submit_button('Cancel', 'secondary', 'ws_cancel_menu_deletion', false);
|
439 |
?>
|
440 |
</div>
|
256 |
<input type="button" class="button"
|
257 |
id="ws_choose_icon_from_media"
|
258 |
title="Upload an image or choose one from your media library"
|
259 |
+
value="Media Library">
|
260 |
<div class="clear"></div>
|
261 |
<?php
|
262 |
endif;
|
435 |
'ws_hide_menu_except_current_user',
|
436 |
false
|
437 |
);
|
438 |
+
submit_button(
|
439 |
+
'Hide it from everyone except Administrator',
|
440 |
+
'secondary',
|
441 |
+
'ws_hide_menu_except_administrator',
|
442 |
+
false
|
443 |
+
);
|
444 |
submit_button('Cancel', 'secondary', 'ws_cancel_menu_deletion', false);
|
445 |
?>
|
446 |
</div>
|
includes/menu-editor-core.php
CHANGED
@@ -77,6 +77,11 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
|
|
77 |
private $get = array();
|
78 |
private $originalPost = array();
|
79 |
|
|
|
|
|
|
|
|
|
|
|
80 |
function init(){
|
81 |
$this->sitewide_options = true;
|
82 |
|
@@ -119,7 +124,9 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
|
|
119 |
$this->settings_link = 'options-general.php?page=menu_editor';
|
120 |
|
121 |
$this->magic_hooks = true;
|
122 |
-
|
|
|
|
|
123 |
|
124 |
//AJAXify screen options
|
125 |
add_action('wp_ajax_ws_ame_save_screen_options', array($this,'ajax_save_screen_options'));
|
@@ -147,6 +154,14 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
|
|
147 |
|
148 |
//Tell first-time users where they can find the plugin settings page.
|
149 |
add_action('all_admin_notices', array($this, 'display_plugin_menu_notice'));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
150 |
}
|
151 |
|
152 |
function init_finish() {
|
@@ -301,6 +316,7 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
|
|
301 |
$message .= '<p><strong>Admin Menu Editor security log</strong></p>';
|
302 |
$message .= $this->get_formatted_security_log();
|
303 |
}
|
|
|
304 |
wp_die($message);
|
305 |
} else {
|
306 |
$this->log_security_note('ALLOW access.');
|
@@ -535,7 +551,7 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
|
|
535 |
$users[$current_user->get('user_login')] = array(
|
536 |
'user_login' => $current_user->get('user_login'),
|
537 |
'id' => $current_user->ID,
|
538 |
-
'roles' => array_values($current_user
|
539 |
'capabilities' => $this->castValuesToBool($current_user->caps),
|
540 |
'is_super_admin' => is_multisite() && is_super_admin(),
|
541 |
);
|
@@ -1483,6 +1499,9 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
|
|
1483 |
return;
|
1484 |
}
|
1485 |
|
|
|
|
|
|
|
1486 |
//Save the custom menu
|
1487 |
$this->set_custom_menu($menu);
|
1488 |
|
@@ -2480,6 +2499,73 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
|
|
2480 |
}
|
2481 |
}
|
2482 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2483 |
/**
|
2484 |
* Tell new users how to access the plugin settings page.
|
2485 |
*/
|
77 |
private $get = array();
|
78 |
private $originalPost = array();
|
79 |
|
80 |
+
/**
|
81 |
+
* @var array A cache of user role names indexed by user ID. E.g. [123 => array("administrator", "foo")]
|
82 |
+
*/
|
83 |
+
private $cached_user_roles = array();
|
84 |
+
|
85 |
function init(){
|
86 |
$this->sitewide_options = true;
|
87 |
|
124 |
$this->settings_link = 'options-general.php?page=menu_editor';
|
125 |
|
126 |
$this->magic_hooks = true;
|
127 |
+
//Run our hooks last (almost). Priority is less than PHP_INT_MAX mostly for defensive programming purposes.
|
128 |
+
//Old PHP versions have known bugs related to large array keys, and WP might have undiscovered edge cases.
|
129 |
+
$this->magic_hook_priority = PHP_INT_MAX - 10;
|
130 |
|
131 |
//AJAXify screen options
|
132 |
add_action('wp_ajax_ws_ame_save_screen_options', array($this,'ajax_save_screen_options'));
|
154 |
|
155 |
//Tell first-time users where they can find the plugin settings page.
|
156 |
add_action('all_admin_notices', array($this, 'display_plugin_menu_notice'));
|
157 |
+
|
158 |
+
//Workaround for buggy plugins that unintentionally remove user roles.
|
159 |
+
/** @see WPMenuEditor::get_user_roles */
|
160 |
+
add_action('set_current_user', array($this, 'update_current_user_cache'), 1, 0); //Run before most plugins.
|
161 |
+
add_action('updated_user_meta', array($this, 'clear_user_role_cache'), 10, 2);
|
162 |
+
add_action('deleted_user_meta', array($this, 'clear_user_role_cache'), 10, 2);
|
163 |
+
//There's also a "set_user_role" hook, but it's only called by WP_User::set_role and not WP_User::add_role.
|
164 |
+
//It's also redundant - WP_User::set_role updates user meta, so the above hooks already cover it.
|
165 |
}
|
166 |
|
167 |
function init_finish() {
|
316 |
$message .= '<p><strong>Admin Menu Editor security log</strong></p>';
|
317 |
$message .= $this->get_formatted_security_log();
|
318 |
}
|
319 |
+
do_action('admin_page_access_denied');
|
320 |
wp_die($message);
|
321 |
} else {
|
322 |
$this->log_security_note('ALLOW access.');
|
551 |
$users[$current_user->get('user_login')] = array(
|
552 |
'user_login' => $current_user->get('user_login'),
|
553 |
'id' => $current_user->ID,
|
554 |
+
'roles' => array_values($this->get_user_roles($current_user)),
|
555 |
'capabilities' => $this->castValuesToBool($current_user->caps),
|
556 |
'is_super_admin' => is_multisite() && is_super_admin(),
|
557 |
);
|
1499 |
return;
|
1500 |
}
|
1501 |
|
1502 |
+
//Sanitize menu item properties.
|
1503 |
+
$menu['tree'] = ameMenu::sanitize($menu['tree']);
|
1504 |
+
|
1505 |
//Save the custom menu
|
1506 |
$this->set_custom_menu($menu);
|
1507 |
|
2499 |
}
|
2500 |
}
|
2501 |
|
2502 |
+
/**
|
2503 |
+
* Get the names of the roles that a user belongs to.
|
2504 |
+
*
|
2505 |
+
* "Why not just read the $user->roles array directly?", you may ask. Because some popular plugins have a really
|
2506 |
+
* nasty bug where they inadvertently remove entries from that array. Specifically, they retrieve the first user
|
2507 |
+
* role like this:
|
2508 |
+
*
|
2509 |
+
* $roleName = array_shift($currentUser->roles);
|
2510 |
+
*
|
2511 |
+
* What some plugin developers fail to realize is that, in addition to returning the first entry, array_shift()
|
2512 |
+
* also *removes* it from the array. As a result, $user->roles is now missing one of the user's roles. This bug
|
2513 |
+
* doesn't cause major problems only because most plugins check capabilities and don't care about roles as such.
|
2514 |
+
* AME needs to know to determine menu permissions for different roles.
|
2515 |
+
*
|
2516 |
+
* Known buggy plugins:
|
2517 |
+
* - W3 Total Cache 0.9.4.1
|
2518 |
+
*
|
2519 |
+
* The current workaround is to cache the role list before it can get corrupted by other plugins. This approach
|
2520 |
+
* has its own risks (cache invalidation is hard), but it should be reasonably safe assuming that everyone uses
|
2521 |
+
* only standard WP APIs to modify user roles (e.g. @see WP_User::add_role ).
|
2522 |
+
*
|
2523 |
+
* @param WP_User $user
|
2524 |
+
* @return array
|
2525 |
+
*/
|
2526 |
+
public function get_user_roles($user) {
|
2527 |
+
if ( empty($user) ) {
|
2528 |
+
return array();
|
2529 |
+
}
|
2530 |
+
if ( !$user->exists() ) {
|
2531 |
+
return $user->roles;
|
2532 |
+
}
|
2533 |
+
|
2534 |
+
if ( !isset($this->cached_user_roles[$user->ID]) ) {
|
2535 |
+
//Note: In rare cases, WP_User::$roles can be false. For AME it's more convenient to have an empty list.
|
2536 |
+
$this->cached_user_roles[$user->ID] = !empty($user->roles) ? $user->roles : array();
|
2537 |
+
}
|
2538 |
+
return $this->cached_user_roles[$user->ID];
|
2539 |
+
}
|
2540 |
+
|
2541 |
+
/**
|
2542 |
+
* The current user has changed; cache their roles.
|
2543 |
+
*/
|
2544 |
+
public function update_current_user_cache() {
|
2545 |
+
$user = wp_get_current_user();
|
2546 |
+
if ( empty($user) || !$user->exists() ) {
|
2547 |
+
return;
|
2548 |
+
}
|
2549 |
+
|
2550 |
+
$this->cached_user_roles[$user->ID] = $user->roles;
|
2551 |
+
}
|
2552 |
+
|
2553 |
+
/**
|
2554 |
+
* User metadata was updated or deleted; invalidate the role cache.
|
2555 |
+
*
|
2556 |
+
* Not all metadata updates are related to role changes, but filtering them is non-trivial (meta keys change)
|
2557 |
+
* and not really necessary for our purposes.
|
2558 |
+
*
|
2559 |
+
* @param int|array $unused_meta_id
|
2560 |
+
* @param int $user_id
|
2561 |
+
*/
|
2562 |
+
public function clear_user_role_cache(/** @noinspection PhpUnusedParameterInspection */$unused_meta_id, $user_id) {
|
2563 |
+
if ( empty($user_id) || !is_numeric($user_id) ) {
|
2564 |
+
return;
|
2565 |
+
}
|
2566 |
+
unset($this->cached_user_roles[$user_id]);
|
2567 |
+
}
|
2568 |
+
|
2569 |
/**
|
2570 |
* Tell new users how to access the plugin settings page.
|
2571 |
*/
|
includes/menu-item.php
CHANGED
@@ -124,13 +124,13 @@ abstract class ameMenuItem {
|
|
124 |
$blank_menu = array_merge($blank_menu, array(
|
125 |
'items' => array(), //List of sub-menu items.
|
126 |
'grant_access' => array(), //Per-role and per-user access. Supersedes role_access.
|
127 |
-
'role_access' => array(), //Per-role access settings.
|
128 |
'colors' => null,
|
129 |
|
130 |
'custom' => false, //True if item is made-from-scratch and has no template.
|
131 |
'missing' => false, //True if our template is no longer present in the default admin menu. Note: Stored values will be ignored. Set upon merging.
|
132 |
'unused' => false, //True if this item was generated from an unused default menu. Note: Stored values will be ignored. Set upon merging.
|
133 |
'hidden' => false, //Hide/show the item. Hiding is purely cosmetic, the item remains accessible.
|
|
|
134 |
|
135 |
'defaults' => self::basic_defaults(),
|
136 |
));
|
@@ -319,7 +319,7 @@ abstract class ameMenuItem {
|
|
319 |
foreach($item['role_access'] as $role_id => $has_access) {
|
320 |
$item['grant_access']['role:' . $role_id] = $has_access;
|
321 |
}
|
322 |
-
$item['role_access']
|
323 |
}
|
324 |
|
325 |
if ( isset($item['items']) ) {
|
@@ -365,6 +365,66 @@ abstract class ameMenuItem {
|
|
365 |
return $item;
|
366 |
}
|
367 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
368 |
/**
|
369 |
* Custom comparison function that compares menu items based on their position in the menu.
|
370 |
*
|
124 |
$blank_menu = array_merge($blank_menu, array(
|
125 |
'items' => array(), //List of sub-menu items.
|
126 |
'grant_access' => array(), //Per-role and per-user access. Supersedes role_access.
|
|
|
127 |
'colors' => null,
|
128 |
|
129 |
'custom' => false, //True if item is made-from-scratch and has no template.
|
130 |
'missing' => false, //True if our template is no longer present in the default admin menu. Note: Stored values will be ignored. Set upon merging.
|
131 |
'unused' => false, //True if this item was generated from an unused default menu. Note: Stored values will be ignored. Set upon merging.
|
132 |
'hidden' => false, //Hide/show the item. Hiding is purely cosmetic, the item remains accessible.
|
133 |
+
'separator' => false, //True if the item is a menu separator.
|
134 |
|
135 |
'defaults' => self::basic_defaults(),
|
136 |
));
|
319 |
foreach($item['role_access'] as $role_id => $has_access) {
|
320 |
$item['grant_access']['role:' . $role_id] = $has_access;
|
321 |
}
|
322 |
+
unset($item['role_access']);
|
323 |
}
|
324 |
|
325 |
if ( isset($item['items']) ) {
|
365 |
return $item;
|
366 |
}
|
367 |
|
368 |
+
/**
|
369 |
+
* Sanitize item properties.
|
370 |
+
*
|
371 |
+
* Strips disallowed HTML and invalid characters from many fields. For example, only users who
|
372 |
+
* have the "unfiltered_html" capability can use arbitrary HTML in menu titles.
|
373 |
+
*
|
374 |
+
* To avoid the performance hit of calling current_user_can('unfiltered_html') for every item,
|
375 |
+
* you can call it once and pass the result to this function.
|
376 |
+
*
|
377 |
+
* @param array $item Menu item in the internal format.
|
378 |
+
* @param bool|null $user_can_unfiltered_html
|
379 |
+
* @return array Sanitized menu item.
|
380 |
+
*/
|
381 |
+
public static function sanitize($item, $user_can_unfiltered_html = null) {
|
382 |
+
if ( $user_can_unfiltered_html === null ) {
|
383 |
+
$user_can_unfiltered_html = current_user_can('unfiltered_html');
|
384 |
+
}
|
385 |
+
|
386 |
+
if ( !$user_can_unfiltered_html ) {
|
387 |
+
$kses_fields = array('menu_title', 'page_title', 'file', 'page_heading');
|
388 |
+
foreach($kses_fields as $field) {
|
389 |
+
$value = self::get($item, $field);
|
390 |
+
if ( is_string($value) && !empty($value) && !self::is_default($item, $field) ) {
|
391 |
+
$item[$field] = wp_kses_post($value);
|
392 |
+
}
|
393 |
+
}
|
394 |
+
}
|
395 |
+
|
396 |
+
//Sanitize CSS class names. Note that the WP implementation of sanitize_html_class() is very basic
|
397 |
+
//and doesn't comply with the CSS2 spec, but that's probably OK in this case.
|
398 |
+
$css_class = self::get($item, 'css_class');
|
399 |
+
if ( !self::is_default($item, 'css_class') && is_string($css_class) && function_exists('sanitize_html_class') ) {
|
400 |
+
$item['css_class'] = implode(' ', array_map('sanitize_html_class', explode(' ', $css_class)));
|
401 |
+
}
|
402 |
+
|
403 |
+
//While menu capabilities are generally not displayed anywhere except this plugin (which already
|
404 |
+
//escapes them properly), lets sanitize them anyway in case another plugin displays them as-is.
|
405 |
+
$capability_fields = array('access_level', 'extra_capability');
|
406 |
+
foreach($capability_fields as $field) {
|
407 |
+
$value = self::get($item, $field);
|
408 |
+
if ( !self::is_default($item, $field) && is_string($value) ) {
|
409 |
+
$item[$field] = strip_tags($value);
|
410 |
+
}
|
411 |
+
}
|
412 |
+
|
413 |
+
//Menu icons can be all kinds of stuff (dashicons, data URIs, etc), but they can't contain HTML.
|
414 |
+
//See /wp-admin/menu-header.php line #90 and onwards for how WordPress handles icons.
|
415 |
+
if ( !self::is_default($item, 'icon_url') ) {
|
416 |
+
$item['icon_url'] = strip_tags($item['icon_url']);
|
417 |
+
}
|
418 |
+
|
419 |
+
//WordPress already sanitizes the menu ID (hookname) on display, but, again, lets clean it just in case.
|
420 |
+
if ( !self::is_default($item, 'hookname') ) {
|
421 |
+
//Regex from menu-header.php, WP 4.1.
|
422 |
+
$item['hookname'] = preg_replace('@[^a-zA-Z0-9_:.]@', '-', self::get($item, 'hookname'));
|
423 |
+
}
|
424 |
+
|
425 |
+
return $item;
|
426 |
+
}
|
427 |
+
|
428 |
/**
|
429 |
* Custom comparison function that compares menu items based on their position in the menu.
|
430 |
*
|
includes/menu.php
CHANGED
@@ -1,7 +1,7 @@
|
|
1 |
<?php
|
2 |
abstract class ameMenu {
|
3 |
const format_name = 'Admin Menu Editor menu';
|
4 |
-
const format_version = '
|
5 |
|
6 |
/**
|
7 |
* Load an admin menu from a JSON string.
|
@@ -58,6 +58,10 @@ abstract class ameMenu {
|
|
58 |
throw new InvalidMenuException("Failed to load a menu - the menu tree is missing.");
|
59 |
}
|
60 |
|
|
|
|
|
|
|
|
|
61 |
$menu = array('tree' => array());
|
62 |
$menu = self::add_format_header($menu);
|
63 |
|
@@ -100,9 +104,15 @@ abstract class ameMenu {
|
|
100 |
}
|
101 |
|
102 |
private static function add_format_header($menu) {
|
103 |
-
$menu['format']
|
104 |
-
'
|
105 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
106 |
);
|
107 |
return $menu;
|
108 |
}
|
@@ -199,6 +209,31 @@ abstract class ameMenu {
|
|
199 |
return false;
|
200 |
}
|
201 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
202 |
/**
|
203 |
* Recursively filter a list of menu items and remove items flagged as missing.
|
204 |
*
|
@@ -220,6 +255,129 @@ abstract class ameMenu {
|
|
220 |
protected static function is_not_missing($item) {
|
221 |
return empty($item['missing']);
|
222 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
223 |
}
|
224 |
|
225 |
|
1 |
<?php
|
2 |
abstract class ameMenu {
|
3 |
const format_name = 'Admin Menu Editor menu';
|
4 |
+
const format_version = '6.0';
|
5 |
|
6 |
/**
|
7 |
* Load an admin menu from a JSON string.
|
58 |
throw new InvalidMenuException("Failed to load a menu - the menu tree is missing.");
|
59 |
}
|
60 |
|
61 |
+
if ( isset($arr['format']) && !empty($arr['format']['compressed']) ) {
|
62 |
+
$arr = self::decompress($arr);
|
63 |
+
}
|
64 |
+
|
65 |
$menu = array('tree' => array());
|
66 |
$menu = self::add_format_header($menu);
|
67 |
|
104 |
}
|
105 |
|
106 |
private static function add_format_header($menu) {
|
107 |
+
if ( !isset($menu['format']) || !is_array($menu['format']) ) {
|
108 |
+
$menu['format'] = array();
|
109 |
+
}
|
110 |
+
$menu['format'] = array_merge(
|
111 |
+
$menu['format'],
|
112 |
+
array(
|
113 |
+
'name' => self::format_name,
|
114 |
+
'version' => self::format_version,
|
115 |
+
)
|
116 |
);
|
117 |
return $menu;
|
118 |
}
|
209 |
return false;
|
210 |
}
|
211 |
|
212 |
+
/**
|
213 |
+
* Sanitize a list of menu items. Array indexes will be preserved.
|
214 |
+
*
|
215 |
+
* @param array $treeItems A list of menu items.
|
216 |
+
* @param bool $unfiltered_html Whether the current user has the unfiltered_html capability.
|
217 |
+
* @return array List of sanitized items.
|
218 |
+
*/
|
219 |
+
public static function sanitize($treeItems, $unfiltered_html = null) {
|
220 |
+
if ( $unfiltered_html === null ) {
|
221 |
+
$unfiltered_html = current_user_can('unfiltered_html');
|
222 |
+
}
|
223 |
+
|
224 |
+
$result = array();
|
225 |
+
foreach($treeItems as $key => $item) {
|
226 |
+
$item = ameMenuItem::sanitize($item, $unfiltered_html);
|
227 |
+
|
228 |
+
if ( !empty($item['items']) ) {
|
229 |
+
$item['items'] = self::sanitize($item['items'], $unfiltered_html);
|
230 |
+
}
|
231 |
+
$result[$key] = $item;
|
232 |
+
}
|
233 |
+
|
234 |
+
return $result;
|
235 |
+
}
|
236 |
+
|
237 |
/**
|
238 |
* Recursively filter a list of menu items and remove items flagged as missing.
|
239 |
*
|
255 |
protected static function is_not_missing($item) {
|
256 |
return empty($item['missing']);
|
257 |
}
|
258 |
+
|
259 |
+
/**
|
260 |
+
* Compress menu configuration (lossless).
|
261 |
+
*
|
262 |
+
* Reduces data size by storing commonly used properties and defaults in one place
|
263 |
+
* instead of in every menu item.
|
264 |
+
*
|
265 |
+
* @param array $menu
|
266 |
+
* @return array
|
267 |
+
*/
|
268 |
+
public static function compress($menu) {
|
269 |
+
$property_dict = ameMenuItem::blank_menu();
|
270 |
+
unset($property_dict['defaults']);
|
271 |
+
|
272 |
+
$common = array(
|
273 |
+
'properties' => $property_dict,
|
274 |
+
'basic_defaults' => ameMenuItem::basic_defaults(),
|
275 |
+
'custom_item_defaults' => ameMenuItem::custom_item_defaults(),
|
276 |
+
);
|
277 |
+
|
278 |
+
$menu['tree'] = self::map_items(
|
279 |
+
$menu['tree'],
|
280 |
+
array(__CLASS__, 'compress_item'),
|
281 |
+
array($common)
|
282 |
+
);
|
283 |
+
|
284 |
+
$menu = self::add_format_header($menu);
|
285 |
+
$menu['format']['compressed'] = true;
|
286 |
+
$menu['format']['common'] = $common;
|
287 |
+
|
288 |
+
return $menu;
|
289 |
+
}
|
290 |
+
|
291 |
+
protected static function compress_item($item, $common) {
|
292 |
+
//These empty arrays can be dropped. They'll be restored either by merging common properties,
|
293 |
+
//or by ameMenuItem::normalize().
|
294 |
+
if ( empty($item['grant_access']) ) {
|
295 |
+
unset($item['grant_access']);
|
296 |
+
}
|
297 |
+
if ( empty($item['items']) ) {
|
298 |
+
unset($item['items']);
|
299 |
+
}
|
300 |
+
|
301 |
+
//Normal and custom menu items have different defaults.
|
302 |
+
//Remove defaults that are the same for all items of that type.
|
303 |
+
$defaults = !empty($item['custom']) ? $common['custom_item_defaults'] : $common['basic_defaults'];
|
304 |
+
if ( isset($item['defaults']) ) {
|
305 |
+
foreach($defaults as $key => $value) {
|
306 |
+
if ( array_key_exists($key, $item['defaults']) && $item['defaults'][$key] === $value ) {
|
307 |
+
unset($item['defaults'][$key]);
|
308 |
+
}
|
309 |
+
}
|
310 |
+
}
|
311 |
+
|
312 |
+
//Remove properties that match the common values.
|
313 |
+
foreach($common['properties'] as $key => $value) {
|
314 |
+
if ( array_key_exists($key, $item) && $item[$key] === $value ) {
|
315 |
+
unset($item[$key]);
|
316 |
+
}
|
317 |
+
}
|
318 |
+
|
319 |
+
return $item;
|
320 |
+
}
|
321 |
+
|
322 |
+
/**
|
323 |
+
* Decompress menu configuration that was previously compressed by ameMenu::compress().
|
324 |
+
*
|
325 |
+
* If the input $menu is not compressed, this method will return it unchanged.
|
326 |
+
*
|
327 |
+
* @param array $menu
|
328 |
+
* @return array
|
329 |
+
*/
|
330 |
+
public static function decompress($menu) {
|
331 |
+
if ( !isset($menu['format']) || empty($menu['format']['compressed']) ) {
|
332 |
+
return $menu;
|
333 |
+
}
|
334 |
+
|
335 |
+
$common = $menu['format']['common'];
|
336 |
+
$menu['tree'] = self::map_items(
|
337 |
+
$menu['tree'],
|
338 |
+
array(__CLASS__, 'decompress_item'),
|
339 |
+
array($common)
|
340 |
+
);
|
341 |
+
|
342 |
+
unset($menu['format']['compressed'], $menu['format']['common']);
|
343 |
+
return $menu;
|
344 |
+
}
|
345 |
+
|
346 |
+
protected static function decompress_item($item, $common) {
|
347 |
+
$item = array_merge($common['properties'], $item);
|
348 |
+
|
349 |
+
$defaults = !empty($item['custom']) ? $common['custom_item_defaults'] : $common['basic_defaults'];
|
350 |
+
$item['defaults'] = array_merge($defaults, $item['defaults']);
|
351 |
+
|
352 |
+
return $item;
|
353 |
+
}
|
354 |
+
|
355 |
+
/**
|
356 |
+
* Recursively apply a callback to every menu item in an array and return the results.
|
357 |
+
* Array keys are preserved.
|
358 |
+
*
|
359 |
+
* @param array $items
|
360 |
+
* @param callable $callback
|
361 |
+
* @param array|null $extra_params Optional. An array of additional parameters to pass to the callback.
|
362 |
+
* @return array
|
363 |
+
*/
|
364 |
+
protected static function map_items($items, $callback, $extra_params = null) {
|
365 |
+
if ( $extra_params === null ) {
|
366 |
+
$extra_params = array();
|
367 |
+
}
|
368 |
+
|
369 |
+
$result = array();
|
370 |
+
foreach($items as $key => $item) {
|
371 |
+
$args = array_merge(array($item), $extra_params);
|
372 |
+
$item = call_user_func_array($callback, $args);
|
373 |
+
|
374 |
+
if ( !empty($item['items']) ) {
|
375 |
+
$item['items'] = self::map_items($item['items'], $callback, $extra_params);
|
376 |
+
}
|
377 |
+
$result[$key] = $item;
|
378 |
+
}
|
379 |
+
return $result;
|
380 |
+
}
|
381 |
}
|
382 |
|
383 |
|
js/menu-editor.js
CHANGED
@@ -3,6 +3,7 @@
|
|
3 |
/*global wsEditorData, defaultMenu, customMenu */
|
4 |
/** @namespace wsEditorData */
|
5 |
|
|
|
6 |
var wsIdCounter = 0;
|
7 |
|
8 |
var AmeCapabilityManager = (function(roles, users) {
|
@@ -154,6 +155,9 @@ var AmeCapabilityManager = (function(roles, users) {
|
|
154 |
return me;
|
155 |
})(wsEditorData.roles, wsEditorData.users);
|
156 |
|
|
|
|
|
|
|
157 |
(function ($){
|
158 |
|
159 |
var selectedActor = null;
|
@@ -363,7 +367,7 @@ function buildMenuItem(itemData, isTopLevel) {
|
|
363 |
itemData.separator ? '' : '<a class="ws_edit_link"> </a><div class="ws_flag_container"> </div>',
|
364 |
'<input type="checkbox" class="ws_actor_access_checkbox">',
|
365 |
'<span class="ws_item_title">',
|
366 |
-
menuTitle,
|
367 |
' </span>',
|
368 |
|
369 |
'</div>',
|
@@ -414,6 +418,13 @@ function jsTrim(str){
|
|
414 |
return str.replace(/^\s+|\s+$/g, "");
|
415 |
}
|
416 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
417 |
//Editor field spec template.
|
418 |
var baseField = {
|
419 |
caption : '[No caption]',
|
@@ -437,16 +448,12 @@ var knownMenuFields = {
|
|
437 |
caption : 'Menu title',
|
438 |
display: function(menuItem, displayValue, input, containerNode) {
|
439 |
//Update the header as well.
|
440 |
-
|
441 |
-
if (itemTitle === '') {
|
442 |
-
itemTitle = ' ';
|
443 |
-
}
|
444 |
-
containerNode.find('.ws_item_title').html(itemTitle);
|
445 |
return displayValue;
|
446 |
},
|
447 |
write: function(menuItem, value, input, containerNode) {
|
448 |
menuItem.menu_title = value;
|
449 |
-
containerNode.find('.ws_item_title').html(input.val() + ' ');
|
450 |
}
|
451 |
}),
|
452 |
|
@@ -798,11 +805,8 @@ function buildEditboxFields(fieldContainer, entry, isTopLevel){
|
|
798 |
/*
|
799 |
* Create an editor for a specified field.
|
800 |
*/
|
|
|
801 |
function buildEditboxField(entry, field_name, field_settings){
|
802 |
-
if (typeof entry[field_name] === 'undefined') {
|
803 |
-
return null; //skip fields this entry doesn't have
|
804 |
-
}
|
805 |
-
|
806 |
//Build a form field of the appropriate type
|
807 |
var inputBox = null;
|
808 |
var basicTextField = '<input type="text" class="ws_field_value">';
|
@@ -1093,6 +1097,8 @@ function readMenuTreeState(){
|
|
1093 |
};
|
1094 |
}
|
1095 |
|
|
|
|
|
1096 |
/**
|
1097 |
* Extract the current menu item settings from its editor widget.
|
1098 |
*
|
@@ -1233,6 +1239,8 @@ function actorCanAccessMenu(menuItem, actor) {
|
|
1233 |
return actorHasAccess;
|
1234 |
}
|
1235 |
|
|
|
|
|
1236 |
function actorHasCustomPermissions(menuItem, actor) {
|
1237 |
if (menuItem.grant_access && menuItem.grant_access.hasOwnProperty && menuItem.grant_access.hasOwnProperty(actor)) {
|
1238 |
return (menuItem.grant_access[actor] !== null);
|
@@ -1338,6 +1346,9 @@ $(document).ready(function(){
|
|
1338 |
$('.ws_hide_if_pro').hide();
|
1339 |
}
|
1340 |
|
|
|
|
|
|
|
1341 |
//Make the top menu box sortable (we only need to do this once)
|
1342 |
var mainMenuBox = $('#ws_menu_box');
|
1343 |
makeBoxSortable(mainMenuBox);
|
@@ -1949,7 +1960,7 @@ $(document).ready(function(){
|
|
1949 |
//Create a custom media frame.
|
1950 |
frame = wp.media.frames.customAdminMenuIcon = wp.media({
|
1951 |
//Set the title of the modal.
|
1952 |
-
title: 'Choose a Custom Icon (
|
1953 |
|
1954 |
//Tell it to show only images.
|
1955 |
library: {
|
@@ -2185,26 +2196,46 @@ $(document).ready(function(){
|
|
2185 |
menuDeletionDialog.dialog('close');
|
2186 |
var selection = menuDeletionDialog.data('selected_menu');
|
2187 |
|
2188 |
-
function
|
2189 |
-
|
2190 |
|
2191 |
var subMenuId = containerNode.data('submenu_id');
|
2192 |
if (subMenuId && containerNode.hasClass('ws_menu')) {
|
2193 |
$('.ws_item', '#' + subMenuId).each(function() {
|
2194 |
var node = $(this);
|
2195 |
-
|
2196 |
updateItemEditor(node);
|
2197 |
});
|
2198 |
}
|
2199 |
|
2200 |
updateItemEditor(containerNode);
|
|
|
|
|
|
|
|
|
|
|
|
|
2201 |
updateParentAccessUi(containerNode);
|
2202 |
}
|
2203 |
|
2204 |
if (hide === 'all') {
|
2205 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2206 |
} else if (hide === 'except_current_user') {
|
2207 |
hideRecursively(selection, 'user:' + wsEditorData.currentUserLogin);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2208 |
}
|
2209 |
};
|
2210 |
|
@@ -2218,6 +2249,9 @@ $(document).ready(function(){
|
|
2218 |
$('#ws_hide_menu_except_current_user').click(function() {
|
2219 |
menuDeletionCallback('except_current_user');
|
2220 |
});
|
|
|
|
|
|
|
2221 |
|
2222 |
/**
|
2223 |
* Attempt to delete a menu item. Will check if the item can actually be deleted and ask the user for confirmation.
|
@@ -2247,9 +2281,8 @@ $(document).ready(function(){
|
|
2247 |
});
|
2248 |
}
|
2249 |
|
2250 |
-
if (!isDefaultItem || otherCopiesExist
|
2251 |
-
//Custom and duplicate items can be deleted normally.
|
2252 |
-
//because it doesn't have role-specific permissions.
|
2253 |
shouldDelete = confirm('Delete this menu?');
|
2254 |
} else {
|
2255 |
//Non-custom items can not be deleted, but they can be hidden. Ask the user if they want to do that.
|
@@ -2257,6 +2290,12 @@ $(document).ready(function(){
|
|
2257 |
menuItem.defaults.is_plugin_page ? 'an item added by another plugin' : 'a built-in menu item'
|
2258 |
);
|
2259 |
menuDeletionDialog.data('selected_menu', selection);
|
|
|
|
|
|
|
|
|
|
|
|
|
2260 |
menuDeletionDialog.dialog('open');
|
2261 |
|
2262 |
//Select "Cancel" as the default button.
|
@@ -2365,7 +2404,7 @@ $(document).ready(function(){
|
|
2365 |
menu_title : 'Custom Menu ' + ws_paste_count,
|
2366 |
file : randomId,
|
2367 |
items: [],
|
2368 |
-
defaults: itemTemplates.getDefaults('')
|
2369 |
});
|
2370 |
|
2371 |
//Make it accessible only to the current actor if one is selected.
|
@@ -2616,7 +2655,7 @@ $(document).ready(function(){
|
|
2616 |
menu_title : 'Custom Item ' + ws_paste_count,
|
2617 |
file : randomMenuId(),
|
2618 |
items: [],
|
2619 |
-
defaults: itemTemplates.getDefaults('')
|
2620 |
});
|
2621 |
|
2622 |
//Make it accessible to only the currently selected actor.
|
@@ -2803,7 +2842,7 @@ $(document).ready(function(){
|
|
2803 |
|
2804 |
if ( (typeof data['download_url'] != 'undefined') && data.download_url ){
|
2805 |
//window.location = data.download_url;
|
2806 |
-
$('#download_menu_button').attr('href', data.download_url);
|
2807 |
$('#export_progress_notice').hide();
|
2808 |
$('#export_complete_notice, #download_menu_button').show();
|
2809 |
}
|
3 |
/*global wsEditorData, defaultMenu, customMenu */
|
4 |
/** @namespace wsEditorData */
|
5 |
|
6 |
+
wsEditorData.wsMenuEditorPro = !!wsEditorData.wsMenuEditorPro; //Cast to boolean.
|
7 |
var wsIdCounter = 0;
|
8 |
|
9 |
var AmeCapabilityManager = (function(roles, users) {
|
155 |
return me;
|
156 |
})(wsEditorData.roles, wsEditorData.users);
|
157 |
|
158 |
+
|
159 |
+
var AmeEditorApi = {};
|
160 |
+
|
161 |
(function ($){
|
162 |
|
163 |
var selectedActor = null;
|
367 |
itemData.separator ? '' : '<a class="ws_edit_link"> </a><div class="ws_flag_container"> </div>',
|
368 |
'<input type="checkbox" class="ws_actor_access_checkbox">',
|
369 |
'<span class="ws_item_title">',
|
370 |
+
stripAllTags(menuTitle),
|
371 |
' </span>',
|
372 |
|
373 |
'</div>',
|
418 |
return str.replace(/^\s+|\s+$/g, "");
|
419 |
}
|
420 |
|
421 |
+
function stripAllTags(input) {
|
422 |
+
//Based on: http://phpjs.org/functions/strip_tags/
|
423 |
+
var tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi,
|
424 |
+
commentsAndPhpTags = /<!--[\s\S]*?-->|<\?(?:php)?[\s\S]*?\?>/gi;
|
425 |
+
return input.replace(commentsAndPhpTags, '').replace(tags, '');
|
426 |
+
}
|
427 |
+
|
428 |
//Editor field spec template.
|
429 |
var baseField = {
|
430 |
caption : '[No caption]',
|
448 |
caption : 'Menu title',
|
449 |
display: function(menuItem, displayValue, input, containerNode) {
|
450 |
//Update the header as well.
|
451 |
+
containerNode.find('.ws_item_title').html(stripAllTags(displayValue) + ' ');
|
|
|
|
|
|
|
|
|
452 |
return displayValue;
|
453 |
},
|
454 |
write: function(menuItem, value, input, containerNode) {
|
455 |
menuItem.menu_title = value;
|
456 |
+
containerNode.find('.ws_item_title').html(stripAllTags(input.val()) + ' ');
|
457 |
}
|
458 |
}),
|
459 |
|
805 |
/*
|
806 |
* Create an editor for a specified field.
|
807 |
*/
|
808 |
+
//noinspection JSUnusedLocalSymbols
|
809 |
function buildEditboxField(entry, field_name, field_settings){
|
|
|
|
|
|
|
|
|
810 |
//Build a form field of the appropriate type
|
811 |
var inputBox = null;
|
812 |
var basicTextField = '<input type="text" class="ws_field_value">';
|
1097 |
};
|
1098 |
}
|
1099 |
|
1100 |
+
AmeEditorApi.readMenuTreeState = readMenuTreeState;
|
1101 |
+
|
1102 |
/**
|
1103 |
* Extract the current menu item settings from its editor widget.
|
1104 |
*
|
1239 |
return actorHasAccess;
|
1240 |
}
|
1241 |
|
1242 |
+
AmeEditorApi.actorCanAccessMenu = actorCanAccessMenu;
|
1243 |
+
|
1244 |
function actorHasCustomPermissions(menuItem, actor) {
|
1245 |
if (menuItem.grant_access && menuItem.grant_access.hasOwnProperty && menuItem.grant_access.hasOwnProperty(actor)) {
|
1246 |
return (menuItem.grant_access[actor] !== null);
|
1346 |
$('.ws_hide_if_pro').hide();
|
1347 |
}
|
1348 |
|
1349 |
+
//Let other plugins filter knownMenuFields.
|
1350 |
+
$(document).trigger('filterMenuFields.adminMenuEditor', [knownMenuFields, baseField]);
|
1351 |
+
|
1352 |
//Make the top menu box sortable (we only need to do this once)
|
1353 |
var mainMenuBox = $('#ws_menu_box');
|
1354 |
makeBoxSortable(mainMenuBox);
|
1960 |
//Create a custom media frame.
|
1961 |
frame = wp.media.frames.customAdminMenuIcon = wp.media({
|
1962 |
//Set the title of the modal.
|
1963 |
+
title: 'Choose a Custom Icon (20x20)',
|
1964 |
|
1965 |
//Tell it to show only images.
|
1966 |
library: {
|
2196 |
menuDeletionDialog.dialog('close');
|
2197 |
var selection = menuDeletionDialog.data('selected_menu');
|
2198 |
|
2199 |
+
function applyCallbackRecursively(containerNode, callback) {
|
2200 |
+
callback(containerNode.data('menu_item'));
|
2201 |
|
2202 |
var subMenuId = containerNode.data('submenu_id');
|
2203 |
if (subMenuId && containerNode.hasClass('ws_menu')) {
|
2204 |
$('.ws_item', '#' + subMenuId).each(function() {
|
2205 |
var node = $(this);
|
2206 |
+
callback(node.data('menu_item'));
|
2207 |
updateItemEditor(node);
|
2208 |
});
|
2209 |
}
|
2210 |
|
2211 |
updateItemEditor(containerNode);
|
2212 |
+
}
|
2213 |
+
|
2214 |
+
function hideRecursively(containerNode, exceptActor) {
|
2215 |
+
applyCallbackRecursively(containerNode, function(menuItem) {
|
2216 |
+
denyAccessForAllExcept(menuItem, exceptActor);
|
2217 |
+
});
|
2218 |
updateParentAccessUi(containerNode);
|
2219 |
}
|
2220 |
|
2221 |
if (hide === 'all') {
|
2222 |
+
if (wsEditorData.wsMenuEditorPro) {
|
2223 |
+
hideRecursively(selection, null);
|
2224 |
+
} else {
|
2225 |
+
//The free version doesn't have role permissions, so use the global "hidden" flag.
|
2226 |
+
applyCallbackRecursively(selection, function(menuItem) {
|
2227 |
+
menuItem.hidden = true;
|
2228 |
+
});
|
2229 |
+
}
|
2230 |
} else if (hide === 'except_current_user') {
|
2231 |
hideRecursively(selection, 'user:' + wsEditorData.currentUserLogin);
|
2232 |
+
} else if (hide === 'except_administrator' && !wsEditorData.wsMenuEditorPro) {
|
2233 |
+
//Set "required capability" to something only the Administrator role would have.
|
2234 |
+
var adminOnlyCap = 'manage_options';
|
2235 |
+
applyCallbackRecursively(selection, function(menuItem) {
|
2236 |
+
menuItem.extra_capability = adminOnlyCap;
|
2237 |
+
});
|
2238 |
+
alert('The "required capability" field was set to "' + adminOnlyCap + '".')
|
2239 |
}
|
2240 |
};
|
2241 |
|
2249 |
$('#ws_hide_menu_except_current_user').click(function() {
|
2250 |
menuDeletionCallback('except_current_user');
|
2251 |
});
|
2252 |
+
$('#ws_hide_menu_except_administrator').click(function() {
|
2253 |
+
menuDeletionCallback('except_administrator');
|
2254 |
+
});
|
2255 |
|
2256 |
/**
|
2257 |
* Attempt to delete a menu item. Will check if the item can actually be deleted and ask the user for confirmation.
|
2281 |
});
|
2282 |
}
|
2283 |
|
2284 |
+
if (!isDefaultItem || otherCopiesExist) {
|
2285 |
+
//Custom and duplicate items can be deleted normally.
|
|
|
2286 |
shouldDelete = confirm('Delete this menu?');
|
2287 |
} else {
|
2288 |
//Non-custom items can not be deleted, but they can be hidden. Ask the user if they want to do that.
|
2290 |
menuItem.defaults.is_plugin_page ? 'an item added by another plugin' : 'a built-in menu item'
|
2291 |
);
|
2292 |
menuDeletionDialog.data('selected_menu', selection);
|
2293 |
+
|
2294 |
+
//Different versions get slightly different options because only the Pro version has
|
2295 |
+
//role-specific permissions.
|
2296 |
+
$('#ws_hide_menu_except_current_user').toggleClass('hidden', !wsEditorData.wsMenuEditorPro);
|
2297 |
+
$('#ws_hide_menu_except_administrator').toggleClass('hidden', wsEditorData.wsMenuEditorPro);
|
2298 |
+
|
2299 |
menuDeletionDialog.dialog('open');
|
2300 |
|
2301 |
//Select "Cancel" as the default button.
|
2404 |
menu_title : 'Custom Menu ' + ws_paste_count,
|
2405 |
file : randomId,
|
2406 |
items: [],
|
2407 |
+
defaults: $.extend({}, itemTemplates.getDefaults(''))
|
2408 |
});
|
2409 |
|
2410 |
//Make it accessible only to the current actor if one is selected.
|
2655 |
menu_title : 'Custom Item ' + ws_paste_count,
|
2656 |
file : randomMenuId(),
|
2657 |
items: [],
|
2658 |
+
defaults: $.extend({}, itemTemplates.getDefaults(''))
|
2659 |
});
|
2660 |
|
2661 |
//Make it accessible to only the currently selected actor.
|
2842 |
|
2843 |
if ( (typeof data['download_url'] != 'undefined') && data.download_url ){
|
2844 |
//window.location = data.download_url;
|
2845 |
+
$('#download_menu_button').attr('href', data.download_url).data('filesize', data.filesize);
|
2846 |
$('#export_progress_notice').hide();
|
2847 |
$('#export_complete_notice, #download_menu_button').show();
|
2848 |
}
|
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.3
|
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.1
|
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.2 =
|
67 |
* Tested on WP 4.1 and 4.2-alpha.
|
68 |
* Fixed a bug that allowed Administrators to bypass custom permissions for the "Appearance -> Customize" menu item.
|
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.1.1
|
7 |
+
Stable tag: 1.4.3
|
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.3 =
|
67 |
+
* Trying to delete a non-custom menu item will now trigger a warning dialog that offers to hide the item instead. In general, it's impossible to permanently delete menus created by WordPress itself or other plugins (without editing their source code, that is).
|
68 |
+
* Added a workaround for a bug in W3 Total Cache 0.9.4.1 that could cause menu permissions to stop working properly when the CDN or New Relic modules were activated.
|
69 |
+
* Fixed a plugin conflict where certain menu items didn't show up in the editor because the plugin that created them used a very low priority.
|
70 |
+
* Signigicantly improved sanitization of menu properties.
|
71 |
+
* Renamed the "Choose Icon" button to "Media Library".
|
72 |
+
* Minor compatibility improvements.
|
73 |
+
|
74 |
= 1.4.2 =
|
75 |
* Tested on WP 4.1 and 4.2-alpha.
|
76 |
* Fixed a bug that allowed Administrators to bypass custom permissions for the "Appearance -> Customize" menu item.
|