Admin Menu Editor - Version 1.4.2

Version Description

  • Tested on WP 4.1 and 4.2-alpha.
  • Fixed a bug that allowed Administrators to bypass custom permissions for the "Appearance -> Customize" menu item.
  • Fixed a regression in the menu highlighting algorithm.
  • Fixed an "array to string conversion" notice caused by passing array data in the query string.
  • Fixed menu scrolling occasionally not working when the user moved an item from one menu to another, much larger menu (e.g. having 20+ submenu items).
  • Fixed a bug where moving a submenu item from a plugin menu that doesn't have a hook callback (i.e. an unusable menu serving as a placeholder) to a different menu would corrupt the menu item URL.
  • Other minor bug fixes.
Download this release

Release Info

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

Code changes from version 1.4.1 to 1.4.2

css/menu-editor.css CHANGED
@@ -870,6 +870,10 @@ select.ws_dropdown optgroup option {
870
  padding-top: 25px;
871
  }
872
 
 
 
 
 
873
  .ws_dont_show_again {
874
  display: inline-block;
875
  margin-top: 1em;
@@ -1054,6 +1058,16 @@ select.ws_dropdown optgroup option {
1054
  margin-left: 0.5em;
1055
  }
1056
 
 
 
 
 
 
 
 
 
 
 
1057
  #ws_sidebar_pro_ad {
1058
  min-width: 225px;
1059
 
870
  padding-top: 25px;
871
  }
872
 
873
+ #ws_import_error_response {
874
+ width: 100%;
875
+ }
876
+
877
  .ws_dont_show_again {
878
  display: inline-block;
879
  margin-top: 1em;
1058
  margin-left: 0.5em;
1059
  }
1060
 
1061
+
1062
+ /************************************
1063
+ Copy Permissions dialog
1064
+ *************************************/
1065
+ #ws-ame-copy-permissions-dialog select {
1066
+ min-width: 280px;
1067
+ }
1068
+
1069
+
1070
+
1071
  #ws_sidebar_pro_ad {
1072
  min-width: 225px;
1073
 
images/copy-permissions.png ADDED
Binary file
includes/editor-page.php CHANGED
@@ -14,6 +14,7 @@ $icons = array(
14
  'delete' => '/page-delete.png',
15
  'new-separator' => '/separator-add.png',
16
  'toggle-all' => '/check-all.png',
 
17
  );
