Admin Menu Editor - Version 1.7

Version Description

  • Added a "Plugins" tab. It lets you hide specific plugins from other users. Note that this only affects the list on the "Plugins" page and tasks like editing plugin files, but it doesn't affect the admin menu.
  • Tested up to WordPress 4.6-beta3.
Download this release

Release Info

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

Code changes from version 1.6.2 to 1.7

includes/ame-utils.php ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Miscellaneous utility functions.
5
+ */
6
+ class ameUtils {
7
+
8
+ /**
9
+ * Get a value from a nested array or object based on a path.
10
+ *
11
+ * @param array|object $array Get an entry from this array.
12
+ * @param array|string $path A list of array keys in hierarchy order, or a string path like "foo.bar.baz".
13
+ * @param mixed $default The value to return if the specified path is not found.
14
+ * @param string $separator Path element separator. Only applies to string paths.
15
+ * @return mixed
16
+ */
17
+ public static function get($array, $path, $default = null, $separator = '.') {
18
+ if (is_string($path)) {
19
+ $path = explode($separator, $path);
20
+ }
21
+ if (empty($path)) {
22
+ return $default;
23
+ }
24
+
25
+ //Follow the $path into $input as far as possible.
26
+ $currentValue = $array;
27
+ $pathExists = true;
28
+ foreach($path as $node) {
29
+ if (is_array($currentValue) && array_key_exists($node, $currentValue)) {
30
+ $currentValue = $currentValue[$node];
31
+ } else if (is_object($currentValue) && property_exists($currentValue, $node)) {
32
+ $currentValue = $currentValue->$node;
33
+ } else {
34
+ $pathExists = false;
35
+ break;
36
+ }
37
+ }
38
+
39
+ if ($pathExists) {
40
+ return $currentValue;
41
+ }
42
+ return $default;
43
+ }
44
+
45
+ /**
46
+ * Get the first non-root directory from a path.
47
+ *
48
+ * Examples:
49
+ * "foo/bar" => "foo"
50
+ * "/foo/bar/baz.txt" => "foo"
51
+ * "bar" => null
52
+ * "baz/" => "baz"
53
+ * "/" => null
54
+ *
55
+ * @param string $fileName
56
+ * @return string|null
57
+ */
58
+ public static function getFirstDirectory($fileName) {
59
+ $fileName = ltrim($fileName, '/');
60
+
61
+ $segments = explode('/', $fileName, 2);
62
+ if ((count($segments) > 1) && ($segments[0] !== '')) {
63
+ return $segments[0];
64
+ }
65
+ return null;
66
+ }
67
+ }
includes/ameArray.php DELETED
@@ -1,11 +0,0 @@
1
- <?php
2
-
3
- /**
4
- * Array utility functions, a la Lodash.
5
- */
6
- class ameArray {
7
- public static function get($array, $path, $default = null) {
8
- //todo
9
- return $default;
10
- }
11
- }
 
 
 
 
 
 
 
 
 
 
 
includes/editor-page.php CHANGED
@@ -54,12 +54,8 @@ if ( !apply_filters('admin_menu_editor_is_pro', false) ){
54
  }
55
 
56
  ?>
57
- <div class="<?php echo esc_attr(implode(' ', $wrap_classes)); ?>">
58
- <?php echo '<', WPMenuEditor::$admin_heading_tag, ' id="ws_ame_editor_heading">'; ?>
59
- <?php echo apply_filters('admin_menu_editor-self_page_title', 'Menu Editor'); ?>
60
- <?php echo '</', WPMenuEditor::$admin_heading_tag, '>'; ?>
61
 
62
- <?php do_action('admin_menu_editor-display_tabs'); ?>
63
 
64
  <?php
