, nodes: array}> */ private const APPEARANCE = [ 'themes' => ['slugs' => ['themes.php'], 'nodes' => ['themes']], 'design' => ['slugs' => ['site-editor.php'], 'nodes' => ['site-editor', 'edit-site']], 'customizer' => ['slugs' => ['customize.php'], 'nodes' => ['customize']], 'fonts' => ['slugs' => ['font-library.php'], 'nodes' => []], 'menus' => ['slugs' => ['nav-menus.php'], 'nodes' => ['menus']], 'widgets' => ['slugs' => ['widgets.php'], 'nodes' => ['widgets']], 'patterns' => ['slugs' => ['site-editor.php?p=/pattern', 'site-editor.php?p=%2Fpattern', 'edit.php?post_type=wp_block'], 'nodes' => []], ]; /** @var array */ private array $appearanceOff = []; private Settings $settings; public function register(Settings $settings): void { $this->settings = $settings; if ($settings->get('cleanup.help_tabs', false)) { add_action('admin_head', [$this, 'removeHelpTabs']); } if ($settings->get('cleanup.screen_options', false)) { add_filter('screen_options_show_screen', '__return_false'); } if ($settings->get('cleanup.update_nags', false)) { add_action('admin_notices', [$this, 'removeUpdateNags'], 1); add_action('network_admin_notices', [$this, 'removeUpdateNags'], 1); } if ($settings->get('cleanup.admin_email_check', true)) { add_filter('admin_email_check_interval', '__return_zero'); } foreach (array_keys(self::APPEARANCE) as $item) { if (! $settings->get('cleanup.appearance.' . $item, true)) { $this->appearanceOff[] = $item; } } if ($this->appearanceOff !== []) { add_action('admin_menu', [$this, 'removeAppearanceItems'], 999); add_action('admin_bar_menu', [$this, 'removeAppearanceNodes'], 999); add_action('admin_init', [$this, 'blockAppearanceScreens']); } if (in_array('customizer', $this->appearanceOff, true)) { add_filter('customize_loaded_components', '__return_empty_array'); } // Late enough for a theme's functions.php to filter it. add_action('after_setup_theme', function (): void { /** * Filters whether FeatherDash turns automatic updates off (core, * plugins, themes and translations). Default: the Admin clean-up * setting (always on in agency mode). * * @param bool $disable */ $disable = Licensing::agency() || (bool) $this->settings->get('cleanup.auto_updates_off', false); if (apply_filters('featherdash/disable_auto_updates', $disable)) { $this->disableAutoUpdates(); } }); } public function removeHelpTabs(): void { if (! function_exists('get_current_screen')) { return; } get_current_screen()?->remove_help_tabs(); } public function removeUpdateNags(): void { if (current_user_can('update_core')) { return; } remove_action('admin_notices', 'update_nag', 3); remove_action('admin_notices', 'maintenance_nag', 10); remove_action('network_admin_notices', 'update_nag', 3); remove_action('network_admin_notices', 'maintenance_nag', 10); } /** * Removes the turned-off sub-items from Appearance (and Patterns from * wherever a plugin or theme put it). */ public function removeAppearanceItems(): void { global $submenu; foreach ((array) $submenu as $parent => $items) { foreach ((array) $items as $index => $item) { $slug = isset($item[2]) && is_string($item[2]) ? $item[2] : ''; if ($slug !== '' && $this->appearanceItemOf($slug) !== null && ($parent === 'themes.php' || $this->appearanceItemOf($slug) === 'patterns')) { unset($submenu[$parent][$index]); } } } } public function removeAppearanceNodes(WP_Admin_Bar $bar): void { foreach ($this->appearanceOff as $item) { foreach (self::APPEARANCE[$item]['nodes'] as $node) { $bar->remove_node($node); } } } /** * Opening a turned-off screen directly redirects to the Dashboard. */ public function blockAppearanceScreens(): void { global $pagenow; if (wp_doing_ajax() || ! is_string($pagenow)) { return; } // Only matched against fixed screen slugs, never stored or printed. $query = isset($_SERVER['QUERY_STRING']) ? preg_replace('/[^\w%=&\/.\-+\[\]]/', '', (string) wp_unslash($_SERVER['QUERY_STRING'])) ?? '' : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $item = $this->appearanceItemOf($pagenow . ($query !== '' ? '?' . $query : '')); if ($item !== null) { wp_safe_redirect(admin_url()); exit; } } /** * Which turned-off Appearance item a menu slug or request belongs to. */ private function appearanceItemOf(string $slug): ?string { $isPatterns = str_starts_with($slug, 'edit.php?post_type=wp_block') || (str_starts_with($slug, 'site-editor.php') && preg_match('#[?&]p=(%2F|/)pattern#i', $slug) === 1); // The Patterns screen runs in the Site Editor but has its own toggle. if ($isPatterns) { return in_array('patterns', $this->appearanceOff, true) ? 'patterns' : null; } // Pages added with add_theme_page() (themes.php?page=…) belong to themes and plugins. if (preg_match('#[?&]page=#', $slug) === 1) { return null; } foreach ($this->appearanceOff as $item) { foreach (self::APPEARANCE[$item]['slugs'] as $prefix) { if ($item !== 'patterns' && ($slug === $prefix || str_starts_with($slug, $prefix . '?') || str_starts_with($slug, $prefix . '&'))) { return $item; } } } return null; } /** * Always off: sites are managed with Composer and deployed, so WordPress * must never update core, plugins, themes or translations by itself. * WordPress still checks for new versions (the footer flags them). */ private function disableAutoUpdates(): void { add_filter('automatic_updater_disabled', '__return_true'); add_filter('auto_update_core', '__return_false'); add_filter('auto_update_plugin', '__return_false'); add_filter('auto_update_theme', '__return_false'); add_filter('auto_update_translation', '__return_false'); // Hide the per-plugin/theme "Enable auto-updates" links, which would do nothing. add_filter('plugins_auto_update_enabled', '__return_false'); add_filter('themes_auto_update_enabled', '__return_false'); } }