18
  foreach($icons as $name => $url) {
19
  $icons[$name] = $images_url . $url;
@@ -43,50 +44,18 @@ if ( !apply_filters('admin_menu_editor_is_pro', false) ){
43
  </h2>
44
 
45
  <?php
46
- if ( !empty($_GET['message']) ){
47
- if ( intval($_GET['message']) == 1 ){
48
- echo '<div id="message" class="updated fade"><p><strong>Settings saved.</strong></p></div>';
49
- } elseif ( intval($_GET['message']) == 2 ) {
50
- echo '<div id="message" class="error"><p><strong>Failed to decode input! The menu wasn\'t modified.</strong></p></div>';
51
- }
52
  }
53
- ?>
54
-
55
- <?php
56
- $hint_id = 'ws_whats_new_120';
57
- $show_whats_new = false && apply_filters('admin_menu_editor_is_pro', false) && !empty($editor_data['show_hints'][$hint_id]);
58
- if ( $show_whats_new ):
59
- ?>
60
- <div class="ws_hint" id="<?php echo esc_attr($hint_id); ?>">
61
- <div class="ws_hint_close" title="Close">x</div>
62
- <div class="ws_hint_content">
63
- <strong>What's New In 1.20 and 1.30</strong>
64
- <ul>
65
- <li>New menu permissions interface.
66
- <a href="http://w-shadow.com/admin-menu-editor-pro/permissions/">Learn more.</a></li>
67
-
68
- <li>You can now use "not:user:username", "capability1,capability2", "capability1+capability2" and other
69
- advanced syntax in the capability field. See the link above for details.</li>
70
-
71
- <li>You can drag sub-menu items to the top level and the other way around. To do it,
72
- drag the item to the very end of the (sub-)menu and drop it on the yellow rectangle that will appear.</li>
73
-
74
- <li>Added a "Target page" drop-down to simplify setting menu URLs. You can still enter an arbitrary URL
75
- by selecting "Custom".</li>
76
-
77
- <li>Miscellaneous bug fixes.</li>
78
-
79
- </ul>
80
- </div>
81
- </div>
82
- <?php
83
- endif;
84
- ?>
85
 
86
- <?php
87
  include dirname(__FILE__) . '/access-editor-dialog.php';
88
  if ( apply_filters('admin_menu_editor_is_pro', false) ) {
89
  include dirname(__FILE__) . '/../extras/menu-color-dialog.php';
 
90
  }
91
  ?>
92
 
@@ -126,6 +95,9 @@ if ( apply_filters('admin_menu_editor_is_pro', false) ) {
126
 
127
  <a id='ws_toggle_all_menus' class='ws_button' href='javascript:void(0)'
128
  title='Toggle all menus for the selected role'><img src='<?php echo $icons['toggle-all']; ?>' alt="Toggle all" /></a>
 
 
 
129
  <?php endif; ?>
130
 
131
  <div class="clear"></div>
14
  'delete' => '/page-delete.png',
15
  'new-separator' => '/separator-add.png',
16
  'toggle-all' => '/check-all.png',
17
+ 'copy-permissions' => '/copy-permissions.png',
18
  );
19
  foreach($icons as $name => $url) {
20
  $icons[$name] = $images_url . $url;
44
  </h2>
45
 
46
  <?php
47
+ if ( !empty($_GET['message']) ){
48
+ if ( intval($_GET['message']) == 1 ){
49
+ echo '<div id="message" class="updated fade"><p><strong>Settings saved.</strong></p></div>';
50
+ } elseif ( intval($_GET['message']) == 2 ) {
51
+ echo '<div id="message" class="error"><p><strong>Failed to decode input! The menu wasn\'t modified.</strong></p></div>';
 
52
  }
53
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
 
55
  include dirname(__FILE__) . '/access-editor-dialog.php';
56
  if ( apply_filters('admin_menu_editor_is_pro', false) ) {
57
  include dirname(__FILE__) . '/../extras/menu-color-dialog.php';
58
+ include dirname(__FILE__) . '/../extras/copy-permissions-dialog.php';
59
  }
60
  ?>
61
 
95
 
96
  <a id='ws_toggle_all_menus' class='ws_button' href='javascript:void(0)'
97
  title='Toggle all menus for the selected role'><img src='<?php echo $icons['toggle-all']; ?>' alt="Toggle all" /></a>
98
+
99
+ <a id='ws_copy_role_permissions' class='ws_button' href='javascript:void(0)'
100
+ title='Copy all menu permissions from one role to another'><img src='<?php echo $icons['copy-permissions']; ?>' alt="Copy permissions" /></a>
101
  <?php endif; ?>
102
 
103
  <div class="clear"></div>
includes/menu-editor-core.php CHANGED
@@ -1076,9 +1076,6 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
1076
  $new_submenu = array();
1077
  $this->title_lookups = array();
1078
 
1079
- //Sort the menu by position
1080
- uasort($tree, 'ameMenuItem::compare_position');
1081
-
1082
  //Prepare the top menu
1083
  $first_nonseparator_found = false;
1084
  foreach ($tree as $topmenu){
@@ -1107,8 +1104,6 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
1107
  $has_submenu_icons = false;
1108
  if( !empty($topmenu['items']) ){
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
@@ -1125,6 +1120,9 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
1125
  //Keep track of which menus have items with icons.
1126
  $has_submenu_icons = $has_submenu_icons || !empty($item['has_submenu_icon']);
1127
  }
 
 
 
1128
  }
1129
 
1130
  //The ame-has-submenu-icons class lets us change the appearance of all submenu items at once,
@@ -1137,6 +1135,9 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
1137
  $new_tree[] = $topmenu;
1138
  }
1139
 
 
 
 
1140
  //Use only the highest-priority capability for each URL.
1141
  foreach($this->page_access_lookup as $url => $capabilities) {
1142
  ksort($capabilities);
@@ -2006,11 +2007,17 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
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
 
2012
  //The current URL must have as few extra parameters as possible.
2013
- $extra_params = array_diff_assoc($current_url['params'], $item_url['params']);
2014
 
2015
  if ( $is_close_match && (count($different_params) == 0) && (count($extra_params) < $best_extra_params) ) {
2016
  $best_item = $item;
@@ -2064,6 +2071,44 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
2064
  return $parsed;
2065
  }
2066
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2067
  /**
2068
  * Check if two paths match. Intended for comparing WP admin URLs.
2069
  *
1076
  $new_submenu = array();
1077
  $this->title_lookups = array();
1078
 
 
 
 
1079
  //Prepare the top menu
1080
  $first_nonseparator_found = false;
1081
  foreach ($tree as $topmenu){
1104
  $has_submenu_icons = false;
1105
  if( !empty($topmenu['items']) ){
1106
  $items = $topmenu['items'];
 
 
1107
 
1108
  foreach ($items as $item) {
1109
  //Skip missing and hidden items
1120
  //Keep track of which menus have items with icons.
1121
  $has_submenu_icons = $has_submenu_icons || !empty($item['has_submenu_icon']);
1122
  }
1123
+
1124
+ //Sort by position
1125
+ uasort($new_items, 'ameMenuItem::compare_position');
1126
  }
1127
 
1128
  //The ame-has-submenu-icons class lets us change the appearance of all submenu items at once,
1135
  $new_tree[] = $topmenu;
1136
  }
1137
 
1138
+ //Sort the menu by position
1139
+ uasort($new_tree, 'ameMenuItem::compare_position');
1140
+
1141
  //Use only the highest-priority capability for each URL.
1142
  foreach($this->page_access_lookup as $url => $capabilities) {
1143
  ksort($capabilities);
2007
  }
2008
  }
2009
 
2010
+ //Special case: In WP 4.0+ the URL of the "Customize" menu changes often due to a "return" query parameter
2011
+ //that contains the current page URL. To reliably recognize this item, we should ignore that parameter.
2012
+ if ( $this->endsWith($item_url['path'], 'customize.php') ) {
2013
+ unset($item_url['params']['return']);
2014
+ }
2015
+
2016
  //The current URL must match all query parameters of the item URL.
2017
+ $different_params = $this->arrayDiffAssocRecursive($item_url['params'], $current_url['params']);
2018
 
2019
  //The current URL must have as few extra parameters as possible.
2020
+ $extra_params = $this->arrayDiffAssocRecursive($current_url['params'], $item_url['params']);
2021
 
2022
  if ( $is_close_match && (count($different_params) == 0) && (count($extra_params) < $best_extra_params) ) {
2023
  $best_item = $item;
2071
  return $parsed;
2072
  }
2073
 
2074
+ /**
2075
+ * Get the difference of two arrays.
2076
+ *
2077
+ * This methods works like array_diff_assoc(), except it also supports nested arrays by comparing them recursively.
2078
+ *
2079
+ * @param array $array1 The base array.
2080
+ * @param array $array2 The array to compare to.
2081
+ * @return array An associative array of values from $array1 that are not present in $array2.
2082
+ */
2083
+ private function arrayDiffAssocRecursive($array1, $array2) {
2084
+ $difference = array();
2085
+
2086
+ foreach($array1 as $key => $value) {
2087
+ if ( !array_key_exists($key, $array2) ) {
2088
+ $difference[$key] = $value;
2089
+ continue;
2090
+ }
2091
+
2092
+ $otherValue = $array2[$key];
2093
+ if ( is_array($value) !== is_array($otherValue) ) {
2094
+ //If only one of the two values is an array then they can't be equal.
2095
+ $difference[$key] = $value;
2096
+ } elseif ( is_array($value) ) {
2097
+ //Compare array values recursively.
2098
+ $subDiff = $this->arrayDiffAssocRecursive($value, $otherValue);
2099
+ if( !empty($subDiff) ) {
2100
+ $difference[$key] = $subDiff;
2101
+ }
2102
+
2103
+ //Like the original array_diff_assoc(), we compare the values as strings.
2104
+ } elseif ( (string)$value !== (string)$array2[$key] ) {
2105
+ $difference[$key] = $value;
2106
+ }
2107
+ }
2108
+
2109
+ return $difference;
2110
+ }
2111
+
2112
  /**
2113
  * Check if two paths match. Intended for comparing WP admin URLs.
2114
  *
includes/menu-item.php CHANGED
@@ -397,7 +397,7 @@ abstract class ameMenuItem {
397
  }
398
 
399
  if ( self::is_hook_or_plugin_page($menu_url, $parent_url) ) {
400
- $base_file = self::is_hook_or_plugin_page($parent_url) ? 'admin.php' : $parent_url;
401
  $url = add_query_arg(array('page' => $menu_url), $base_file);
402
  } else {
403
  $url = $menu_url;
@@ -411,15 +411,23 @@ abstract class ameMenuItem {
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
@@ -430,26 +438,8 @@ abstract class ameMenuItem {
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
- }
446
-
447
- $hasHook = (get_plugin_page_hook($page_url, $parent_page_url) !== null);
448
- if ( $hasHook ) {
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);
@@ -460,6 +450,43 @@ abstract class ameMenuItem {
460
  return false;
461
  }
462
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
463
  /**
464
  * Check if a field is currently set to its default value.
465
  *
397
  }
398
 
399
  if ( self::is_hook_or_plugin_page($menu_url, $parent_url) ) {
400
+ $base_file = self::is_wp_admin_file($parent_url) ? $parent_url : 'admin.php';
401
  $url = add_query_arg(array('page' => $menu_url), $base_file);
402
  } else {
403
  $url = $menu_url;
411
  }
412
  $pageFile = self::remove_query_from($page_url);
413
 
414
+ //Files in /wp-admin are part of WP core so they're not plugin pages.
415
+ if ( self::is_wp_admin_file($pageFile) ) {
416
+ return false;
417
+ }
418
+
419
+ $hasHook = (get_plugin_page_hook($page_url, $parent_page_url) !== null);
420
+ if ( $hasHook ) {
421
+ return true;
422
+ }
423
+
424
  /*
425
  * Special case: Absolute paths.
426
  *
427
  * - add_submenu_page() applies plugin_basename() to the menu slug, so we don't need to worry about plugin
428
  * paths. However, absolute paths that *don't* point point to the plugins directory can be a problem.
429
  *
430
+ * - Due to a known PHP bug, certain invalid paths can crash PHP. See self::is_safe_to_append().
 
 
431
  *
432
  * - WP 3.9.2 and 4.0+ unintentionally break menu URLs like "foo.php?page=c:\a\b.php" because esc_url()
433
  * interprets the part before the colon as an invalid protocol. As a result, such links have an empty URL
438
  * can still be used as unique slugs for menus with hook callbacks, so we shouldn't reject them outright.
439
  * Related: https://core.trac.wordpress.org/ticket/10011
440
  */
441
+ $allowPathConcatenation = self::is_safe_to_append($pageFile);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
442
 
 
443
  $pluginFileExists = $allowPathConcatenation
444
  && ($page_url != 'index.php')
445
  && is_file(WP_PLUGIN_DIR . '/' . $pageFile);
450
  return false;
451
  }
452
 
453
+ /**
454
+ * Check if a file exists inside the /wp-admin subdirectory.
455
+ *
456
+ * @param string $filename
457
+ * @return bool
458
+ */
459
+ private static function is_wp_admin_file($filename) {
460
+ //Check our hard-coded list of admin pages first. It's measurably faster than
461
+ //hitting the disk with is_file().
462
+ if ( isset(self::$known_wp_admin_files[$filename]) ) {
463
+ return self::$known_wp_admin_files[$filename];
464
+ }
465
+
466
+ //Now actually check the filesystem.
467
+ $adminFileExists = self::is_safe_to_append($filename)
468
+ && is_file(ABSPATH . 'wp-admin/' . $filename);
469
+
470
+ //Cache the result for later. We can generally expect more than one call per top level menu URL.
471
+ self::$known_wp_admin_files[$filename] = $adminFileExists;
472
+
473
+ return $adminFileExists;
474
+ }
475
+
476
+ /**
477
+ * Verify that it's safe to append a given filename to another path.
478
+ *
479
+ * If we blindly append an absolute path to another path, we can get something like "C:\a\b/wp-admin/C:\c\d.php".
480
+ * PHP 5.2.5 has a known bug where calling file_exists() on that kind of an invalid filename will cause
481
+ * a timeout and a crash in some configurations. See: https://bugs.php.net/bug.php?id=44412
482
+ *
483
+ * @param string $filename
484
+ * @return bool
485
+ */
486
+ private static function is_safe_to_append($filename) {
487
+ return (substr($filename, 1, 1) !== ':'); //Reject "C:\whatever" and similar.
488
+ }
489
+
490
  /**
491
  * Check if a field is currently set to its default value.
492
  *
js/menu-editor.js CHANGED
@@ -144,6 +144,8 @@ var AmeCapabilityManager = (function(roles, users) {
144
  case 'user':
145
  specificity = 10;
146
  break;
 
 
147
  }
148
  return specificity;
149
  };
@@ -203,11 +205,7 @@ var itemTemplates = {
203
  */
204
  function setInputValue(input, value) {
205
  if (input.attr('type') == 'checkbox'){
206
- if (value){
207
- input.attr('checked', 'checked');
208
- } else {
209
- input.removeAttr('checked');
210
- }
211
  } else {
212
  input.val(value);
213
  }
@@ -1633,7 +1631,7 @@ $(document).ready(function(){
1633
 
1634
  var actorHasAccess = actorCanAccessMenu(menuItem, actor);
1635
  if (actorHasAccess) {
1636
- checkbox.attr('checked', 'checked');
1637
  }
1638
 
1639
  alternate = (alternate == '') ? 'alternate' : '';
@@ -2429,6 +2427,82 @@ $(document).ready(function(){
2429
  });
2430
  });
2431
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2432
  /*************************************************************************
2433
  Item toolbar buttons
2434
  *************************************************************************/
@@ -2758,7 +2832,8 @@ $(document).ready(function(){
2758
  });
2759
 
2760
  $('#ws_import_menu').click(function(){
2761
- $('#import_progress_notice, #import_progress_notice2, #import_complete_notice').hide();
 
2762
  $('#import_menu_form').resetForm();
2763
  //The "Upload" button is disabled until the user selects a file
2764
  $('#ws_start_import').attr('disabled', 'disabled');
@@ -2772,6 +2847,21 @@ $(document).ready(function(){
2772
  $('#ws_start_import').prop('disabled', ! $(this).val() );
2773
  });
2774
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2775
  //AJAXify the upload form
2776
  $('#import_menu_form').ajaxForm({
2777
  dataType : 'json',
@@ -2793,7 +2883,9 @@ $(document).ready(function(){
2793
  $('#ws_start_import').attr('disabled', 'disabled');
2794
  return true;
2795
  },
2796
- success: function(data){
 
 
2797
  var importDialog = $('#import_dialog');
2798
  if ( !importDialog.dialog('isOpen') ){
2799
  //Whoops, the user closed the dialog while the upload was in progress.
@@ -2801,13 +2893,17 @@ $(document).ready(function(){
2801
  return;
2802
  }
2803
 
 
 
 
 
 
2804
  if ( typeof data['error'] != 'undefined' ){
2805
  alert(data.error);
2806
  //Let the user try again
2807
  $('#import_menu_form').resetForm();
2808
  importDialog.find('.hide-when-uploading').show();
2809
  }
2810
- $('#import_progress_notice').hide();
2811
 
2812
  if ( (typeof data['tree'] != 'undefined') && data.tree ){
2813
  //Whee, we got back a (seemingly) valid menu. A veritable miracle!
@@ -2823,6 +2919,9 @@ $(document).ready(function(){
2823
  }), 500);
2824
  }
2825
 
 
 
 
2826
  }
2827
  });
2828
 
144
  case 'user':
145
  specificity = 10;
146
  break;
147
+ default:
148
+ specificity = 0;
149
  }
150
  return specificity;
151
  };
205
  */
206
  function setInputValue(input, value) {
207
  if (input.attr('type') == 'checkbox'){
208
+ input.prop('checked', value);
 
 
 
 
209
  } else {
210
  input.val(value);
211
  }
1631
 
1632
  var actorHasAccess = actorCanAccessMenu(menuItem, actor);
1633
  if (actorHasAccess) {
1634
+ checkbox.prop('checked', true);
1635
  }
1636
 
1637
  alternate = (alternate == '') ? 'alternate' : '';
2427
  });
2428
  });
2429
 
2430
+ //Copy all menu permissions from one role to another.
2431
+ var copyPermissionsDialog = $('#ws-ame-copy-permissions-dialog').dialog({
2432
+ autoOpen: false,
2433
+ modal: true,
2434
+ closeText: ' ',
2435
+ draggable: false
2436
+ });
2437
+
2438
+ //Populate source/destination lists.
2439
+ var sourceActorList = $('#ame-copy-source-actor'), destinationActorList = $('#ame-copy-destination-actor');
2440
+ $.each(wsEditorData.actors, function(actor, name) {
2441
+ var option = $('<option>', {val: actor, text: name});
2442
+ sourceActorList.append(option);
2443
+ destinationActorList.append(option.clone());
2444
+ });
2445
+
2446
+ //The "Copy permissions" toolbar button.
2447
+ $('#ws_copy_role_permissions').click(function() {
2448
+ //Pre-select the current actor as the destination.
2449
+ if (selectedActor !== null) {
2450
+ destinationActorList.val(selectedActor);
2451
+ }
2452
+ copyPermissionsDialog.dialog('open');
2453
+ });
2454
+
2455
+ //Actually copy the permissions when the user click the confirmation button.
2456
+ var copyConfirmationButton = $('#ws-ame-confirm-copy-permissions');
2457
+ copyConfirmationButton.click(function() {
2458
+ var sourceActor = sourceActorList.val();
2459
+ var destinationActor = destinationActorList.val();
2460
+
2461
+ if (sourceActor === null || destinationActor === null) {
2462
+ alert('Select a source and a destination first.');
2463
+ return;
2464
+ }
2465
+
2466
+ //Iterate over all menu items and copy the permissions from one actor to the other.
2467
+ var allMenuNodes = $('.ws_menu', '#ws_menu_box').add('.ws_item', '#ws_submenu_box');
2468
+ allMenuNodes.each(function() {
2469
+ var node = $(this);
2470
+ var menuItem = node.data('menu_item');
2471
+
2472
+ //Only change permissions when they don't match. This ensures we won't unnecessarily overwrite default
2473
+ //permissions and bloat the configuration with extra grant_access entries.
2474
+ var sourceAccess = actorCanAccessMenu(menuItem, sourceActor);
2475
+ var destinationAccess = actorCanAccessMenu(menuItem, destinationActor);
2476
+ if (sourceAccess !== destinationAccess) {
2477
+ setActorAccess(node, destinationActor, sourceAccess);
2478
+ //Note: In theory, we could also look at the default permissions for destinationActor and
2479
+ //revert to default instead of overwriting if that would make the two actors' permissions match.
2480
+ }
2481
+ });
2482
+
2483
+ //If the user is currently looking at the destination actor, force the UI to refresh
2484
+ //so that they can see the new permissions.
2485
+ if (selectedActor === destinationActor) {
2486
+ //This is a bit of a hack, but right now there's no better way to refresh all items at once.
2487
+ setSelectedActor(null);
2488
+ setSelectedActor(destinationActor);
2489
+ }
2490
+
2491
+ //All done.
2492
+ copyPermissionsDialog.dialog('close');
2493
+ });
2494
+
2495
+ //Only enable the copy button when the user selects a valid source and destination.
2496
+ copyConfirmationButton.prop('disabled', true);
2497
+ sourceActorList.add(destinationActorList).click(function() {
2498
+ var sourceActor = sourceActorList.val();
2499
+ var destinationActor = destinationActorList.val();
2500
+
2501
+ var validInputs = (sourceActor !== null) && (destinationActor !== null) && (sourceActor !== destinationActor);
2502
+ copyConfirmationButton.prop('disabled', !validInputs);
2503
+ });
2504
+
2505
+
2506
  /*************************************************************************
2507
  Item toolbar buttons
2508
  *************************************************************************/
2832
  });
2833
 
2834
  $('#ws_import_menu').click(function(){
2835
+ $('#import_progress_notice, #import_progress_notice2, #import_complete_notice, #ws_import_error').hide();
2836
+ $('#ws_import_panel').show();
2837
  $('#import_menu_form').resetForm();
2838
  //The "Upload" button is disabled until the user selects a file
2839
  $('#ws_start_import').attr('disabled', 'disabled');
2847
  $('#ws_start_import').prop('disabled', ! $(this).val() );
2848
  });
2849
 
2850
+ //This function displays unhandled server side errors. In theory, our upload handler always returns a well-formed
2851
+ //response even if there's an error. In practice, stuff can go wrong in unexpected ways (e.g. plugin conflicts).
2852
+ function handleUnexpectedImportError(xhr, errorMessage) {
2853
+ //The server-side code didn't catch this error, so it's probably something serious
2854
+ //and retrying won't work.
2855
+ $('#import_menu_form').resetForm();
2856
+ $('#ws_import_panel').hide();
2857
+
2858
+ //Display error information.
2859
+ $('#ws_import_error_message').text(errorMessage);
2860
+ $('#ws_import_error_http_code').text(xhr.status);
2861
+ $('#ws_import_error_response').text((xhr.responseText !== '') ? xhr.responseText : '[Empty response]');
2862
+ $('#ws_import_error').show();
2863
+ }
2864
+
2865
  //AJAXify the upload form
2866
  $('#import_menu_form').ajaxForm({
2867
  dataType : 'json',
2883
  $('#ws_start_import').attr('disabled', 'disabled');
2884
  return true;
2885
  },
2886
+ success: function(data, status, xhr) {
2887
+ $('#import_progress_notice').hide();
2888
+
2889
  var importDialog = $('#import_dialog');
2890
  if ( !importDialog.dialog('isOpen') ){
2891
  //Whoops, the user closed the dialog while the upload was in progress.
2893
  return;
2894
  }
2895
 
2896
+ if ( data === null ) {
2897
+ handleUnexpectedImportError(xhr, 'Invalid response from server. Please check your PHP error log.');
2898
+ return;
2899
+ }
2900
+
2901
  if ( typeof data['error'] != 'undefined' ){
2902
  alert(data.error);
2903
  //Let the user try again
2904
  $('#import_menu_form').resetForm();
2905
  importDialog.find('.hide-when-uploading').show();
2906
  }
 
2907
 
2908
  if ( (typeof data['tree'] != 'undefined') && data.tree ){
2909
  //Whee, we got back a (seemingly) valid menu. A veritable miracle!
2919
  }), 500);
2920
  }