65
  if ( !empty($_GET['message']) ){
@@ -313,7 +309,7 @@ function ame_output_sort_buttons($icons) {
313
 
314
  </div> <!-- / .ws_menu_editor -->
315
 
316
- </div> <!-- / .wrap -->
317
 
318
 
319
 
54
  }
55
 
56
  ?>
 
 
 
 
57
 
58
+ <?php do_action('admin_menu_editor-display_header'); ?>
59
 
60
  <?php
61
  if ( !empty($_GET['message']) ){
309
 
310
  </div> <!-- / .ws_menu_editor -->
311
 
312
+ <?php do_action('admin_menu_editor-display_footer'); ?>
313
 
314
 
315
 
includes/menu-editor-core.php CHANGED
@@ -11,10 +11,12 @@ if (class_exists('WPMenuEditor')){
11
  $thisDirectory = dirname(__FILE__);
12
  require $thisDirectory . '/shadow_plugin_framework.php';
13
  require $thisDirectory . '/role-utils.php';
 
14
  require $thisDirectory . '/menu-item.php';
15
  require $thisDirectory . '/menu.php';
16
  require $thisDirectory . '/auto-versioning.php';
17
  require $thisDirectory . '/ajax-helper.php';
 
18
 
19
  class WPMenuEditor extends MenuEd_ShadowPluginFramework {
20
  const WPML_CONTEXT = 'admin-menu-editor menu texts';
@@ -240,12 +242,18 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
240
  //Multisite: Clear role and capability caches when switching to another site.
241
  add_action('switch_blog', array($this, 'clear_site_specific_caches'), 10, 0);
242
 
 
243
  add_action('admin_menu_editor-display_tabs', array($this, 'display_editor_tabs'));
 
 
244
 
245
  //Modules
246
  include dirname(__FILE__) . '/../modules/actor-selector/actor-selector.php';
247
  new ameActorSelector($this);
248
 
 
 
 
249
  $proModuleDirectory = AME_ROOT_DIR . '/extras/modules';
250
  if ( @is_dir($proModuleDirectory) ) {
251
  //The widget module requires PHP 5.3.
@@ -256,6 +264,11 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
256
  require_once $proModuleDirectory . '/dashboard-widget-editor/load.php';
257
  new ameWidgetEditor($this);
258
  }
 
 
 
 
 
259
  }
260
 
261
  //Set up the tabs for the menu editor page.
@@ -1990,6 +2003,25 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
1990
  require dirname(__FILE__) . '/editor-page.php';
1991
  }
1992
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1993
  /**
1994
  * Display the tabs for the settings page.
1995
  */
@@ -2038,9 +2070,7 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
2038
  * @return bool
2039
  */
2040
  protected function is_editor_page() {
2041
- return is_admin()
2042
- && isset($this->get['page']) && ($this->get['page'] == 'menu_editor')
2043
- && ($this->current_tab === 'editor');
2044
  }
2045
 
2046
  /**
@@ -2049,9 +2079,19 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
2049
  * @return bool
2050
  */
2051
  protected function is_settings_page() {
 
 
 
 
 
 
 
 
 
 
2052
  return is_admin()
2053
- && ($this->current_tab === 'settings')
2054
- && isset($this->get['page']) && ($this->get['page'] == 'menu_editor');
2055
  }
2056
 
2057
  /**
@@ -3236,7 +3276,7 @@ class WPMenuEditor extends MenuEd_ShadowPluginFramework {
3236
 
3237
  }
3238
 
3239
- private function is_pro_version() {
3240
  return apply_filters('admin_menu_editor_is_pro', false);
3241
  }
3242
 
11
  $thisDirectory = dirname(__FILE__);
12
  require $thisDirectory . '/shadow_plugin_framework.php';
13
  require $thisDirectory . '/role-utils.php';
14
+ require $thisDirectory . '/ame-utils.php';
15
  require $thisDirectory . '/menu-item.php';
16
  require $thisDirectory . '/menu.php';
17
  require $thisDirectory . '/auto-versioning.php';
18
  require $thisDirectory . '/ajax-helper.php';
19
+ require $thisDirectory . '/module.php';
20
 
21
  class WPMenuEditor extends MenuEd_ShadowPluginFramework {
22
  const WPML_CONTEXT = 'admin-menu-editor menu texts';
242
  //Multisite: Clear role and capability caches when switching to another site.
243
  add_action('switch_blog', array($this, 'clear_site_specific_caches'), 10, 0);
244
 
245
+ //Utility actions. Modules can use them in their templates.
246
  add_action('admin_menu_editor-display_tabs', array($this, 'display_editor_tabs'));
247
+ add_action('admin_menu_editor-display_header', array($this, 'display_settings_page_header'));
248
+ add_action('admin_menu_editor-display_footer', array($this, 'display_settings_page_footer'));
249
 
250
  //Modules
251
  include dirname(__FILE__) . '/../modules/actor-selector/actor-selector.php';
252
  new ameActorSelector($this);
253
 
254
+ include dirname(__FILE__) . '/../modules/plugin-visibility/plugin-visibility.php';
255
+ new amePluginVisibility($this);
256
+
257
  $proModuleDirectory = AME_ROOT_DIR . '/extras/modules';
258
  if ( @is_dir($proModuleDirectory) ) {
259
  //The widget module requires PHP 5.3.
264
  require_once $proModuleDirectory . '/dashboard-widget-editor/load.php';
265
  new ameWidgetEditor($this);
266
  }
267
+
268
+ if ( is_file($proModuleDirectory . '/super-users/super-users.php') ) {
269
+ require $proModuleDirectory . '/super-users/super-users.php';
270
+ new ameSuperUsers($this);
271
+ }
272
  }
273
 
274
  //Set up the tabs for the menu editor page.
2003
  require dirname(__FILE__) . '/editor-page.php';
2004
  }
2005
 
2006
+ /**
2007
+ * Display the header of the "Menu Editor" page.
2008
+ * This includes the page heading and tab list.
2009
+ */
2010
+ public function display_settings_page_header() {
2011
+ echo '<div class="wrap">';
2012
+ printf(
2013
+ '<%1$s id="ws_ame_editor_heading">%2$s</%1$s>',
2014
+ self::$admin_heading_tag,
2015
+ apply_filters('admin_menu_editor-self_page_title', 'Menu Editor')
2016
+ );
2017
+
2018
+ do_action('admin_menu_editor-display_tabs');
2019
+ }
2020
+
2021
+ public function display_settings_page_footer() {
2022
+ echo '</div>'; //div.wrap
2023
+ }
2024
+
2025
  /**
2026
  * Display the tabs for the settings page.
2027
  */
2070
  * @return bool
2071
  */
2072
  protected function is_editor_page() {
2073
+ return $this->is_tab_open('editor');
 
 
2074
  }
2075
 
2076
  /**
2079
  * @return bool
2080
  */
2081
  protected function is_settings_page() {
2082
+ return $this->is_tab_open('settings');
2083
+ }
2084
+
2085
+ /**
2086
+ * Check if the specified AME settings tab is currently open.
2087
+ *
2088
+ * @param string $tab_slug
2089
+ * @return bool
2090
+ */
2091
+ public function is_tab_open($tab_slug) {
2092
  return is_admin()
2093
+ && ($this->current_tab === $tab_slug)
2094
+ && isset($this->get['page']) && ($this->get['page'] == 'menu_editor');
2095
  }
2096
 
2097
  /**
3276
 
3277
  }
3278
 
3279
+ public function is_pro_version() {
3280
  return apply_filters('admin_menu_editor_is_pro', false);
3281
  }
3282
 
includes/module.php ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ abstract class ameModule {
3
+ protected $tabSlug = '';
4
+ protected $tabTitle = '';
5
+ protected $tabOrder = 10;
6
+
7
+ protected $moduleId = '';
8
+ protected $moduleDir = '';
9
+
10
+ /**
11
+ * @var WPMenuEditor
12
+ */
13
+ protected $menuEditor;
14
+
15
+ public function __construct($menuEditor) {
16
+ $this->menuEditor = $menuEditor;
17
+
18
+ if ( class_exists('ReflectionClass', false) ) {
19
+ $reflector = new ReflectionClass(get_class($this));
20
+ $this->moduleDir = dirname($reflector->getFileName());
21
+ $this->moduleId = basename($this->moduleDir);
22
+ }
23
+
24
+ //Register the module tab.
25
+ if ( ($this->tabSlug !== '') && is_string($this->tabSlug) ) {
26
+ add_action('admin_menu_editor-tabs', array($this, 'addTab'), $this->tabOrder);
27
+ add_action('admin_menu_editor-section-' . $this->tabSlug, array($this, 'displaySettingsPage'));
28
+ }
29
+ }
30
+
31
+ public function addTab($tabs) {
32
+ $tabs[$this->tabSlug] = !empty($this->tabTitle) ? $this->tabTitle : $this->tabSlug;
33
+ return $tabs;
34
+ }
35
+
36
+ public function displaySettingsPage() {
37
+ $this->menuEditor->display_settings_page_header();
38
+
39
+ if ( !$this->outputMainTemplate() ) {
40
+ printf("[ %1\$s : Module \"%2\$s\" doesn't have a primary template. ]", __METHOD__, $this->moduleId);
41
+ }
42
+
43
+ $this->menuEditor->display_settings_page_footer();
44
+ }
45
+
46
+ protected function getTabUrl($queryParameters = array()) {
47
+ $queryParameters = array_merge(
48
+ array(
49
+ 'page' => 'menu_editor',
50
+ 'sub_section' => $this->tabSlug
51
+ ),
52
+ $queryParameters
53
+ );
54
+ return add_query_arg($queryParameters, admin_url('options-general.php'));
55
+ }
56
+
57
+ protected function outputMainTemplate() {
58
+ return $this->outputTemplate($this->moduleId);
59
+ }
60
+
61
+ protected function outputTemplate($name) {
62
+ $templateFile = $this->moduleDir . '/' . $name . '-template.php';
63
+ if ( file_exists($templateFile) ) {
64
+ /** @noinspection PhpIncludeInspection */
65
+ require $templateFile;
66
+ return true;
67
+ }
68
+ return false;
69
+ }
70
+ }
includes/settings-page.php CHANGED
@@ -15,12 +15,7 @@ $formActionUrl = add_query_arg('noheader', 1, $settings_page_url);
15
  $isProVersion = apply_filters('admin_menu_editor_is_pro', false);
16
  ?>
17
 
18
- <div class="wrap">
19
- <<?php echo WPMenuEditor::$admin_heading_tag; ?> id="ws_ame_editor_heading">
20
- <?php echo apply_filters('admin_menu_editor-self_page_title', 'Menu Editor'); ?>
21
- </<?php echo WPMenuEditor::$admin_heading_tag; ?>>
22
-
23
- <?php do_action('admin_menu_editor-display_tabs'); ?>
24
 
25
  <form method="post" action="<?php echo esc_attr($formActionUrl); ?>" id="ws_plugin_settings_form">
26
 
@@ -282,7 +277,7 @@ $isProVersion = apply_filters('admin_menu_editor_is_pro', false);
282
  ?>
283
  </form>
284
 
285
- </div>
286
 
287
  <script type="text/javascript">
288
  jQuery(function($) {
15
  $isProVersion = apply_filters('admin_menu_editor_is_pro', false);
16
  ?>
17
 
18
+ <?php do_action('admin_menu_editor-display_header'); ?>
 
 
 
 
 
19
 
20
  <form method="post" action="<?php echo esc_attr($formActionUrl); ?>" id="ws_plugin_settings_form">
21
 
277
  ?>
278
  </form>
279
 
280
+ <?php do_action('admin_menu_editor-display_footer'); ?>
281
 
282
  <script type="text/javascript">
283
  jQuery(function($) {
js/actor-manager.ts CHANGED
@@ -213,7 +213,7 @@ class AmeActorManager {
213
  }
214
  }
215
 
216
- hasCap(actorId, capability, context?: {[actor: string] : any}): boolean {
217
  context = context || {};
218
  return this.actorHasCap(actorId, capability, [context, this.grantedCapabilities]);
219
  }
213
  }
214
  }
215
 
216
+ hasCap(actorId: string, capability, context?: {[actor: string] : any}): boolean {
217
  context = context || {};
218
  return this.actorHasCap(actorId, capability, [context, this.grantedCapabilities]);
219
  }
js/menu-editor.js CHANGED
@@ -1381,7 +1381,6 @@ function encodeMenuAsJSON(tree){
1381
 
1382
  //Compress the admin menu.
1383
  tree = compressMenu(tree);
1384
- console.log(tree); //xxxx debugging code
1385
 
1386
  return $.toJSON(tree);
1387
  }
@@ -1505,7 +1504,7 @@ function readItemState(itemDiv, position){
1505
  position = (typeof position === 'undefined') ? 0 : position;
1506
 
1507
  itemDiv = $(itemDiv);
1508
- var item = $.extend({}, wsEditorData.blankMenuItem, itemDiv.data('menu_item'), readAllFields(itemDiv));
1509
 
1510
  item.defaults = itemDiv.data('menu_item').defaults;
1511
 
@@ -2156,7 +2155,7 @@ function ameOnDomReady() {
2156
 
2157
  AmeItemAccessEditor.setup({
2158
  api: AmeEditorApi,
2159
- actors: wsEditorData.actors,
2160
  postTypes: wsEditorData.postTypes,
2161
  taxonomies: wsEditorData.taxonomies,
2162
  lodash: _,
@@ -3415,14 +3414,14 @@ function ameOnDomReady() {
3415
 
3416
  //The new menu starts out rather bare
3417
  var randomId = randomMenuId();
3418
- var menu = $.extend({}, wsEditorData.blankMenuItem, {
3419
  custom: true, //Important : flag the new menu as custom, or it won't show up after saving.
3420
  template_id : '',
3421
  menu_title : 'Custom Menu ' + ws_paste_count,
3422
  file : randomId,
3423
- items: [],
3424
- defaults: $.extend({}, itemTemplates.getDefaults(''))
3425
  });
 
3426
 
3427
  //Make it accessible only to the current actor if one is selected.
3428
  if (actorSelectorWidget.selectedActor !== null) {
@@ -3771,14 +3770,14 @@ function ameOnDomReady() {
3771
 
3772
  ws_paste_count++;
3773
 
3774
- var entry = $.extend({}, wsEditorData.blankMenuItem, {
3775
  custom: true,
3776
  template_id : '',
3777
  menu_title : 'Custom Item ' + ws_paste_count,
3778
  file : randomMenuId(),
3779
- items: [],
3780
- defaults: $.extend({}, itemTemplates.getDefaults(''))
3781
  });
 
3782
 
3783
  //Make it accessible to only the currently selected actor.
3784
  if (actorSelectorWidget.selectedActor !== null) {
1381
 
1382
  //Compress the admin menu.
1383
  tree = compressMenu(tree);
 
1384
 
1385
  return $.toJSON(tree);
1386
  }
1504
  position = (typeof position === 'undefined') ? 0 : position;
1505
 
1506
  itemDiv = $(itemDiv);
1507
+ var item = $.extend(true, {}, wsEditorData.blankMenuItem, itemDiv.data('menu_item'), readAllFields(itemDiv));
1508
 
1509
  item.defaults = itemDiv.data('menu_item').defaults;
1510
 
2155
 
2156
  AmeItemAccessEditor.setup({
2157
  api: AmeEditorApi,
2158
+ actorSelector: actorSelectorWidget,
2159
  postTypes: wsEditorData.postTypes,
2160
  taxonomies: wsEditorData.taxonomies,
2161
  lodash: _,
3414
 
3415
  //The new menu starts out rather bare
3416
  var randomId = randomMenuId();
3417
+ var menu = $.extend(true, {}, wsEditorData.blankMenuItem, {
3418
  custom: true, //Important : flag the new menu as custom, or it won't show up after saving.
3419
  template_id : '',
3420
  menu_title : 'Custom Menu ' + ws_paste_count,
3421
  file : randomId,
3422
+ items: []
 
3423
  });
3424
+ menu.defaults = $.extend(true, {}, itemTemplates.getDefaults(''));
3425
 
3426
  //Make it accessible only to the current actor if one is selected.
3427
  if (actorSelectorWidget.selectedActor !== null) {
3770
 
3771
  ws_paste_count++;
3772
 
3773
+ var entry = $.extend(true, {}, wsEditorData.blankMenuItem, {
3774
  custom: true,
3775
  template_id : '',
3776
  menu_title : 'Custom Item ' + ws_paste_count,
3777
  file : randomMenuId(),
3778
+ items: []
 
3779
  });
3780
+ entry.defaults = $.extend(true, {}, itemTemplates.getDefaults(''));
3781
 
3782
  //Make it accessible to only the currently selected actor.
3783
  if (actorSelectorWidget.selectedActor !== null) {
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.6.2
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.7
7
  Author: Janis Elsts
8
  Author URI: http://w-shadow.com/blog/
9
  */
modules/access-editor/access-editor.js CHANGED
@@ -1,4 +1,4 @@
1
- /* globals AmeCapabilityManager, jQuery */
2
 
3
  window.AmeItemAccessEditor = (function ($) {
4
  'use strict';
@@ -16,7 +16,7 @@ window.AmeItemAccessEditor = (function ($) {
16
  var _,
17
  api,
18
  isProVersion = false,
19
- actors,
20
  postTypes,
21
  taxonomies,
22
 
@@ -79,7 +79,9 @@ window.AmeItemAccessEditor = (function ($) {
79
  return $(this).data('actor') === selectedActor;
80
  }).addClass('ws_cpt_selected_role');
81
 
82
- $editor.find('.ws_aed_selected_actor_name').text(actors[selectedActor]);
 
 
83
  }
84
 
85
  if (hasExtendedPermissions) {
@@ -323,6 +325,7 @@ window.AmeItemAccessEditor = (function ($) {
323
  /**
324
  * @param {AmeEditorApi} config.api
325
  * @param {Object} config.actors
 
326
  * @param {Object} config.postTypes
327
  * @param {Object} config.taxonomies
328
  * @param {lodash} config.lodash
@@ -334,7 +337,7 @@ window.AmeItemAccessEditor = (function ($) {
334
  setup: function(config) {
335
  _ = config.lodash;
336
  api = config.api;
337
- actors = config.actors; //Note: This can change on the fly if the user changes visible users.
338
 
339
  postTypes = config.postTypes;
340
  taxonomies = config.taxonomies;
@@ -377,26 +380,25 @@ window.AmeItemAccessEditor = (function ($) {
377
 
378
  //Generate the actor list.
379
  var table = $editor.find('.ws_role_table_body tbody').empty(),
380
- alternate = '';
381
- for(var actor in actors) {
382
- if (!actors.hasOwnProperty(actor)) {
383
- continue;
384
- }
385
 
386
- var checkboxId = 'allow_' + actor.replace(/[^a-zA-Z0-9_]/g, '_');
387
  var checkbox = $('<input type="checkbox">').addClass('ws_role_access').attr('id', checkboxId);
388
 
389
- var actorHasAccess = api.actorCanAccessMenu(menuItem, actor);
390
  checkbox.prop('checked', actorHasAccess);
391
 
392
  alternate = (alternate === '') ? 'alternate' : '';
393
 
394
  var cell = '<td>';
395
- var row = $('<tr>').data('actor', actor).attr('class', alternate).append(
396
  $(cell).addClass('ws_column_access').append(checkbox),
397
  $(cell).addClass('ws_column_role post-title').append(
398
  $('<label>').attr('for', checkboxId).append(
399
- $('<span>').text(actors[actor])
400
  )
401
  ),
402
  $(cell).addClass('ws_column_selected_role_tip').append(
@@ -467,7 +469,7 @@ window.AmeItemAccessEditor = (function ($) {
467
 
468
  if (hasExtendedPermissions) {
469
  //Select either the currently selected actor, or just the first one.
470
- setSelectedActor(state.selectedActor || _.keys(actors)[0] || null);
471
 
472
  //The permission table must be at least as tall as the actor list or the selected row won't look right.
473
  var roleTable = $editor.find('.ws_role_table_body'),
1
+ /* globals AmeCapabilityManager, jQuery, AmeActors */
2
 
3
  window.AmeItemAccessEditor = (function ($) {
4
  'use strict';
16
  var _,
17
  api,
18
  isProVersion = false,
19
+ actorSelector,
20
  postTypes,
21
  taxonomies,
22
 
79
  return $(this).data('actor') === selectedActor;
80
  }).addClass('ws_cpt_selected_role');
81
 
82
+ $editor.find('.ws_aed_selected_actor_name').text(
83
+ actorSelector.getNiceName(AmeActors.getActor(selectedActor))
84
+ );
85
  }
86
 
87
  if (hasExtendedPermissions) {
325
  /**
326
  * @param {AmeEditorApi} config.api
327
  * @param {Object} config.actors
328
+ * @param {AmeActorSelector} config.actorSelector
329
  * @param {Object} config.postTypes
330
  * @param {Object} config.taxonomies
331
  * @param {lodash} config.lodash
337
  setup: function(config) {
338
  _ = config.lodash;
339
  api = config.api;
340
+ actorSelector = config.actorSelector;
341
 
342
  postTypes = config.postTypes;
343
  taxonomies = config.taxonomies;
380
 
381
  //Generate the actor list.
382
  var table = $editor.find('.ws_role_table_body tbody').empty(),
383
+ alternate = '',
384
+ visibleActors = actorSelector.getVisibleActors();
385
+ for(var index = 0; index < visibleActors.length; index++) {
386
+ var actor = visibleActors[index];
 
387
 
388
+ var checkboxId = 'allow_' + actor.id.replace(/[^a-zA-Z0-9_]/g, '_');
389
  var checkbox = $('<input type="checkbox">').addClass('ws_role_access').attr('id', checkboxId);
390
 
391
+ var actorHasAccess = api.actorCanAccessMenu(menuItem, actor.id);
392
  checkbox.prop('checked', actorHasAccess);
393
 
394
  alternate = (alternate === '') ? 'alternate' : '';
395
 
396
  var cell = '<td>';
397
+ var row = $('<tr>').data('actor', actor.id).attr('class', alternate).append(
398
  $(cell).addClass('ws_column_access').append(checkbox),
399
  $(cell).addClass('ws_column_role post-title').append(
400
  $('<label>').attr('for', checkboxId).append(
401
+ $('<span>').text(actorSelector.getNiceName(actor))
402
  )
403
  ),
404
  $(cell).addClass('ws_column_selected_role_tip').append(
469
 
470
  if (hasExtendedPermissions) {
471
  //Select either the currently selected actor, or just the first one.
472
+ setSelectedActor(state.selectedActor || (visibleActors[0].id) || null);
473
 
474
  //The permission table must be at least as tall as the actor list or the selected row won't look right.
475
  var roleTable = $editor.find('.ws_role_table_body'),
modules/actor-selector/actor-selector.js CHANGED
@@ -119,16 +119,7 @@ var AmeActorSelector = (function () {
119
  actorSelector.append('<li><a href="#" class="current ws_actor_option ws_no_actor" data-text="All">All</a></li>');
120
  var visibleActors = this.getVisibleActors();
121
  for (var i = 0; i < visibleActors.length; i++) {
122
- var actor = visibleActors[i];
123
- var name_1 = actor.displayName;
124
- if (actor instanceof AmeUser) {
125
- if (actor.userLogin === this.currentUserLogin) {
126
- name_1 = 'Current user (' + actor.userLogin + ')';
127
- }
128
- else {
129
- name_1 = actor.displayName + ' (' + actor.userLogin + ')';
130
- }
131
- }
132
  actorSelector.append($('<li></li>').append($('<a></a>')
133
  .attr('href', '#' + actor.id)
134
  .attr('data-text', name_1)
@@ -190,6 +181,21 @@ var AmeActorSelector = (function () {
190
  'visible_users': jQuery.toJSON(this.visibleUsers)
191
  });
192
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  AmeActorSelector._ = wsAmeLodash;
194
  return AmeActorSelector;
195
  }());
119
  actorSelector.append('<li><a href="#" class="current ws_actor_option ws_no_actor" data-text="All">All</a></li>');
120
  var visibleActors = this.getVisibleActors();
121
  for (var i = 0; i < visibleActors.length; i++) {
122
+ var actor = visibleActors[i], name_1 = this.getNiceName(actor);
 
 
 
 
 
 
 
 
 
123
  actorSelector.append($('<li></li>').append($('<a></a>')
124
  .attr('href', '#' + actor.id)
125
  .attr('data-text', name_1)
181
  'visible_users': jQuery.toJSON(this.visibleUsers)
182
  });
183
  };
184
+ AmeActorSelector.prototype.getCurrentUserActor = function () {
185
+ return this.actorManager.getUser(this.currentUserLogin);
186
+ };
187
+ AmeActorSelector.prototype.getNiceName = function (actor) {
188
+ var name = actor.displayName;
189
+ if (actor instanceof AmeUser) {
190
+ if (actor.userLogin === this.currentUserLogin) {
191
+ name = 'Current user (' + actor.userLogin + ')';
192
+ }
193
+ else {
194
+ name = actor.displayName + ' (' + actor.userLogin + ')';
195
+ }
196
+ }
197
+ return name;
198
+ };
199
  AmeActorSelector._ = wsAmeLodash;
200
  return AmeActorSelector;
201
  }());
modules/actor-selector/actor-selector.ts CHANGED
@@ -165,16 +165,8 @@ class AmeActorSelector {
165
 
166
  var visibleActors = this.getVisibleActors();
167
  for (let i = 0; i < visibleActors.length; i++) {
168
- const actor = visibleActors[i];
169
-
170
- let name = actor.displayName;
171
- if (actor instanceof AmeUser) {
172
- if (actor.userLogin === this.currentUserLogin) {
173
- name = 'Current user (' + actor.userLogin + ')';
174
- } else {
175
- name = actor.displayName + ' (' + actor.userLogin + ')';
176
- }
177
- }
178
 
179
  actorSelector.append(
180
  $('<li></li>').append(
@@ -257,4 +249,20 @@ class AmeActorSelector {
257
  }
258
  );
259
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  }
165
 
166
  var visibleActors = this.getVisibleActors();
167
  for (let i = 0; i < visibleActors.length; i++) {
168
+ const actor = visibleActors[i],
169
+ name = this.getNiceName(actor);
 
 
 
 
 
 
 
 
170
 
171
  actorSelector.append(
172
  $('<li></li>').append(
249
  }
250
  );
251
  }
252
+
253
+ getCurrentUserActor(): AmeUser {
254
+ return this.actorManager.getUser(this.currentUserLogin);
255
+ }
256
+
257
+ getNiceName(actor: AmeBaseActor): string {
258
+ let name = actor.displayName;
259
+ if (actor instanceof AmeUser) {
260
+ if (actor.userLogin === this.currentUserLogin) {
261
+ name = 'Current user (' + actor.userLogin + ')';
262
+ } else {
263
+ name = actor.displayName + ' (' + actor.userLogin + ')';
264
+ }
265
+ }
266
+ return name;
267
+ }
268
  }
modules/plugin-visibility/plugin-visibility-template.php ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php do_action('admin_menu_editor-display_header'); ?>
2
+
3
+ <div id="ame-plugin-visibility-editor">
4
+ <form method="post" data-bind="submit: saveChanges" class="ame-pv-save-form" action="<?php
5
+ echo esc_attr(add_query_arg(
6
+ array(
7
+ 'page' => 'menu_editor',
8
+ 'noheader' => '1',
9
+ 'sub_section' => amePluginVisibility::TAB_SLUG,
10
+ ),
11
+ admin_url('options-general.php')
12
+ ));
13
+ ?>">
14
+
15
+ <?php submit_button('Save Changes', 'primary', 'submit', false); ?>
16
+
17
+ <input type="hidden" name="action" value="save_plugin_visibility">
18
+ <?php wp_nonce_field('save_plugin_visibility'); ?>
19
+
20
+ <input type="hidden" name="settings" value="" data-bind="value: settingsData">
21
+ <input type="hidden" name="selected_actor" value="" data-bind="value: selectedActor">
22
+ </form>
23
+
24
+ <?php require AME_ROOT_DIR . '/modules/actor-selector/actor-selector-template.php'; ?>
25
+
26
+ <table class="widefat plugins">
27
+ <thead>
28
+ <tr>
29
+ <th scope="col" class="ame-check-column">
30
+ <!--suppress HtmlFormInputWithoutLabel -->
31
+ <input type="checkbox" data-bind="checked: areAllPluginsChecked">
32
+ </th>
33
+ <th scope="col">Plugin</th>
34
+ <th scope="col">Description</th>
35
+ </tr>
36
+ </thead>
37
+
38
+ <tbody data-bind="foreach: plugins">
39
+ <tr
40
+ data-bind="
41
+ css: {
42
+ 'active': isActive,
43
+ 'inactive': !isActive
44
+ }
45
+ ">
46
+
47
+ <!--
48
+ Alas, we can't use the "check-column" class for this checkbox because WP would apply
49
+ the default "check all boxes" behaviour and override our Knockout bindings.
50
+ -->
51
+ <th scope="row" class="ame-check-column">
52
+ <!--suppress HtmlFormInputWithoutLabel -->
53
+ <input
54
+ type="checkbox"
55
+ data-bind="
56
+ checked: isChecked,
57
+ attr: {
58
+ id: 'ame-plugin-visible-' + $index(),
59
+ 'data-plugin-file': fileName
60
+ }">
61
+ </th>
62
+
63
+ <td class="plugin-title">
64
+ <label data-bind="attr: { 'for': 'ame-plugin-visible-' + $index() }">
65
+ <strong data-bind="text: name"></strong>
66
+ </label>
67
+ </td>
68
+
69
+ <td><p data-bind="text: description"></p></td>
70
+ </tr>
71
+ </tbody>
72
+
73
+ <tfoot>
74
+ <tr>
75
+ <th scope="col" class="ame-check-column">
76
+ <!--suppress HtmlFormInputWithoutLabel -->
77
+ <input type="checkbox" data-bind="checked: areAllPluginsChecked">
78
+ </th>
79
+ <th scope="col">Plugin</th>
80
+ <th scope="col">Description</th>
81
+ </tr>
82
+ </tfoot>
83
+
84
+ </table>
85
+
86
+ </div> <!-- /module container -->
87
+
88
+ <?php do_action('admin_menu_editor-display_footer'); ?>
modules/plugin-visibility/plugin-visibility.css ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ Plugin visibility module
3
+ ------------------------
4
+ */
5
+ .plugins thead .ame-check-column,
6
+ .plugins tfoot .ame-check-column {
7
+ padding: 4px 0 0 6px;
8
+ vertical-align: middle;
9
+ width: 2.2em; }
10
+
11
+ .plugins .ame-check-column {
12
+ vertical-align: top; }
13
+
14
+ /*
15
+ Plugin status indicator on the check column
16
+ */
17
+ .plugins .active th.ame-check-column,
18
+ .plugin-update-tr.active td {
19
+ border-left: 4px solid #2ea2cc; }
20
+
21
+ .plugins thead th.ame-check-column,
22
+ .plugins tfoot th.ame-check-column,
23
+ .plugins .inactive th.ame-check-column {
24
+ padding-left: 6px; }
25
+
26
+ .plugins tbody th.ame-check-column,
27
+ .plugins tbody {
28
+ padding: 8px 0 0 2px; }
29
+
30
+ .plugins tbody th.ame-check-column input[type=checkbox] {
31
+ margin-top: 4px; }
32
+
33
+ /*
34
+ The "Save Changes" form
35
+ */
36
+ .ame-pv-save-form {
37
+ float: right;
38
+ margin-top: 5px;
39
+ margin-bottom: 6px; }
40
+
41
+ #ws_actor_selector_container {
42
+ margin-right: 130px; }
43
+
44
+ /*# sourceMappingURL=plugin-visibility.css.map */
modules/plugin-visibility/plugin-visibility.js ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /// <reference path="../../js/knockout.d.ts" />
2
+ /// <reference path="../../js/jquery.d.ts" />
3
+ /// <reference path="../../js/jqueryui.d.ts" />
4
+ /// <reference path="../../js/lodash-3.10.d.ts" />
5
+ /// <reference path="../../modules/actor-selector/actor-selector.ts" />
6
+ var AmePluginVisibilityModule = (function () {
7
+ function AmePluginVisibilityModule(scriptData) {
8
+ var _this = this;
9
+ var _ = AmePluginVisibilityModule._;
10
+ this.actorSelector = new AmeActorSelector(AmeActors, scriptData.isProVersion);
11
+ //Wrap the selected actor in a computed observable so that it can be used with Knockout.
12
+ var _selectedActor = ko.observable(this.actorSelector.selectedActor);
13
+ this.selectedActor = ko.computed({
14
+ read: function () {
15
+ return _selectedActor();
16
+ },
17
+ write: function (newActor) {
18
+ _this.actorSelector.setSelectedActor(newActor);
19
+ }
20
+ });
21
+ this.actorSelector.onChange(function (newSelectedActor) {
22
+ _selectedActor(newSelectedActor);
23
+ });
24
+ //Re-select the previously selected actor, or select "All" (null) by default.
25
+ this.selectedActor(scriptData.selectedActor);
26
+ this.canRoleManagePlugins = scriptData.canManagePlugins;
27
+ this.isMultisite = scriptData.isMultisite;
28
+ this.grantAccessByDefault = {};
29
+ _.forEach(this.actorSelector.getVisibleActors(), function (actor) {
30
+ _this.grantAccessByDefault[actor.id] = ko.observable(_.get(scriptData.settings.grantAccessByDefault, actor.id, _this.canManagePlugins(actor)));
31
+ });
32
+ this.plugins = _.map(scriptData.installedPlugins, function (plugin) {
33
+ return new AmePlugin(plugin, _.get(scriptData.settings.plugins, plugin.fileName, {}), _this);
34
+ });
35
+ this.privilegedActors = [this.actorSelector.getCurrentUserActor()];
36
+ if (this.isMultisite) {
37
+ this.privilegedActors.push(AmeActors.getSuperAdmin());
38
+ }
39
+ this.areAllPluginsChecked = ko.computed({
40
+ read: function () {
41
+ return _.every(_this.plugins, function (plugin) {
42
+ return _this.isPluginVisible(plugin);
43
+ });
44
+ },
45
+ write: function (isChecked) {
46
+ if (_this.selectedActor() !== null) {
47
+ var canSeePluginsByDefault = _this.getGrantAccessByDefault(_this.selectedActor());
48
+ canSeePluginsByDefault(isChecked);
49
+ }
50
+ _.forEach(_this.plugins, function (plugin) {
51
+ _this.setPluginVisibility(plugin, isChecked);
52
+ });
53
+ }
54
+ });
55
+ //This observable will be populated when saving changes.
56
+ this.settingsData = ko.observable('');
57
+ }
58
+ AmePluginVisibilityModule.prototype.isPluginVisible = function (plugin) {
59
+ var actorId = this.selectedActor();
60
+ if (actorId === null) {
61
+ return plugin.isVisibleByDefault();
62
+ }
63
+ else {
64
+ var canSeePluginsByDefault = this.getGrantAccessByDefault(actorId), isVisible = plugin.getGrantObservable(actorId, plugin.isVisibleByDefault() && canSeePluginsByDefault());
65
+ return isVisible();
66
+ }
67
+ };
68
+ AmePluginVisibilityModule.prototype.setPluginVisibility = function (plugin, isVisible) {
69
+ var _this = this;
70
+ var selectedActor = this.selectedActor();
71
+ if (selectedActor === null) {
72
+ plugin.isVisibleByDefault(isVisible);
73
+ //Show/hide from everyone except the current user and Super Admin.
74
+ //However, don't enable plugins for roles that can't access the "Plugins" page in the first place.
75
+ var _1 = AmePluginVisibilityModule._;
76
+ _1.forEach(this.actorSelector.getVisibleActors(), function (actor) {
77
+ var allowAccess = plugin.getGrantObservable(actor.id, isVisible);
78
+ if (!_this.canManagePlugins(actor)) {
79
+ allowAccess(false);
80
+ }
81
+ else if (_1.includes(_this.privilegedActors, actor)) {
82
+ allowAccess(true);
83
+ }
84
+ else {
85
+ allowAccess(isVisible);
86
+ }
87
+ });
88
+ }
89
+ else {
90
+ //Show/hide from the selected role or user.
91
+ var allowAccess = plugin.getGrantObservable(selectedActor, isVisible);
92
+ allowAccess(isVisible);
93
+ }
94
+ };
95
+ AmePluginVisibilityModule.prototype.canManagePlugins = function (actor) {
96
+ var _this = this;
97
+ var _ = AmePluginVisibilityModule._;
98
+ if ((actor instanceof AmeRole) && _.has(this.canRoleManagePlugins, actor.name)) {
99
+ return this.canRoleManagePlugins[actor.name];
100
+ }
101
+ if (actor instanceof AmeSuperAdmin) {
102
+ return true;
103
+ }
104
+ if (actor instanceof AmeUser) {
105
+ //Can any of the user's roles manage plugins?
106
+ var result_1 = false;
107
+ _.forEach(actor.roles, function (roleId) {
108
+ if (_.get(_this.canRoleManagePlugins, roleId, false)) {
109
+ result_1 = true;
110
+ return false;
111
+ }
112
+ });
113
+ return (result_1 || AmeActors.hasCap(actor.id, 'activate_plugins'));
114
+ }
115
+ return false;
116
+ };
117
+ AmePluginVisibilityModule.prototype.getGrantAccessByDefault = function (actorId) {
118
+ if (!this.grantAccessByDefault.hasOwnProperty(actorId)) {
119
+ this.grantAccessByDefault[actorId] = ko.observable(this.canManagePlugins(AmeActors.getActor(actorId)));
120
+ }
121
+ return this.grantAccessByDefault[actorId];
122
+ };
123
+ AmePluginVisibilityModule.prototype.getSettings = function () {
124
+ var _ = AmePluginVisibilityModule._;
125
+ var result = {};
126
+ result.grantAccessByDefault = _.mapValues(this.grantAccessByDefault, function (allow) {
127
+ return allow();
128
+ });
129
+ result.plugins = {};
130
+ _.forEach(this.plugins, function (plugin) {
131
+ result.plugins[plugin.fileName] = {
132
+ isVisibleByDefault: plugin.isVisibleByDefault(),
133
+ grantAccess: _.mapValues(plugin.grantAccess, function (allow) {
134
+ return allow();
135
+ })
136
+ };
137
+ });
138
+ return result;
139
+ };
140
+ AmePluginVisibilityModule.prototype.saveChanges = function () {
141
+ var settings = this.getSettings();
142
+ //Remove settings associated with roles and users that no longer exist or are not visible.
143
+ var _ = AmePluginVisibilityModule._, visibleActorIds = _.pluck(this.actorSelector.getVisibleActors(), 'id');
144
+ _.forEach(settings.plugins, function (plugin) {
145
+ plugin.grantAccess = _.pick(plugin.grantAccess, visibleActorIds);
146
+ });
147
+ //Populate form field(s).
148
+ this.settingsData(jQuery.toJSON(settings));
149
+ return true;
150
+ };
151
+ AmePluginVisibilityModule._ = wsAmeLodash;
152
+ return AmePluginVisibilityModule;
153
+ }());
154
+ var AmePlugin = (function () {
155
+ function AmePlugin(details, visibility, module) {
156
+ var _this = this;
157
+ this.name = AmePlugin.stripAllTags(details.name);
158
+ this.description = AmePlugin.stripAllTags(details.description);
159
+ this.fileName = details.fileName;
160
+ this.isActive = details.isActive;
161
+ var _ = AmePluginVisibilityModule._;
162
+ this.isVisibleByDefault = ko.observable(_.get(visibility, 'isVisibleByDefault', true));
163
+ var emptyGrant = {};
164
+ this.grantAccess = _.mapValues(_.get(visibility, 'grantAccess', emptyGrant), function (hasAccess) {
165
+ return ko.observable(hasAccess);
166
+ });
167
+ this.isChecked = ko.computed({
168
+ read: function () {
169
+ return module.isPluginVisible(_this);
170
+ },
171
+ write: function (isVisible) {
172
+ return module.setPluginVisibility(_this, isVisible);
173
+ }
174
+ });
175
+ }
176
+ AmePlugin.prototype.getGrantObservable = function (actorId, defaultValue) {
177
+ if (defaultValue === void 0) { defaultValue = true; }
178
+ if (!this.grantAccess.hasOwnProperty(actorId)) {
179
+ this.grantAccess[actorId] = ko.observable(defaultValue);
180
+ }
181
+ return this.grantAccess[actorId];
182
+ };
183
+ AmePlugin.stripAllTags = function (input) {
184
+ //Based on: http://phpjs.org/functions/strip_tags/
185
+ var tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi, commentsAndPhpTags = /<!--[\s\S]*?-->|<\?(?:php)?[\s\S]*?\?>/gi;
186
+ return input.replace(commentsAndPhpTags, '').replace(tags, '');
187
+ };
188
+ return AmePlugin;
189
+ }());
190
+ jQuery(function ($) {
191
+ amePluginVisibility = new AmePluginVisibilityModule(wsPluginVisibilityData);
192
+ ko.applyBindings(amePluginVisibility, document.getElementById('ame-plugin-visibility-editor'));
193
+ //Permanently dismiss the usage hint via AJAX.
194
+ $('#ame-pv-usage-notice').on('click', '.notice-dismiss', function () {
195
+ $.post(wsPluginVisibilityData.adminAjaxUrl, {
196
+ 'action': 'ws_ame_dismiss_pv_usage_notice',
197
+ '_ajax_nonce': wsPluginVisibilityData.dismissNoticeNonce
198
+ });
199
+ });
200
+ });
201
+ //# sourceMappingURL=plugin-visibility.js.map
modules/plugin-visibility/plugin-visibility.php ADDED
@@ -0,0 +1,435 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ class amePluginVisibility {
3
+ const OPTION_NAME = 'ws_ame_plugin_visibility';
4
+ const TAB_SLUG = 'plugin-visibility';
5
+
6
+ const HIDE_USAGE_NOTICE_FLAG = 'ws_ame_hide_pv_notice';
7
+
8
+ private static $lastInstance = null;
9
+
10
+ /**
11
+ * @var WPMenuEditor
12
+ */
13
+ private $menuEditor;
14
+ private $settings = array();
15
+
16
+ public function __construct($menuEditor) {
17
+ $this->menuEditor = $menuEditor;
18
+ self::$lastInstance = $this;
19
+
20
+ //Remove "hidden" plugins from the list on the "Plugins -> Installed Plugins" page.
21
+ add_filter('all_plugins', array($this, 'filterPluginList'));
22
+
23
+ //It's not possible to completely prevent a user from (de)activating "hidden" plugins because plugin API
24
+ //functions like activate_plugin() and deactivate_plugins() don't provide a way to abort (de)activation.
25
+ //However, we can still block edits and *some* other actions that WP verifies with check_admin_referer().
26
+ add_action('check_admin_referer', array($this, 'authorizePluginAction'));
27
+
28
+ //Register the plugin visibility tab.
29
+ add_action('admin_menu_editor-tabs', array($this, 'addSettingsTab'), 20);
30
+ add_action('admin_menu_editor-section-' . self::TAB_SLUG, array($this, 'displayUi'));
31
+ add_action('admin_menu_editor-header', array($this, 'handleFormSubmission'), 10, 2);
32
+
33
+ //Enqueue scripts and styles.
34
+ add_action('admin_menu_editor-enqueue_scripts-' . self::TAB_SLUG, array($this, 'enqueueScripts'));
35
+ add_action('admin_menu_editor-enqueue_styles-' . self::TAB_SLUG, array($this, 'enqueueStyles'));
36
+
37
+ //Display a usage hint in our tab.
38
+ add_action('admin_notices', array($this, 'displayUsageNotice'));
39
+ $dismissNoticeAction = new ameAjaxAction('ws_ame_dismiss_pv_usage_notice');
40
+ $dismissNoticeAction
41
+ ->setAuthCallback(array($this->menuEditor, 'current_user_can_edit_menu'))
42
+ ->setHandler(array($this, 'ajaxDismissUsageNotice'));
43
+ }
44
+
45
+ public function getSettings() {
46
+ if (!empty($this->settings)) {
47
+ return $this->settings;
48
+ }
49
+
50
+ if ( $this->menuEditor->get_plugin_option('menu_config_scope') === 'site' ) {
51
+ $json = get_option(self::OPTION_NAME, null);
52
+ } else {
53
+ $json = get_site_option(self::OPTION_NAME, null);
54
+ }
55
+
56
+ if ( is_string($json) ) {
57
+ $settings = json_decode($json, true);
58
+ } else {
59
+ $settings = array();
60
+ }
61
+
62
+ $this->settings = array_merge(
63
+ array(
64
+ 'plugins' => array(),
65
+ 'grantAccessByDefault' => array(),
66
+ ),
67
+ $settings
68
+ );
69
+
70
+ return $this->settings;
71
+ }
72
+
73
+ private function saveSettings() {
74
+ //Save per site or site-wide based on plugin configuration.
75
+ $settings = json_encode($this->settings);
76
+ if ($this->menuEditor->get_plugin_option('menu_config_scope') === 'site') {
77
+ update_option(self::OPTION_NAME, $settings);
78
+ } else {
79
+ WPMenuEditor::atomic_update_site_option(self::OPTION_NAME, $settings);
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Check if a plugin is visible to the current user.
85
+ *
86
+ * Goals:
87
+ * - You can easily hide a plugin from everyone, including new roles. See: isVisibleByDefault
88
+ * - You can configure a role so that new plugins are hidden by default. See: grantAccessByDefault
89
+ * - You can change visibility per role and per user, just like with admin menus.
90
+ * - Roles that don't have access to plugins are not considered when deciding visibility.
91
+ * - Precedence order: user > super admin > all roles.
92
+ *
93
+ * @param string $pluginFileName Plugin file name as returned by plugin_basename().
94
+ * @param WP_User $user Current user.
95
+ * @return bool
96
+ */
97
+ private function isPluginVisible($pluginFileName, $user = null) {
98
+ //TODO: Can we refactor this to be shorter?
99
+ static $isMultisite = null;
100
+ if (!isset($isMultisite)) {
101
+ $isMultisite = is_multisite();
102
+ }
103
+
104
+ if ($user === null) {
105
+ $user = wp_get_current_user();
106
+ }
107
+ $settings = $this->getSettings();
108
+
109
+ //Do we have custom settings for this plugin?
110
+ if (isset($settings['plugins'][$pluginFileName])) {
111
+ $isVisibleByDefault = $settings['plugins'][$pluginFileName]['isVisibleByDefault'];
112
+ $grantAccess = $settings['plugins'][$pluginFileName]['grantAccess'];
113
+
114
+ if ($isVisibleByDefault) {
115
+ $grantAccess = array_merge($settings['grantAccessByDefault'], $grantAccess);
116
+ }
117
+ } else {
118
+ $isVisibleByDefault = true;
119
+ $grantAccess = $settings['grantAccessByDefault'];
120
+ }
121
+
122
+ //User settings take precedence over everything else.
123
+ $userActor = 'user:' . $user->get('user_login');
124
+ if (isset($grantAccess[$userActor])) {
125
+ return $grantAccess[$userActor];
126
+ }
127
+
128
+ //Super Admin is next.
129
+ if ($isMultisite && is_super_admin($user->ID)) {
130
+ //By default the Super Admin has access to everything.
131
+ return ameUtils::get($grantAccess, 'special:super_admin', true);
132
+ }
133
+
134
+ //Finally, the user can see the plugin if at least one of their roles can.
135
+ $roles = $this->menuEditor->get_user_roles($user);
136
+ foreach ($roles as $roleId) {
137
+ if (ameUtils::get($grantAccess, 'role:' . $roleId, $isVisibleByDefault && $this->canManagePlugins($roleId))) {
138
+ return true;
139
+ }
140
+ }
141
+
142
+ return false;
143
+ }
144
+
145
+
146
+ /**
147
+ * @param string $roleId
148
+ * @param WP_Role $role
149
+ * @return bool
150
+ */
151
+ private function canManagePlugins($roleId, $role = null) {
152
+ static $cache = array();
153
+
154
+ if (isset($cache[$roleId])) {
155
+ return $cache[$roleId];
156
+ }
157
+
158
+ //Any role that has any of the following capabilities has some degree of control over plugins,
159
+ //so plugin visibility settings apply to that role.
160
+ $pluginCaps = array(
161
+ 'activate_plugins', 'install_plugins', 'edit_plugins', 'update_plugins', 'delete_plugins',
162
+ 'manage_network_plugins',
163
+ );
164
+
165
+ if (!isset($role)) {
166
+ $role = get_role($roleId);
167
+ }
168
+
169
+ $result = false;
170
+ foreach ($pluginCaps as $cap) {
171
+ if ($role->has_cap($cap)) {
172
+ $result = true;
173
+ break;
174
+ }
175
+ }
176
+
177
+ $cache[$roleId] = $result;
178
+
179
+ return $result;
180
+ }
181
+
182
+ /**
183
+ * Filter a plugin list by removing plugins that are not visible to the current user.
184
+ *
185
+ * @param array $plugins
186
+ * @return array
187
+ */
188
+ public function filterPluginList($plugins) {
189
+ $user = wp_get_current_user();
190
+
191
+ //Remove all hidden plugins.
192
+ $pluginFileNames = array_keys($plugins);
193
+ foreach($pluginFileNames as $fileName) {
194
+ if ( !$this->isPluginVisible($fileName, $user) ) {
195
+ unset($plugins[$fileName]);
196
+ }
197
+ }
198
+
199
+ return $plugins;
200
+ }
201
+
202
+ /**
203
+ * Verify that the current user is allowed to see the plugin that they're trying to edit, activate or deactivate.
204
+ * Note that this doesn't catch bulk (de-)activation or various plugin management plugins.
205
+ *
206
+ * This is a callback for the "check_admin_referer" action.
207
+ * @param string $action
208
+ */
209
+ public function authorizePluginAction($action) {
210
+ //Is the user trying to edit a plugin?
211
+ if (preg_match('@^edit-plugin_(?P<file>.+)$@', $action, $matches)) {
212
+
213
+ //The file that's being edited is part of a plugin. Find that plugin.
214
+ $fileName = wp_normalize_path($matches['file']);
215
+ $fileDirectory = ameUtils::getFirstDirectory($fileName);
216
+ $selectedPlugin = null;
217
+
218
+ $pluginFiles = array_keys(get_plugins());
219
+ foreach ($pluginFiles as $pluginFile) {
220
+ //Is the user editing the main plugin file?
221
+ if ($pluginFile === $fileName) {
222
+ $selectedPlugin = $pluginFile;
223
+ break;
224
+ }
225
+
226
+ //Is the file inside this plugin's directory?
227
+ $pluginDirectory = ameUtils::getFirstDirectory($pluginFile);
228
+ if (($pluginDirectory !== null) && ($pluginDirectory === $fileDirectory)) {
229
+ $selectedPlugin = $pluginFile;
230
+ break;
231
+ }
232
+ }
233
+
234
+ if ($selectedPlugin !== null) {
235
+ //Can the current user see the selected plugin?
236
+ $isVisible = $this->isPluginVisible($selectedPlugin);
237
+
238
+ if (!$isVisible) {
239
+ wp_die('You do not have sufficient permissions to edit this plugin.');
240
+ }
241
+ }
242
+
243
+ //Is the user trying to (de-)activate a single plugin?
244
+ } elseif (preg_match('@(?P<action>deactivate|activate)-plugin_(?P<plugin>.+)$@', $action, $matches)) {
245
+ //Can the current user see this plugin?
246
+ $isVisible = $this->isPluginVisible($matches['plugin']);
247
+
248
+ if (!$isVisible) {
249
+ wp_die(sprintf(
250
+ 'You do not have sufficient permissions to %s this plugin.',
251
+ $matches['action']
252
+ ));
253
+ }
254
+
255
+ //Are they acting on multiple plugins? One of them might be hidden.
256
+ } elseif (($action === 'bulk-plugins') && isset($_POST['checked']) && is_array($_POST['checked'])) {
257
+
258
+ $user = wp_get_current_user();
259
+ foreach ($_POST['checked'] as $pluginFile) {
260
+ if (!$this->isPluginVisible(strval($pluginFile), $user)) {
261
+ wp_die(sprintf(
262
+ 'You do not have sufficient permissions to manage this plugin: "%s".',
263
+ $pluginFile
264
+ ));
265
+ }
266
+ }
267
+ }
268
+ }
269
+
270
+ public function addSettingsTab($tabs) {
271
+ $tabs[self::TAB_SLUG] = 'Plugins';
272
+ return $tabs;
273
+ }
274
+
275
+ public function displayUi() {
276
+ require dirname(__FILE__) . '/plugin-visibility-template.php';
277
+ }
278
+
279
+ public function handleFormSubmission($action, $post = array()) {
280
+ //Note: We don't need to check user permissions here because plugin core already did.
281
+ if ( $action === 'save_plugin_visibility' ) {
282
+ check_admin_referer($action);
283
+
284
+ $this->settings = json_decode($post['settings'], true);
285
+ $this->saveSettings();
286
+
287
+ $params = array('updated' => 1);
288
+
289
+ //Re-select the same actor.
290
+ if ( !empty($post['selected_actor']) ) {
291
+ $params['selected_actor'] = strval($post['selected_actor']);
292
+ }
293
+
294
+ wp_redirect($this->getTabUrl($params));
295
+ exit;
296
+ }
297
+ }
298
+
299
+ private function getTabUrl($queryParameters = array()) {
300
+ $queryParameters = array_merge(
301
+ array(
302
+ 'page' => 'menu_editor',
303
+ 'sub_section' => self::TAB_SLUG
304
+ ),
305
+ $queryParameters
306
+ );
307
+ return add_query_arg($queryParameters, admin_url('options-general.php'));
308
+ }
309
+
310
+ public function enqueueScripts() {
311
+ wp_register_auto_versioned_script(
312
+ 'knockout',
313
+ plugins_url('js/knockout.js', $this->menuEditor->plugin_file)
314
+ );
315
+
316
+ wp_register_auto_versioned_script(
317
+ 'ame-plugin-visibility',
318
+ plugins_url('plugin-visibility.js', __FILE__),
319
+ array('ame-lodash', 'knockout', 'ame-actor-selector', 'jquery-json',)
320
+ );
321
+ wp_enqueue_script('ame-plugin-visibility');
322
+
323
+ //Reselect the same actor.
324
+ $query = $this->menuEditor->get_query_params();
325
+ $selectedActor = null;
326
+ if ( isset($query['selected_actor']) ) {
327
+ $selectedActor = strval($query['selected_actor']);
328
+ }
329
+
330
+ $scriptData = $this->getScriptData();
331
+ $scriptData['selectedActor'] = $selectedActor;
332
+ wp_localize_script('ame-plugin-visibility', 'wsPluginVisibilityData', $scriptData);
333
+ }
334
+
335
+ public function getScriptData(){
336
+ //Pass the list of installed plugins and their state (active/inactive) to UI JavaScript.
337
+ $installedPlugins = get_plugins();
338
+
339
+ $activePlugins = array_map('plugin_basename', wp_get_active_and_valid_plugins());
340
+ $activeNetworkPlugins = array();
341
+ if (function_exists('wp_get_active_network_plugins')) {
342
+ //This function is only available on Multisite.
343
+ $activeNetworkPlugins = array_map('plugin_basename', wp_get_active_network_plugins());
344
+ }
345
+
346
+ $plugins = array();
347
+ foreach($installedPlugins as $pluginFile => $header) {
348
+ $isActiveForNetwork = in_array($pluginFile, $activeNetworkPlugins);
349
+ $isActive = in_array($pluginFile, $activePlugins);
350
+
351
+ $plugins[] = array(
352
+ 'fileName' => $pluginFile,
353
+ 'name' => $header['Name'],
354
+ 'description' => isset($header['Description']) ? $header['Description'] : '',
355
+ 'isActive' => $isActive || $isActiveForNetwork,
356
+ );
357
+ }
358
+
359
+ //Flag roles that can manage plugins.
360
+ $canManagePlugins = array();
361
+ $wpRoles = ameRoleUtils::get_roles();
362
+ foreach($wpRoles->role_objects as $id => $role) {
363
+ $canManagePlugins[$id] = $this->canManagePlugins($id, $role);
364
+ }
365
+
366
+ return array(
367
+ 'settings' => $this->getSettings(),
368
+ 'installedPlugins' => $plugins,
369
+ 'canManagePlugins' => $canManagePlugins,
370
+ 'isMultisite' => is_multisite(),
371
+ 'isProVersion' => $this->menuEditor->is_pro_version(),
372
+
373
+ 'dismissNoticeNonce' => wp_create_nonce('ws_ame_dismiss_pv_usage_notice'),
374
+ 'adminAjaxUrl' => admin_url('admin-ajax.php'),
375
+ );
376
+ }
377
+
378
+ public function enqueueStyles() {
379
+ wp_enqueue_auto_versioned_style(
380
+ 'ame-plugin-visibility-css',
381
+ plugins_url('plugin-visibility.css', __FILE__)
382
+ );
383
+ }
384
+
385
+ public function displayUsageNotice() {
386
+ if ( !$this->menuEditor->is_tab_open(self::TAB_SLUG) ) {
387
+ return;
388
+ }
389
+
390
+ //If the user has already made some changes, they probably don't need to see this notice any more.
391
+ $settings = $this->getSettings();
392
+ if ( !empty($settings['plugins']) ) {
393
+ return;
394
+ }
395
+
396
+ //The notice is dismissible.
397
+ if ( get_site_option(self::HIDE_USAGE_NOTICE_FLAG, false) ) {
398
+ return;
399
+ }
400
+
401
+ echo '<div class="notice notice-info is-dismissible" id="ame-pv-usage-notice">
402
+ <p>
403
+ <strong>Tip:</strong> This screen lets you hide plugins from other users.
404
+ These settings only affect the "Plugins" page, not the admin menu or the dashboard.
405
+ </p>
406
+ </div>';
407
+ }
408
+
409
+ public function ajaxDismissUsageNotice() {
410
+ $result = update_site_option(self::HIDE_USAGE_NOTICE_FLAG, true);
411
+ return array('success' => true, 'updateResult' => $result);
412
+ }
413
+
414
+ /**
415
+ * Get the most recently created instance of this class.
416
+ * Note: This function should only be used for testing purposes.
417
+ *
418
+ * @return amePluginVisibility|null
419
+ */
420
+ public static function getLastCreatedInstance() {
421
+ return self::$lastInstance;
422
+ }
423
+
424
+ /**
425
+ * Remove any visibility settings associated with the specified plugin.
426
+ *
427
+ * @param string $pluginFile
428
+ */
429
+ public function forgetPlugin($pluginFile) {
430
+ $settings = $this->getSettings();
431
+ unset($settings['plugins'][$pluginFile]);
432
+ $this->settings = $settings;
433
+ $this->saveSettings();
434
+ }
435
+ }
modules/plugin-visibility/plugin-visibility.scss ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ Plugin visibility module
3
+ ------------------------
4
+ */
5
+
6
+ .plugins thead .ame-check-column,
7
+ .plugins tfoot .ame-check-column {
8
+ padding: 4px 0 0 6px;
9
+ vertical-align: middle;
10
+ width: 2.2em;
11
+ }
12
+
13
+ .plugins .ame-check-column {
14
+ vertical-align: top;
15
+ }
16
+
17
+ /*
18
+ Plugin status indicator on the check column
19
+ */
20
+ .plugins .active th.ame-check-column,
21
+ .plugin-update-tr.active td {
22
+ border-left: 4px solid #2ea2cc;
23
+ }
24
+
25
+ .plugins thead th.ame-check-column,
26
+ .plugins tfoot th.ame-check-column,
27
+ .plugins .inactive th.ame-check-column {
28
+ padding-left: 6px;
29
+ }
30
+
31
+ .plugins tbody th.ame-check-column,
32
+ .plugins tbody {
33
+ padding: 8px 0 0 2px;
34
+ }
35
+
36
+ .plugins tbody th.ame-check-column input[type=checkbox] {
37
+ margin-top: 4px;
38
+ }
39
+
40
+ /*
41
+ The "Save Changes" form
42
+ */
43
+
44
+ .ame-pv-save-form {
45
+ float: right;
46
+ margin-top: 5px;
47
+ margin-bottom: 6px;
48
+ }
49
+
50
+ //Make room for th save button.
51
+ #ws_actor_selector_container {
52
+ margin-right: 130px;
53
+ }
modules/plugin-visibility/plugin-visibility.ts ADDED
@@ -0,0 +1,293 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /// <reference path="../../js/knockout.d.ts" />
2
+ /// <reference path="../../js/jquery.d.ts" />
3
+ /// <reference path="../../js/jqueryui.d.ts" />
4
+ /// <reference path="../../js/lodash-3.10.d.ts" />
5
+ /// <reference path="../../modules/actor-selector/actor-selector.ts" />
6
+
7
+ declare var amePluginVisibility: AmePluginVisibilityModule;
8
+ declare var wsPluginVisibilityData: PluginVisibilityScriptData;
9
+
10
+ interface PluginVisibilityScriptData {
11
+ isMultisite: boolean,
12
+ canManagePlugins: {[roleId : string] : boolean},
13
+ selectedActor: string,
14
+ installedPlugins: Array<PvPluginInfo>,
15
+ settings: PluginVisibilitySettings,
16
+ isProVersion: boolean,
17
+
18
+ adminAjaxUrl: string,
19
+ dismissNoticeNonce: string
20
+ }
21
+
22
+ interface PluginVisibilitySettings {
23
+ grantAccessByDefault: GrantAccessMap,
24
+ plugins: {
25
+ [fileName : string] : {
26
+ isVisibleByDefault: boolean,
27
+ grantAccess: GrantAccessMap
28
+ }
29
+ }
30
+ }
31
+
32
+ interface GrantAccessMap {
33
+ [actorId : string] : boolean
34
+ }
35
+
36
+ interface PvPluginInfo {
37
+ name: string,
38
+ fileName: string,
39
+ description: string,
40
+ isActive: boolean
41
+ }
42
+
43
+ class AmePluginVisibilityModule {
44
+ static _ = wsAmeLodash;
45
+
46
+ plugins: Array<AmePlugin>;
47
+ private canRoleManagePlugins: {[roleId: string] : boolean};
48
+ grantAccessByDefault: {[actorId: string] : KnockoutObservable<boolean>};
49
+ private isMultisite: boolean;
50
+
51
+ actorSelector: AmeActorSelector;
52
+ selectedActor: KnockoutComputed<string>;
53
+ settingsData: KnockoutObservable<string>;
54
+
55
+ areAllPluginsChecked: KnockoutComputed<boolean>;
56
+
57
+ /**
58
+ * Actors that don't lose access to a plugin when you uncheck it in the "All" view.
59
+ * This is a convenience feature that lets the user quickly hide a bunch of plugins from everyone else.
60
+ */
61
+ private privilegedActors: Array<AmeBaseActor>;
62
+
63
+ constructor(scriptData: PluginVisibilityScriptData) {
64
+ const _ = AmePluginVisibilityModule._;
65
+
66
+ this.actorSelector = new AmeActorSelector(AmeActors, scriptData.isProVersion);
67
+
68
+ //Wrap the selected actor in a computed observable so that it can be used with Knockout.
69
+ var _selectedActor = ko.observable(this.actorSelector.selectedActor);
70
+ this.selectedActor = ko.computed<string>({
71
+ read: function () {
72
+ return _selectedActor();
73
+ },
74
+ write: (newActor: string) => {
75
+ this.actorSelector.setSelectedActor(newActor);
76
+ }
77
+ });
78
+ this.actorSelector.onChange((newSelectedActor: string) => {
79
+ _selectedActor(newSelectedActor);
80
+ });
81
+
82
+ //Re-select the previously selected actor, or select "All" (null) by default.
83
+ this.selectedActor(scriptData.selectedActor);
84
+
85
+ this.canRoleManagePlugins = scriptData.canManagePlugins;
86
+ this.isMultisite = scriptData.isMultisite;
87
+
88
+ this.grantAccessByDefault = {};
89
+ _.forEach(this.actorSelector.getVisibleActors(), (actor: AmeBaseActor) => {
90
+ this.grantAccessByDefault[actor.id] = ko.observable<boolean>(
91
+ _.get(scriptData.settings.grantAccessByDefault, actor.id, this.canManagePlugins(actor))
92
+ );
93
+ });
94
+
95
+ this.plugins = _.map(scriptData.installedPlugins, (plugin) => {
96
+ return new AmePlugin(plugin, _.get(scriptData.settings.plugins, plugin.fileName, {}), this);
97
+ });
98
+
99
+ this.privilegedActors = [this.actorSelector.getCurrentUserActor()];
100
+ if (this.isMultisite) {
101
+ this.privilegedActors.push(AmeActors.getSuperAdmin());
102
+ }
103
+
104
+ this.areAllPluginsChecked = ko.computed({
105
+ read: () => {
106
+ return _.every(this.plugins, (plugin) => {
107
+ return this.isPluginVisible(plugin);
108
+ });
109
+ },
110
+ write: (isChecked) => {
111
+ if (this.selectedActor() !== null) {
112
+ let canSeePluginsByDefault = this.getGrantAccessByDefault(this.selectedActor());
113
+ canSeePluginsByDefault(isChecked);
114
+ }
115
+ _.forEach(this.plugins, (plugin) => {
116
+ this.setPluginVisibility(plugin, isChecked);
117
+ });
118
+ }
119
+ });
120
+
121
+ //This observable will be populated when saving changes.
122
+ this.settingsData = ko.observable('');
123
+ }
124
+
125
+ isPluginVisible(plugin: AmePlugin): boolean {
126
+ let actorId = this.selectedActor();
127
+ if (actorId === null) {
128
+ return plugin.isVisibleByDefault();
129
+ } else {
130
+ let canSeePluginsByDefault = this.getGrantAccessByDefault(actorId),
131
+ isVisible = plugin.getGrantObservable(actorId, plugin.isVisibleByDefault() && canSeePluginsByDefault());
132
+ return isVisible();
133
+ }
134
+ }
135
+
136
+ setPluginVisibility(plugin: AmePlugin, isVisible: boolean) {
137
+ const selectedActor = this.selectedActor();
138
+ if (selectedActor === null) {
139
+ plugin.isVisibleByDefault(isVisible);
140
+
141
+ //Show/hide from everyone except the current user and Super Admin.
142
+ //However, don't enable plugins for roles that can't access the "Plugins" page in the first place.
143
+ const _ = AmePluginVisibilityModule._;
144
+ _.forEach(this.actorSelector.getVisibleActors(), (actor: AmeBaseActor) => {
145
+ let allowAccess = plugin.getGrantObservable(actor.id, isVisible);
146
+ if (!this.canManagePlugins(actor)) {
147
+ allowAccess(false);
148
+ } else if (_.includes(this.privilegedActors, actor)) {
149
+ allowAccess(true);
150
+ } else {
151
+ allowAccess(isVisible);
152
+ }
153
+ });
154
+ } else {
155
+ //Show/hide from the selected role or user.
156
+ let allowAccess = plugin.getGrantObservable(selectedActor, isVisible);
157
+ allowAccess(isVisible);
158
+ }
159
+ }
160
+
161
+ private canManagePlugins(actor: AmeBaseActor) {
162
+ const _ = AmePluginVisibilityModule._;
163
+ if ((actor instanceof AmeRole) && _.has(this.canRoleManagePlugins, actor.name)) {
164
+ return this.canRoleManagePlugins[actor.name];
165
+ }
166
+ if (actor instanceof AmeSuperAdmin) {
167
+ return true;
168
+ }
169
+
170
+ if (actor instanceof AmeUser) {
171
+ //Can any of the user's roles manage plugins?
172
+ let result = false;
173
+ _.forEach(actor.roles, (roleId) => {
174
+ if (_.get(this.canRoleManagePlugins, roleId, false)) {
175
+ result = true;
176
+ return false;
177
+ }
178
+ });
179
+ return (result || AmeActors.hasCap(actor.id, 'activate_plugins'));
180
+ }
181
+
182
+ return false;
183
+ }
184
+
185
+ private getGrantAccessByDefault(actorId: string): KnockoutObservable<boolean> {
186
+ if (!this.grantAccessByDefault.hasOwnProperty(actorId)) {
187
+ this.grantAccessByDefault[actorId] = ko.observable(this.canManagePlugins(AmeActors.getActor(actorId)));
188
+ }
189
+ return this.grantAccessByDefault[actorId];
190
+ }
191
+
192
+ private getSettings(): PluginVisibilitySettings {
193
+ const _ = AmePluginVisibilityModule._;
194
+ let result: PluginVisibilitySettings = <PluginVisibilitySettings>{};
195
+
196
+ result.grantAccessByDefault = _.mapValues(this.grantAccessByDefault, (allow): boolean => {
197
+ return allow();
198
+ });
199
+ result.plugins = {};
200
+ _.forEach(this.plugins, (plugin: AmePlugin) => {
201
+ result.plugins[plugin.fileName] = {
202
+ isVisibleByDefault: plugin.isVisibleByDefault(),
203
+ grantAccess: _.mapValues(plugin.grantAccess, (allow): boolean => {
204
+ return allow();
205
+ })
206
+ };
207
+ });
208
+
209
+ return result;
210
+ }
211
+
212
+ saveChanges() {
213
+ const settings = this.getSettings();
214
+
215
+ //Remove settings associated with roles and users that no longer exist or are not visible.
216
+ const _ = AmePluginVisibilityModule._,
217
+ visibleActorIds = _.pluck(this.actorSelector.getVisibleActors(), 'id');
218
+ _.forEach(settings.plugins, (plugin) => {
219
+ plugin.grantAccess = _.pick<GrantAccessMap, GrantAccessMap>(plugin.grantAccess, visibleActorIds);
220
+ });
221
+
222
+ //Populate form field(s).
223
+ this.settingsData(jQuery.toJSON(settings));
224
+
225
+ return true;
226
+ }
227
+ }
228
+
229
+ class AmePlugin implements PvPluginInfo {
230
+ name: string;
231
+ fileName: string;
232
+ description: string;
233
+ isActive: boolean;
234
+
235
+ isChecked: KnockoutComputed<boolean>;
236
+
237
+ isVisibleByDefault: KnockoutObservable<boolean>;
238
+ grantAccess: {[actorId : string] : KnockoutObservable<boolean>};
239
+
240
+ constructor(details: PvPluginInfo, visibility: Object, module: AmePluginVisibilityModule) {
241
+ this.name = AmePlugin.stripAllTags(details.name);
242
+ this.description = AmePlugin.stripAllTags(details.description);
243
+ this.fileName = details.fileName;
244
+ this.isActive = details.isActive;
245
+
246
+ const _ = AmePluginVisibilityModule._;
247
+ this.isVisibleByDefault = ko.observable(_.get(visibility, 'isVisibleByDefault', true));
248
+
249
+ const emptyGrant: {[actorId : string] : boolean} = {};
250
+ this.grantAccess = _.mapValues(_.get(visibility, 'grantAccess', emptyGrant), (hasAccess) => {
251
+ return ko.observable<boolean>(hasAccess);
252
+ });
253
+
254
+ this.isChecked = ko.computed<boolean>({
255
+ read: () => {
256
+ return module.isPluginVisible(this);
257
+ },
258
+ write: (isVisible: boolean) => {
259
+ return module.setPluginVisibility(this, isVisible);
260
+ }
261
+ });
262
+ }
263
+
264
+ getGrantObservable(actorId: string, defaultValue: boolean = true): KnockoutObservable<boolean> {
265
+ if (!this.grantAccess.hasOwnProperty(actorId)) {
266
+ this.grantAccess[actorId] = ko.observable<boolean>(defaultValue);
267
+ }
268
+ return this.grantAccess[actorId];
269
+ }
270
+
271
+ static stripAllTags(input): string {
272
+ //Based on: http://phpjs.org/functions/strip_tags/
273
+ var tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi,
274
+ commentsAndPhpTags = /<!--[\s\S]*?-->|<\?(?:php)?[\s\S]*?\?>/gi;
275
+ return input.replace(commentsAndPhpTags, '').replace(tags, '');
276
+ }
277
+ }
278
+
279
+ jQuery(function ($) {
280
+ amePluginVisibility = new AmePluginVisibilityModule(wsPluginVisibilityData);
281
+ ko.applyBindings(amePluginVisibility, document.getElementById('ame-plugin-visibility-editor'));
282
+
283
+ //Permanently dismiss the usage hint via AJAX.
284
+ $('#ame-pv-usage-notice').on('click', '.notice-dismiss', function() {
285
+ $.post(
286
+ wsPluginVisibilityData.adminAjaxUrl,
287
+ {
288
+ 'action' : 'ws_ame_dismiss_pv_usage_notice',
289
+ '_ajax_nonce' : wsPluginVisibilityData.dismissNoticeNonce
290
+ }
291
+ );
292
+ });
293
+ });
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: 4.5.2
7
- Stable tag: 1.6.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,6 +63,10 @@ Plugins installed in the `mu-plugins` directory are treated as "always on", so y
63
 
64
  == Changelog ==
65
 
 
 
 
 
66
  = 1.6.2 =
67
  * Fixed a bug that made menu items "jump" slightly to the left when you start to drag them.
68
  * Fixed a Multisite-specific bug where temporarily switching to another site using the switch_to_blog() function could result in the user having the wrong permissions.
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: 4.6-beta3
7
+ Stable tag: 1.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.7 =
67
+ * Added a "Plugins" tab. It lets you hide specific plugins from other users. Note that this only affects the list on the "Plugins" page and tasks like editing plugin files, but it doesn't affect the admin menu.
68
+ * Tested up to WordPress 4.6-beta3.
69
+
70
  = 1.6.2 =
71
  * Fixed a bug that made menu items "jump" slightly to the left when you start to drag them.
72
  * Fixed a Multisite-specific bug where temporarily switching to another site using the switch_to_blog() function could result in the user having the wrong permissions.
uninstall.php CHANGED
@@ -19,4 +19,13 @@ if( defined( 'ABSPATH') && defined('WP_UNINSTALL_PLUGIN') ) {
19
  if ( function_exists('delete_metadata') ) {
20
  delete_metadata('user', 0, 'ame_show_hints', '', true);
21
  }
 
 
 
 
 
 
 
 
 
22
  }
19
  if ( function_exists('delete_metadata') ) {
20
  delete_metadata('user', 0, 'ame_show_hints', '', true);
21
  }
22
+
23
+ //Remove module settings.
24
+ delete_option('ws_ame_plugin_visibility');
25
+ delete_option('ws_ame_dashboard_widgets');
26
+ if ( function_exists('delete_site_option') ){
27
+ delete_site_option('ws_ame_plugin_visibility');
28
+ delete_site_option('ws_ame_hide_pv_notice');
29
+ delete_site_option('ws_ame_dashboard_widgets');
30
+ }
31
  }