| Server IP : 51.178.161.41 / Your IP : 216.73.216.125 Web Server : nginx/1.18.0 System : Linux vms33 5.10.0-36-amd64 #1 SMP Debian 5.10.244-1 (2025-09-29) x86_64 User : web18 ( 5018) PHP Version : 8.3.26 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : OFF | Perl : OFF | Python : OFF | Sudo : OFF | Pkexec : OFF Directory : /var/www/clients/client1/web18/web/wp-content/mu-plugins/ |
Upload File : |
<?php
/**
* Plugin Name: Developer Update Visibility (MU)
* Description: Hides all update notices and update actions (plugins, themes, languages, core) for everyone except a configured allowlist of usernames.
* Author: Lite Solutions
* Version: 1.2.0
* Must-Use: true
*/
declare(strict_types=1);
namespace LiteSolutions\Mu\UpdateVisibility;
// ------------------------------------------------------------------------
// Final class: DeveloperUpdateVisibility
// Provides per-user control to hide update UI and deny update capabilities.
// The allowlist of usernames can be configured via wp-config.php using the
// constant DEVELOPER_UPDATE_ALLOWED_USERS. If not defined, defaults to ["developer"].
// All hooks are registered after 'plugins_loaded' to ensure core functions exist.
// ------------------------------------------------------------------------
final class DeveloperUpdateVisibility {
/**
* @var array<int, string> DEFAULT_ALLOWED
* Default allowlist when wp-config.php does not define DEVELOPER_UPDATE_ALLOWED_USERS.
*/
private const DEFAULT_ALLOWED = ['developer'];
/**
* @var array<int, string> UPDATE_CAPS
* List of capability keys related to updates and installations that should
* be disabled (unset) for non-allowed users.
*/
private const UPDATE_CAPS = [
'update_plugins',
'update_themes',
'update_core',
'update_languages',
'install_plugins',
'install_themes',
'delete_plugins',
'delete_themes',
];
/**
* register
*
* Registers all necessary hooks after WordPress is loaded enough so that
* pluggable functions (like is_user_logged_in) are available. No work is done
* if running in WP-CLI context.
*
* @return void
*/
public static function register(): void {
// Skip entirely in WP-CLI to avoid interfering with automated tasks.
if (\defined('WP_CLI') && \WP_CLI) {
return;
}
// Capabilities are filtered always; early return inside the callback if allowed.
\add_filter('user_has_cap', [self::class, 'filterUserCaps'], 20, 3);
// Admin-only visual/UI restrictions.
\add_action('admin_init', [self::class, 'removeUpdateNag'], 1);
\add_action('admin_head', [self::class, 'printAdminCssToHideUpdates'], 999);
\add_action('admin_bar_menu', [self::class, 'removeAdminBarUpdates'], 999);
\add_action('admin_menu', [self::class, 'cleanupAdminMenus'], 999);
\add_action('network_admin_menu', [self::class, 'cleanupAdminMenus'], 999);
\add_action('current_screen', [self::class, 'guardUpdateCoreScreen'], 1);
\add_action('load-update-core.php', [self::class, 'redirectAwayFromUpdates'], 1);
}
/**
* getAllowedUsernames
*
* Resolves the allowed usernames from wp-config.php. Accepts either:
* - An array constant (e.g., ['developer','alice'])
* - A comma-separated string (e.g., 'developer, alice')
* Fallback: ["developer"].
*
* @return array<int, string>
* Normalized, unique, lowercase usernames allowed to see updates.
*/
public static function getAllowedUsernames(): array {
/** @var mixed $raw */
$raw = \defined('DEVELOPER_UPDATE_ALLOWED_USERS') ? \constant('DEVELOPER_UPDATE_ALLOWED_USERS') : null;
$list = [];
// If wp-config provides an array, use it as-is.
if (\is_array($raw)) {
$list = $raw;
}
// If wp-config provides a string, split by commas.
elseif (\is_string($raw) && $raw !== '') {
$parts = \explode(',', $raw);
foreach ($parts as $part) {
$list[] = $part;
}
}
// Fallback to default.
if (empty($list)) {
$list = self::DEFAULT_ALLOWED;
}
// Normalize: trim, lowercase, remove empties and duplicates.
$normalized = [];
foreach ($list as $username) {
$u = \strtolower(\trim((string) $username));
if ($u !== '') {
$normalized[] = $u;
}
}
$normalized = \array_values(\array_unique($normalized));
/**
* Filter: developer_update_visibility/allowed_usernames
* Allows altering the final allowlist.
*
* @param array<int, string> $normalized
* The normalized allowlist.
*/
$normalized = (array) \apply_filters('developer_update_visibility/allowed_usernames', $normalized);
return $normalized;
}
/**
* isAllowedUser
*
* Determines whether the current user belongs to the privileged allowlist
* who can see and use update features.
*
* @return bool
* TRUE if current user is allowed; FALSE otherwise.
*/
public static function isAllowedUser(): bool {
// If no session or user context is available, deny by default.
if (!\function_exists('is_user_logged_in') || !\is_user_logged_in()) {
return false;
}
$user = \wp_get_current_user();
if (!$user || empty($user->user_login)) {
return false;
}
$allowed = self::getAllowedUsernames();
$login = \strtolower(\trim((string) $user->user_login));
return \in_array($login, $allowed, true);
}
/**
* filterUserCaps
*
* Unsets all update/install related capabilities for non-allowed users.
* This relies on WordPress' native capability checks so buttons and actions
* disappear without hacking core or altering transients.
*
* @param array<string, bool> $allCaps
* All primitive capabilities for the user.
* @param array<string> $caps
* Required caps for the meta capability being checked.
* @param array<string, mixed> $args
* Context of the capability check.
*
* @return array<string, bool>
* Filtered capabilities array.
*/
public static function filterUserCaps(array $allCaps, array $caps, array $args): array {
// If allowed, do not modify capabilities.
if (self::isAllowedUser() === true) {
return $allCaps;
}
foreach (self::UPDATE_CAPS as $cap) {
if (isset($allCaps[$cap])) {
unset($allCaps[$cap]);
}
}
return $allCaps;
}
/**
* removeUpdateNag
*
* Removes the classic core "update nag" notice and related hooks to reduce
* visual noise for non-allowed users.
*
* @return void
*/
public static function removeUpdateNag(): void {
if (self::isAllowedUser() === true) {
return;
}
// Old-style update nag in admin notices.
\remove_action('admin_notices', 'update_nag', 3);
\remove_action('network_admin_notices', 'update_nag', 3);
// Some hosts/plugins add their own nags about updates; attempt to silence common ones.
\remove_action('admin_notices', 'maintenance_nag'); // best effort
\remove_action('network_admin_notices', 'maintenance_nag'); // best effort
}
/**
* printAdminCssToHideUpdates
*
* Prints a tiny CSS snippet to hide visual update badges, inline update rows
* and UI elements that might still appear even when capabilities are removed.
* This is a non-invasive last layer for edge cases and 3rd-party UIs.
*
* @return void
*/
public static function printAdminCssToHideUpdates(): void {
if (self::isAllowedUser() === true) {
return;
}
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<style id="developer-update-visibility-css">
/* Hide red update bubbles on menu items and toolbar */
.update-plugins, .plugin-count, .theme-count, .awaiting-mod, #wp-admin-bar-updates, .notice.notice-warning.update-nag { display: none !important; }
/* Hide inline plugin/theme update rows and actions (best effort) */
.update, .plugin-update-tr, .theme-update, tr.plugin-update-tr, .update-message, .notice.notice-warning.inline.update-message { display: none !important; }
/* Hide update action links that some UIs render even without caps */
.row-actions .update, .row-actions .update-now, .row-actions .update-link { display: none !important; }
/* Hide "Update now" buttons/links in various contexts */
a.upgrade, a.update-link, .update-now, .button.upgrade, .button.update-now { display: none !important; }
</style>';
}
/**
* removeAdminBarUpdates
*
* Removes the Updates node from the admin bar for non-allowed users.
*
* @param \WP_Admin_Bar $wp_admin_bar
* The admin bar instance.
*
* @return void
*/
public static function removeAdminBarUpdates(\WP_Admin_Bar $wp_admin_bar): void {
if (self::isAllowedUser() === true) {
return;
}
$wp_admin_bar->remove_node('updates');
}
/**
* cleanupAdminMenus
*
* Removes the "Updates" page from the admin (and network admin) menus.
* This avoids users directly navigating through the UI to update-core.php.
*
* @return void
*/
public static function cleanupAdminMenus(): void {
if (self::isAllowedUser() === true) {
return;
}
// Main "Updates" screen.
\remove_submenu_page('index.php', 'update-core.php');
\remove_menu_page('update-core.php');
}
/**
* guardUpdateCoreScreen
*
* If a non-allowed user reaches the updates screen, redirect them away.
*
* @param \WP_Screen $screen
* Current admin screen object.
*
* @return void
*/
public static function guardUpdateCoreScreen(\WP_Screen $screen): void {
if (self::isAllowedUser() === true) {
return;
}
if (isset($screen->base) && $screen->base === 'update-core') {
self::safeRedirect(\admin_url());
}
}
/**
* redirectAwayFromUpdates
*
* Extra guard executed when update-core.php loads (direct access/bookmarks).
*
* @return void
*/
public static function redirectAwayFromUpdates(): void {
if (self::isAllowedUser() === true) {
return;
}
self::safeRedirect(\admin_url());
}
/**
* safeRedirect
*
* Performs a safe admin redirect and exits if headers can be sent.
*
* @param string $url
* Destination URL for the redirect.
*
* @return void
*/
private static function safeRedirect(string $url): void {
if (!\headers_sent()) {
\wp_safe_redirect($url);
exit;
}
}
}
// ------------------------------------------------------------------------
// Bootstrap
// Initialize after plugins are loaded to ensure pluggable functions exist.
// ------------------------------------------------------------------------
\add_action('plugins_loaded', [DeveloperUpdateVisibility::class, 'register'], 0);