2921
 
2922
+ },
2923
+ error: function(xhr, status, errorMessage) {
2924
+ handleUnexpectedImportError(xhr, errorMessage);
2925
  }
2926
  });
2927
 
js/menu-highlight-fix.js CHANGED
@@ -182,8 +182,8 @@ jQuery(function($) {
182
  (otherHighlightedMenus.length > 0);
183
 
184
  if (isWrongMenuHighlighted) {
185
- //Account for users who use a plugin to keep all menus expanded.
186
- var shouldCloseOtherMenus = $('li.wp-has-current-submenu', '#adminmenu').length <= 1;
187
  if (shouldCloseOtherMenus) {
188
  otherHighlightedMenus
189
  .add('> a', otherHighlightedMenus)
@@ -196,6 +196,14 @@ jQuery(function($) {
196
  if (parentMenu.hasClass('wp-has-submenu')) {
197
  parentMenuAndLink.addClass('wp-has-current-submenu wp-menu-open');
198
  }
 
 
 
 
 
 
 
 
199
  }
200
 
201
  if (isWrongItemHighlighted) {
182
  (otherHighlightedMenus.length > 0);
183
 
184
  if (isWrongMenuHighlighted) {
185
+ //Account for users who use the Expanded Admin Menus plugin to keep all menus expanded.
186
+ var shouldCloseOtherMenus = ! $('div.expand-arrow', '#adminmenu').get(0);
187
  if (shouldCloseOtherMenus) {
188
  otherHighlightedMenus
189
  .add('> a', otherHighlightedMenus)
196
  if (parentMenu.hasClass('wp-has-submenu')) {
197
  parentMenuAndLink.addClass('wp-has-current-submenu wp-menu-open');
198
  }
199
+
200
+ //Note: WordPress switches the admin menu between `position: fixed` and `position: relative` depending on
201
+ //how tall it is compared to the browser window. Opening a different submenu can change the menu's height,
202
+ //so we must trigger the position update to avoid bugs. If we don't, we can end up with a very tall menu
203
+ //that's not scrollable (due to being stuck with `position: fixed`).
204
+ if ((typeof window['stickyMenu'] === 'object') && (typeof window['stickyMenu']['update'] === 'function')) {
205
+ window.stickyMenu.update();
206
+ }
207
  }
208
 
209
  if (isWrongItemHighlighted) {
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.1
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.2
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
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,6 +63,15 @@ Plugins installed in the `mu-plugins` directory are treated as "always on", so y
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.
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.2
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.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.
69
+ * Fixed a regression in the menu highlighting algorithm.
70
+ * Fixed an "array to string conversion" notice caused by passing array data in the query string.
71
+ * Fixed menu scrolling occasionally not working when the user moved an item from one menu to another, much larger menu (e.g. having 20+ submenu items).
72
+ * Fixed a bug where moving a submenu item from a plugin menu that doesn't have a hook callback (i.e. an unusable menu serving as a placeholder) to a different menu would corrupt the menu item URL.
73
+ * Other minor bug fixes.
74
+
75
  = 1.4.1 =
76
  * Fixed "Appearance -> Customize" always showing up as "new" and ignoring custom settings.
77
  * Fixed a WooCommerce 2.2.1+ compatibility issue that caused a superfluous "WooCommerce -> WooCommerce" submenu item to show up. Normally this item is invisible.