array(),
'grantAccessByDefault' => array(),
);
private static $lastInstance = null;
/**
* @var Ajaw_v1_Action
*/
private $dismissNoticeAction;
public function __construct($menuEditor) {
parent::__construct($menuEditor);
self::$lastInstance = $this;
if ( !$this->isEnabledForRequest() ) {
return;
}
//Remove "hidden" plugins from the list on the "Plugins -> Installed Plugins" page.
add_filter('all_plugins', array($this, 'filterPluginList'), 15);
//Hide updates for hidden plugins.
add_filter('site_transient_update_plugins', array($this, 'filterPluginUpdates'), 15);
//It's not possible to completely prevent a user from (de)activating "hidden" plugins because plugin API
//functions like activate_plugin() and deactivate_plugins() don't provide a way to abort (de)activation.
//However, we can still block edits and *some* other actions that WP verifies with check_admin_referer().
add_action('check_admin_referer', array($this, 'authorizePluginAction'));
//Also block disallowed AJAX plugin edits by using the "editable_extensions" filter
//to remove all file extensions from the list for hidden plugins.
//See functions called by wp_ajax_edit_theme_plugin_file().
add_filter('editable_extensions', array($this, 'authorizePluginFileEdit'), 15, 2);
//Register the plugin visibility tab.
add_action('admin_menu_editor-header', array($this, 'handleFormSubmission'), 10, 2);
//Display a usage hint in our tab.
add_action('admin_notices', array($this, 'displayUsageNotice'));
$this->dismissNoticeAction = ajaw_v1_CreateAction('ws_ame_dismiss_pv_usage_notice')
->handler(array($this, 'ajaxDismissUsageNotice'))
->permissionCallback(array($this->menuEditor, 'current_user_can_edit_menu'))
->method('post')
->register();
}
/**
* Check if a plugin is visible to the current user.
*
* Goals:
* - You can easily hide a plugin from everyone, including new roles. See: isVisibleByDefault
* - You can configure a role so that new plugins are hidden by default. See: grantAccessByDefault
* - You can change visibility per role and per user, just like with admin menus.
* - Roles that don't have access to plugins are not considered when deciding visibility.
* - Precedence order: user > super admin > all roles.
*
* @param string $pluginFileName Plugin file name as returned by plugin_basename().
* @param WP_User $user Current user.
* @return bool
*/
private function isPluginVisible($pluginFileName, $user = null) {
//TODO: Can we refactor this to be shorter?
static $isMultisite = null;
if ( !isset($isMultisite) ) {
$isMultisite = is_multisite();
}
if ( $user === null ) {
$user = wp_get_current_user();
}
$settings = $this->loadSettings();
//Do we have custom settings for this plugin?
if ( isset($settings['plugins'][$pluginFileName]) ) {
$isVisibleByDefault = ameUtils::get($settings['plugins'][$pluginFileName], 'isVisibleByDefault', true);
$grantAccess = ameUtils::get($settings['plugins'][$pluginFileName], 'grantAccess', array());
if ( $isVisibleByDefault ) {
$grantAccess = array_merge($settings['grantAccessByDefault'], $grantAccess);
}
} else {
$isVisibleByDefault = true;
$grantAccess = $settings['grantAccessByDefault'];
}
//User settings take precedence over everything else.
$userActor = 'user:' . $user->get('user_login');
if ( isset($grantAccess[$userActor]) ) {
return $grantAccess[$userActor];
}
//Super Admin is next.
if ( $isMultisite && is_super_admin($user->ID) ) {
//By default, the Super Admin has access to everything.
return ameUtils::get($grantAccess, 'special:super_admin', true);
}
//Finally, the user can see the plugin if at least one of their roles can.
$anyRoleHasSettings = false;
$roles = $this->menuEditor->get_user_roles($user);
foreach ($roles as $roleId) {
/** @noinspection PhpRedundantOptionalArgumentInspection -- In case the default changes. */
$hasAccess = ameUtils::get($grantAccess, 'role:' . $roleId, null);
if ( $hasAccess !== null ) {
$anyRoleHasSettings = true;
} else {
$hasAccess = $isVisibleByDefault && $this->roleCanManagePlugins($roleId);
}
if ( $hasAccess ) {
return true;
}
}
if ( $anyRoleHasSettings ) {
//At least one role had per-plugin settings or access-by-default settings,
//and those settings did not grant access.
return false;
} else if ( $isVisibleByDefault ) {
//Check user capabilities.
return $this->userCanManagePlugins($user);
}
return false;
}
/**
* @param string $roleId
* @param WP_Role $role
* @return bool
*/
private function roleCanManagePlugins($roleId, $role = null) {
static $cache = array();
if ( isset($cache[$roleId]) ) {
return $cache[$roleId];
}
if ( !isset($role) ) {
$role = get_role($roleId);
if ( !isset($role) ) {
//This should never happen, but a user reported that it did on their site.
$cache[$roleId] = false;
return false;
}
}
$result = false;
foreach (self::PLUGIN_MANAGEMENT_CAPS as $cap) {
if ( $role->has_cap($cap) ) {
$result = true;
break;
}
}
$cache[$roleId] = $result;
return $result;
}
/**
* @param \WP_User $user
* @return boolean
*/
private function userCanManagePlugins($user) {
static $cache = array();
$userId = $user->ID;
if ( isset($cache[$userId]) ) {
return $cache[$userId];
}
$result = false;
foreach (self::PLUGIN_MANAGEMENT_CAPS as $cap) {
if ( user_can($user, $cap) ) {
$result = true;
break;
}
}
$cache[$userId] = $result;
return $result;
}
/**
* Filter a plugin list by removing plugins that are not visible to the current user.
*
* @param array $plugins
* @return array
*/
public function filterPluginList($plugins) {
if ( !is_array($plugins) && !($plugins instanceof ArrayAccess) ) {
return $plugins;
}
$user = wp_get_current_user();
$settings = $this->loadSettings();
//Don't try to hide plugins outside the WP admin. It prevents WP-CLI from seeing all installed plugins.
if ( !$user->exists() || !is_admin() ) {
return $plugins;
}
$editableProperties = array(
'Name' => 'name',
'Description' => 'description',
'Author' => 'author',
'PluginURI' => 'siteUrl',
'AuthorURI' => 'siteUrl',
'Version' => 'version',
);
$pluginFileNames = array_keys($plugins);
foreach ($pluginFileNames as $fileName) {
//Remove all hidden plugins.
if ( !$this->isPluginVisible($fileName, $user) ) {
unset($plugins[$fileName]);
continue;
}
//Set custom names, descriptions, and other properties.
foreach ($editableProperties as $header => $property) {
$customValue = ameUtils::get($settings, array('plugins', $fileName, 'custom' . ucfirst($property)), '');
if ( $customValue !== '' ) {
$plugins[$fileName][$header] = $customValue;
}
}
}
return $plugins;
}
/**
* Filter out updates associated with plugins that are not visible to the current user.
*
* @param StdClass|null $updates
* @return StdClass|null
*/
public function filterPluginUpdates($updates) {
if ( !isset($updates->response) || !is_array($updates->response) ) {
//Either there are no updates or we don't recognize the format.
return $updates;
}
//Let's not hide anything when no one is logged in. We don't check is_admin() here
//because plugin updates can appear in the Toolbar and that's visible in the front-end.
$user = wp_get_current_user();
if ( !$user->exists() || (defined('DOING_CRON') && DOING_CRON) ) {
return $updates;
}
$pluginFileNames = array_keys($updates->response);
foreach ($pluginFileNames as $fileName) {
//Remove all hidden plugins.
if ( !$this->isPluginVisible($fileName, $user) ) {
unset($updates->response[$fileName]);
continue;
}
}
return $updates;
}
/**
* Verify that the current user is allowed to see the plugin that they're trying to edit, activate or deactivate.
* Note that this doesn't catch bulk (de-)activation or various plugin management plugins.
*
* This is a callback for the "check_admin_referer" action.
*
* @param string $action
*/
public function authorizePluginAction($action) {
//PHPCS special case: This hook callback runs inside a function that validates
//nonces and selectively overrides the behaviour of that function.
//phpcs:disable WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- See above
//Is the user trying to edit a plugin?
if ( preg_match('@^edit-plugin_(?P
Tip: This screen lets you hide plugins from other users.
These settings only affect the "Plugins" page, not the admin menu or the dashboard.