Admin screen ID to module ID map. */ private static $admin_screens = []; /** @var array Admin post type to module ID map. */ private static $admin_post_types = []; /** Register an exact WordPress admin screen as belonging to a module. */ public static function register_admin_screen( string $module_id, string $screen_id ): void { if ( '' !== $module_id && '' !== $screen_id ) { self::$admin_screens[ $screen_id ] = $module_id; } } /** Register an admin post type as belonging to a module. */ public static function register_admin_post_type( string $module_id, string $post_type ): void { if ( '' !== $module_id && '' !== $post_type ) { self::$admin_post_types[ $post_type ] = $module_id; } } /** Determine whether an admin screen belongs to a module. */ public static function is_admin_screen_for_module( string $module_id, $screen ): bool { if ( ! is_object( $screen ) ) { return false; } $screen_id = isset( $screen->id ) ? (string) $screen->id : ''; if ( isset( self::$admin_screens[ $screen_id ] ) ) { return $module_id === self::$admin_screens[ $screen_id ]; } $post_type = isset( $screen->post_type ) ? (string) $screen->post_type : ''; return isset( self::$admin_post_types[ $post_type ] ) && $module_id === self::$admin_post_types[ $post_type ]; } /** Resolve a registered screen's canonical public module slug. */ public static function get_module_slug_for_screen( $screen ): ?string { $module_id = self::$admin_screens[ $screen->id ] ?? null; if ( null === $module_id && ! empty( $screen->post_type ) ) { $module_id = self::$admin_post_types[ $screen->post_type ] ?? null; } if ( null === $module_id ) { return null; } $module = self::get_module( $module_id ); return $module['slug'] ?? null; } /** * Get the full list of registered modules, keyed by module ID. * * @return array */ public static function get_modules(): array { if ( null === self::$modules ) { $modules = apply_filters( 'adminoptimizer_module_registry', self::get_raw_modules() ); self::$modules = self::normalize_modules( is_array( $modules ) ? $modules : [] ); } return self::$modules; } /** * Get a single module definition by ID. * * @param string $module_id Module ID. * * @return array|null */ public static function get_module( string $module_id ): ?array { $modules = self::get_modules(); $module = $modules[ $module_id ] ?? null; return apply_filters( 'adminoptimizer_module_registry_module', $module, $module_id ); } /** * Whether a module ID exists in the registry. * * @param string $module_id Module ID. * * @return bool */ public static function has_module( string $module_id ): bool { return isset( self::get_modules()[ $module_id ] ); } /** * Get all modules belonging to a given section. * * @param string $section Section slug. * * @return array */ public static function get_modules_by_section( string $section ): array { return array_filter( self::get_modules(), static function ( $module ) use ( $section ) { return $module['section'] === $section; } ); } /** * Get all modules belonging to a given tier. * * @param string $tier Tier value. * * @return array */ public static function get_modules_by_tier( string $tier ): array { return array_filter( self::get_modules(), static function ( $module ) use ( $tier ) { return $module['tier'] === $tier; } ); } /** * Get the tier of a module. * * @param string $module_id Module ID. * * @return string */ public static function get_tier( string $module_id ): string { $module = self::get_module( $module_id ); return $module['tier'] ?? self::DEFAULT_TIER; } /** * Whether a module is Pro-only (requires Pro to be active, regardless of edition). * * @param string $module_id Module ID. * * @return bool */ public static function is_pro_module( string $module_id ): bool { $tier = self::get_tier( $module_id ); return in_array( $tier, [ 'pro', 'pro_infrastructure' ], true ); } /** * Whether a module is available in the Free edition. * * @param string $module_id Module ID. * * @return bool */ public static function is_free_module( string $module_id ): bool { $tier = self::get_tier( $module_id ); return in_array( $tier, [ 'free', 'free_with_pro_options' ], true ); } /** * Whether a module can be used in the current edition (Free vs Pro active). * * @param string $module_id Module ID. * * @return bool */ public static function is_module_available( string $module_id ): bool { if ( ! self::has_module( $module_id ) ) { return false; } if ( self::is_free_module( $module_id ) ) { return true; } return defined( 'ADMINOPTIMIZER_PRO_ID' ); } /** * Get the toggle key (option array key) for a module, or null if it has none. * * @param string $module_id Module ID. * * @return string|null */ public static function get_toggle_key( string $module_id ): ?string { $module = self::get_module( $module_id ); return $module['toggle'] ?? null; } /** * Whether a string is a registered module's toggle key (the MODULES_OPTION array key). * * @param string $toggle_key Toggle key. * * @return bool */ public static function is_valid_toggle_key( string $toggle_key ): bool { foreach ( self::get_modules() as $module ) { if ( ( $module['toggle'] ?? null ) === $toggle_key ) { return true; } } return false; } /** * Get a module definition by its public-facing slug (e.g. 'seo-manager'). * * @param string $slug Module slug. * * @return array|null */ public static function get_module_by_slug( string $slug ): ?array { foreach ( self::get_modules() as $module ) { if ( ( $module['slug'] ?? null ) === $slug ) { return $module; } } return null; } /** * Resolve any known module reference (ID, slug, or toggle key) to its toggle key. * * @param string $module_reference Module ID, slug, or toggle key. * * @return string|null */ public static function resolve_toggle_key( string $module_reference ): ?string { if ( self::is_valid_toggle_key( $module_reference ) ) { return $module_reference; } $module = self::get_module( $module_reference ) ?? self::get_module_by_slug( $module_reference ); return $module['toggle'] ?? null; } /** * Check if a module is activated. * * @param string $module_name Module ID, slug, or toggle key. * * @return bool */ public static function is_module_activated( string $module_name ): bool { $toggle_key = self::resolve_toggle_key( sanitize_text_field( $module_name ) ); if ( empty( $toggle_key ) ) { return false; } $modules = get_option( MODULES_OPTION, [] ); return array_key_exists( $toggle_key, $modules ); } /** * Activate a particular module. * * @param string $module_name Module ID, slug, or toggle key (e.g. 'seo-manager' or 'enable_seo_manager'). * * @return true|\WP_Error True on success, or if the module was already activated. \WP_Error if the module name is invalid or activation fails. */ public static function activate_module( string $module_name ) { $toggle_key = self::resolve_toggle_key( sanitize_text_field( $module_name ) ); if ( empty( $toggle_key ) ) { return new \WP_Error( 'invalid_module', 'Invalid module name.' ); } if ( self::is_module_activated( $toggle_key ) ) { return true; } $modules = get_option( MODULES_OPTION, [] ); $modules[ $toggle_key ] = true; if ( ! update_option( MODULES_OPTION, $modules ) ) { return new \WP_Error( 'activation_failed', 'Failed to activate module.' ); } return true; } /** * Get the dedicated option name for a module, or null if it has none. * * @param string $module_id Module ID. * * @return string|null */ public static function get_option_name( string $module_id ): ?string { $module = self::get_module( $module_id ); return $module['option'] ?? null; } /** * Raw (un-normalized) module catalog. * * @return array */ private static function get_raw_modules(): array { return [ // Performance. 'db_cleaner' => [ 'id' => 'db_cleaner', 'name' => __( 'Database Cleaner', 'admin-optimizer' ), 'slug' => 'db-cleaner', 'section' => 'performance', 'tier' => 'free_with_pro_options', 'toggle' => 'enable_db_cleaner', 'option' => 'adminoptim_db_cleaner', 'category' => 'performance', 'description' => __( 'Schedule regular optimization and cleaning up of the WP database to improve the performance of the site.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/database-cleaner/', 'supports_ai' => false, ], 'heartbeat_control' => [ 'id' => 'heartbeat_control', 'name' => __( 'Heartbeat Control', 'admin-optimizer' ), 'slug' => 'heartbeat-control', 'section' => 'performance', 'tier' => 'free', 'toggle' => 'enable_heartbeat_control', 'option' => 'adminoptim_heartbeat_control', 'category' => 'performance', 'description' => __( 'Modify the interval of the WordPress heartbeat API to reduce CPU load on the server.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/heartbeat-control/', 'supports_ai' => false, ], 'disable_emojis' => [ 'id' => 'disable_emojis', 'name' => __( 'Disable Emojis', 'admin-optimizer' ), 'slug' => 'disable-emojis', 'section' => 'performance', 'tier' => 'free', 'toggle' => 'disable_emojis', 'option' => null, 'category' => 'performance', 'description' => __( 'Disable emoji support and remove emoji scripts and styles from your site.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/disable-emojis/', 'supports_ai' => false, ], 'disable_jquery_migrate' => [ 'id' => 'disable_jquery_migrate', 'name' => __( 'Disable jQuery Migrate', 'admin-optimizer' ), 'slug' => 'disable-jquery-migrate', 'section' => 'performance', 'tier' => 'free', 'toggle' => 'disable_jquery_migrate', 'option' => null, 'category' => 'performance', 'description' => __( 'Remove script bloat from your site.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/disable-jquery-migrate/', 'supports_ai' => false, ], 'disable_404_redirect' => [ 'id' => 'disable_404_redirect', 'name' => __( 'Disable 404 URL Redirect', 'admin-optimizer' ), 'slug' => 'disable-404-redirect', 'section' => 'performance', 'tier' => 'free', 'toggle' => 'disable_404_redirect', 'option' => null, 'category' => 'performance', 'description' => __( 'Disable the guessing of a redirect URL for a 404 request and return the 404 page without performing a redirect.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/disable-404-url-redirect/', 'supports_ai' => false, ], 'remove_oembed' => [ 'id' => 'remove_oembed', 'name' => __( 'Remove oEmbed Links', 'admin-optimizer' ), 'slug' => 'remove-oembed', 'section' => 'performance', 'tier' => 'free', 'toggle' => 'remove_oembed', 'option' => null, 'category' => 'performance', 'description' => __( 'Remove links used for embedding your content on other sites.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/remove-oembed-links/', 'supports_ai' => false, ], /* ---------------------------------------------------------------- */ /* Security */ /* ---------------------------------------------------------------- */ 'block_login' => [ 'id' => 'block_login', 'name' => __( 'Block Failed Login', 'admin-optimizer' ), 'slug' => 'block-login', 'section' => 'security', 'tier' => 'free_with_pro_options', 'toggle' => 'enable_block_login', 'option' => 'adminoptim_block_login', 'category' => 'security', 'description' => __( 'Block the user from login after repeated failed login attempts.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/block-failed-login/', 'supports_ai' => false, ], 'two_factor_authentication' => [ 'id' => 'two_factor_authentication', 'name' => __( 'Two-Factor Authentication', 'admin-optimizer' ), 'slug' => 'two-factor-authentication', 'section' => 'security', 'tier' => 'free_with_pro_options', 'toggle' => 'enable_2fa', 'option' => 'adminoptim_2fa', 'category' => 'security', 'description' => __( 'Enable two factor authentication (2FA) for all users.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/two-factor-authentication/', 'supports_ai' => false, ], 'custom_login_url' => [ 'id' => 'custom_login_url', 'name' => __( 'Custom Login URL', 'admin-optimizer' ), 'slug' => 'custom-login-url', 'section' => 'security', 'tier' => 'free', 'toggle' => 'enable_custom_login_url', 'option' => 'adminoptim_custom_login', 'category' => 'security', 'description' => __( 'Hide the wp-login page and create a custom login URL for all users.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/custom-login-url/', 'supports_ai' => false, ], 'disable_xmlrpc' => [ 'id' => 'disable_xmlrpc', 'name' => __( 'Disable XML-RPC', 'admin-optimizer' ), 'slug' => 'disable-xmlrpc', 'section' => 'security', 'tier' => 'free', 'toggle' => 'disable_xmlrpc', 'option' => null, 'category' => 'security', 'description' => __( 'Disable XML-RPC for the whole site.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/disable-xml-rpc/', 'supports_ai' => false, ], 'disable_user_account' => [ 'id' => 'disable_user_account', 'name' => __( 'Disable User Account', 'admin-optimizer' ), 'slug' => 'disable-user-account', 'section' => 'users', 'tier' => 'free', 'toggle' => 'disable_user_account', 'option' => null, 'category' => 'security', 'description' => __( 'Disable user accounts and prevent them from logging in.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/disable-user-accounts/', 'supports_ai' => false, ], 'user_switching' => [ 'id' => 'user_switching', 'name' => __( 'User Switching', 'admin-optimizer' ), 'slug' => 'user-switching', 'section' => 'users', 'tier' => 'pro', 'toggle' => 'enable_user_switching', 'option' => null, 'category' => 'users', 'description' => __( 'Temporarily switch into non-privileged user accounts to verify role and admin-access behavior.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/user-switching/', 'supports_ai' => false, ], 'hide_wp_version' => [ 'id' => 'hide_wp_version', 'name' => __( 'Hide WP Version', 'admin-optimizer' ), 'slug' => 'hide-wp-version', 'section' => 'security', 'tier' => 'free', 'toggle' => 'hide_wp_version', 'option' => null, 'category' => 'security', 'description' => __( 'Remove the WP version from the header.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/hide-wp-version/', 'supports_ai' => false, ], 'remove_pingback_header' => [ 'id' => 'remove_pingback_header', 'name' => __( 'Remove Pingback HTTP Header', 'admin-optimizer' ), 'slug' => 'remove-pingback-header', 'section' => 'security', 'tier' => 'free', 'toggle' => 'remove_pingback_header', 'option' => null, 'category' => 'security', 'description' => __( "Remove 'X-Pingback' from the HTTP header.", 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/remove-pingback-http-header/', 'supports_ai' => false, ], 'remove_powered_by_header' => [ 'id' => 'remove_powered_by_header', 'name' => __( 'Remove Powered By HTTP Header', 'admin-optimizer' ), 'slug' => 'remove-powered-by-header', 'section' => 'security', 'tier' => 'free', 'toggle' => 'remove_powered_by_header', 'option' => null, 'category' => 'security', 'description' => __( "Remove 'X-Powered-By' from the HTTP header.", 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/remove-powered-by-http-header/', 'supports_ai' => false, ], 'remove_rest_api_link' => [ 'id' => 'remove_rest_api_link', 'name' => __( 'Remove REST API Link', 'admin-optimizer' ), 'slug' => 'remove-rest-api-link', 'section' => 'security', 'tier' => 'free', 'toggle' => 'remove_rest_api_link', 'option' => null, 'category' => 'security', 'description' => __( 'Remove the REST API link from the header.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/remove-rest-api-link/', 'supports_ai' => false, ], 'remove_rsd' => [ 'id' => 'remove_rsd', 'name' => __( 'Remove Really Simple Discovery Link', 'admin-optimizer' ), 'slug' => 'remove-rsd', 'section' => 'security', 'tier' => 'free', 'toggle' => 'remove_rsd', 'option' => null, 'category' => 'security', 'description' => __( 'Remove the Really Simple Discovery (RSD) link tag from the header.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/remove-really-simple-discovery-rsd-link/', 'supports_ai' => false, ], 'remove_shortlink' => [ 'id' => 'remove_shortlink', 'name' => __( 'Remove Shortlink', 'admin-optimizer' ), 'slug' => 'remove-shortlink', 'section' => 'security', 'tier' => 'free', 'toggle' => 'remove_shortlink', 'option' => null, 'category' => 'security', 'description' => __( 'Remove the shortlink from the header.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/remove-shortlink/', 'supports_ai' => false, ], /* ---------------------------------------------------------------- */ /* SEO / Content & Media */ /* ---------------------------------------------------------------- */ 'post_cloner' => [ 'id' => 'post_cloner', 'name' => __( 'Post Cloning', 'admin-optimizer' ), 'slug' => 'post-cloner', 'section' => 'content-media', 'tier' => 'free_with_pro_options', 'toggle' => 'enable_post_cloner', 'option' => 'adminoptim_post_cloner', 'category' => 'content', 'description' => __( 'Duplicate posts, pages, and custom posts with a single click.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/post-cloner/', 'supports_ai' => false, ], 'seo_manager' => [ 'id' => 'seo_manager', 'name' => __( 'SEO Manager', 'admin-optimizer' ), 'slug' => 'seo-manager', 'section' => 'seo', 'tier' => 'pro', 'toggle' => 'enable_seo_manager', 'option' => 'adminoptim_seo_manager', 'category' => 'seo', 'description' => __( 'Manage SEO metadata, social tags, schema, and AI-assisted SEO suggestions.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/modules/seo-manager/', 'supports_ai' => true, ], 'content_refresh' => [ 'id' => 'content_refresh', 'name' => __( 'Content Refresh', 'admin-optimizer' ), 'slug' => 'content-refresh', 'section' => 'content-media', 'tier' => 'free_with_pro_options', 'toggle' => 'enable_post_republisher', 'option' => 'adminoptim_post_republisher', 'category' => 'content', 'description' => __( 'Refresh stale-but-ranking content with a new updated content and publish date while preserving SEO value.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/post-republisher/', 'supports_ai' => true, ], 'xml_sitemap' => [ 'id' => 'xml_sitemap', 'name' => __( 'XML Sitemap & Google News Sitemap', 'admin-optimizer' ), 'slug' => 'xml-sitemap', 'section' => 'seo', 'tier' => 'free_with_pro_options', 'toggle' => 'enable_xml_sitemap', 'option' => 'adminoptim_xml_sitemap', 'category' => 'seo', 'description' => __( 'Enable and manage XML Sitemap and Google News Sitemap for your site.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/xml-sitemap/', 'supports_ai' => false, ], 'redirect_manager' => [ 'id' => 'redirect_manager', 'name' => __( 'Redirect Manager', 'admin-optimizer' ), 'slug' => 'redirect-manager', 'section' => 'seo', 'tier' => 'free_with_pro_options', 'toggle' => 'enable_redirect_manager', 'option' => 'adminoptim_redirect_manager', 'category' => 'seo', 'description' => __( 'Enable and manage URL redirection for your site.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/redirect-manager/', 'supports_ai' => false, ], 'publish_missed_schedule' => [ 'id' => 'publish_missed_schedule', 'name' => __( 'Auto Publish Missed Schedule', 'admin-optimizer' ), 'slug' => 'publish-missed-schedule', 'section' => 'content-media', 'tier' => 'free', 'toggle' => 'enable_publish_missed_posts', 'option' => null, 'category' => 'content', 'description' => __( 'Check posts every 15 minutes and auto publish posts with missed schedule.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/auto-publish-posts/', 'supports_ai' => false, ], 'disable_comments' => [ 'id' => 'disable_comments', 'name' => __( 'Disable Comments', 'admin-optimizer' ), 'slug' => 'disable-comments', 'section' => 'content-media', 'tier' => 'free', 'toggle' => 'enable_disable_comments', 'option' => null, 'category' => 'content', 'description' => __( 'Disable comments and pings across all post types and remove comment-related admin interfaces.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/disable-comments/', 'supports_ai' => false, ], 'modified_date' => [ 'id' => 'modified_date', 'name' => __( 'Lock Post Modified Date', 'admin-optimizer' ), 'slug' => 'modified-date', 'section' => 'content-media', 'tier' => 'free_with_pro_options', 'toggle' => 'manage_post_modified_date', 'option' => 'adminoptim_modified_date', 'category' => 'content', 'description' => __( 'Change or prevent updating of the last modified date for each post.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/lock-modified-date/', 'supports_ai' => false, ], 'svg_upload' => [ 'id' => 'svg_upload', 'name' => __( 'SVG Image Upload', 'admin-optimizer' ), 'slug' => 'svg-upload', 'section' => 'content-media', 'tier' => 'free', 'toggle' => 'enable_svg_upload', 'option' => null, 'category' => 'media', 'description' => __( 'Enable upload of SVG files.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/enable-svg-image-upload/', 'supports_ai' => false, ], 'image_alt_text' => [ 'id' => 'image_alt_text', 'name' => __( 'Set Image Filename As Alt Text', 'admin-optimizer' ), 'slug' => 'image-alt-text', 'section' => 'content-media', 'tier' => 'free', 'toggle' => 'enable_image_alt_text', 'option' => null, 'category' => 'media', 'description' => __( 'Auto set image filename as alt text.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/set-image-filename-as-alt-text/', 'supports_ai' => false, ], 'ads_robots_txt' => [ 'id' => 'ads_robots_txt', 'name' => __( 'Robots.txt & Ads.txt Manager', 'admin-optimizer' ), 'slug' => 'ads-robots-txt', 'section' => 'seo', 'tier' => 'free', 'toggle' => 'enable_txt_management', 'option' => 'adminoptim_ads_robots_txt', 'category' => 'seo', 'description' => __( 'Manage robots.txt, ads.txt, and app-ads.txt directly in WordPress.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/robots-ads-app-ads-txt/', 'supports_ai' => false, ], 'limit_image_size' => [ 'id' => 'limit_image_size', 'name' => __( 'Limit Image Upload File Size', 'admin-optimizer' ), 'slug' => 'limit-image-size', 'section' => 'content-media', 'tier' => 'free_with_pro_options', 'toggle' => 'enable_limit_image_upload_size', 'option' => 'adminoptim_limit_image_size', 'category' => 'media', 'description' => __( 'Limit the image upload file size.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/limit-image-upload-file-size/', 'supports_ai' => false, ], 'image_underscore_to_hyphen' => [ 'id' => 'image_underscore_to_hyphen', 'name' => __( 'Convert Underscore In Image Filename To Hyphen', 'admin-optimizer' ), 'slug' => 'image-underscore-to-hyphen', 'section' => 'content-media', 'tier' => 'free', 'toggle' => 'enable_image_underscore_to_hyphen', 'option' => null, 'category' => 'media', 'description' => __( 'Auto sanitize the image filename and convert underscore (_) to hyphen (-) during image upload.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/convert-underscore-in-filename-to-hyphen/', 'supports_ai' => false, ], 'auto_add_anchor' => [ 'id' => 'auto_add_anchor', 'name' => __( 'Auto Add HTML Anchor To Heading Block', 'admin-optimizer' ), 'slug' => 'auto-add-anchor', 'section' => 'content-media', 'tier' => 'free', 'toggle' => 'enable_auto_add_anchor', 'option' => null, 'category' => 'content', 'description' => __( 'Transform your WordPress headings into clickable link targets.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/auto-add-html-anchor/', 'supports_ai' => false, ], 'disable_category_archive' => [ 'id' => 'disable_category_archive', 'name' => __( 'Disable Category Archive Page', 'admin-optimizer' ), 'slug' => 'disable-category-archive', 'section' => 'seo', 'tier' => 'free', 'toggle' => 'disable_category_archive', 'option' => null, 'category' => 'seo', 'description' => __( 'Disable all Category archive pages in the site frontend.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/disable-category-archive-page/', 'supports_ai' => false, ], 'disable_tag_archive' => [ 'id' => 'disable_tag_archive', 'name' => __( 'Disable Tag Archive Page', 'admin-optimizer' ), 'slug' => 'disable-tag-archive', 'section' => 'seo', 'tier' => 'free', 'toggle' => 'disable_tag_archive', 'option' => null, 'category' => 'seo', 'description' => __( 'Disable all Tag archive pages in the site frontend.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/disable-tag-archive-page/', 'supports_ai' => false, ], 'disable_author_archive' => [ 'id' => 'disable_author_archive', 'name' => __( 'Disable Author Archive Page', 'admin-optimizer' ), 'slug' => 'disable-author-archive', 'section' => 'seo', 'tier' => 'free', 'toggle' => 'disable_author_archive', 'option' => null, 'category' => 'seo', 'description' => __( 'Disable all Author archive pages in the site frontend.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/disable-author-archive-page/', 'supports_ai' => false, ], 'disable_date_archive' => [ 'id' => 'disable_date_archive', 'name' => __( 'Disable Date Archive Page', 'admin-optimizer' ), 'slug' => 'disable-date-archive', 'section' => 'seo', 'tier' => 'free', 'toggle' => 'disable_date_archive', 'option' => null, 'category' => 'seo', 'description' => __( 'Disable all Date archive pages in the site frontend.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/disable-date-archive-page/', 'supports_ai' => false, ], /* ---------------------------------------------------------------- */ /* Custom Fields & Types */ /* ---------------------------------------------------------------- */ 'custom_fields_builder' => [ 'id' => 'custom_fields_builder', 'name' => __( 'Custom Fields Builder', 'admin-optimizer' ), 'slug' => 'custom-fields-builder', 'section' => 'custom-fields-types', 'tier' => 'pro', 'toggle' => 'enable_custom_fields_builder', 'option' => null, 'category' => 'custom-fields', 'description' => __( 'A powerful builder that allows you to easily add custom fields and content for various Post Types.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/custom-fields-builder/', 'supports_ai' => false, ], 'custom_post_status' => [ 'id' => 'custom_post_status', 'name' => __( 'Custom Post Status', 'admin-optimizer' ), 'slug' => 'custom-post-status', 'section' => 'custom-fields-types', 'tier' => 'free_with_pro_options', 'toggle' => 'enable_custom_post_status', 'option' => null, 'category' => 'custom-fields', 'description' => __( 'Enable the creation of custom post status.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/custom-post-status/', 'supports_ai' => false, ], 'custom_post_types' => [ 'id' => 'custom_post_types', 'name' => __( 'Custom Post Types', 'admin-optimizer' ), 'slug' => 'custom-post-types', 'section' => 'custom-fields-types', 'tier' => 'free', 'toggle' => 'enable_custom_post_types', 'option' => 'adminoptim-post-types', 'category' => 'custom-fields', 'description' => __( 'Enable the creation of custom post types.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/custom-post-type/', 'supports_ai' => false, ], 'custom_taxonomies' => [ 'id' => 'custom_taxonomies', 'name' => __( 'Custom Taxonomies', 'admin-optimizer' ), 'slug' => 'custom-taxonomies', 'section' => 'custom-fields-types', 'tier' => 'free', 'toggle' => 'enable_custom_taxonomies', 'option' => 'adminoptim-custom-taxonomies', 'category' => 'custom-fields', 'description' => __( 'Enable the creation of custom taxonomies.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/custom-taxonomies/', 'supports_ai' => false, ], /* ---------------------------------------------------------------- */ /* Admin & Users */ /* ---------------------------------------------------------------- */ 'hide_update_notice' => [ 'id' => 'hide_update_notice', 'name' => __( 'Hide Update Notice', 'admin-optimizer' ), 'slug' => 'hide-update-notice', 'section' => 'users', 'tier' => 'free', 'toggle' => 'hide_update_notices', 'option' => null, 'category' => 'admin', 'description' => __( 'Only show update notices to Administrators and those with update capabilities.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/hide-update-notice/', 'supports_ai' => false, ], 'disable_gutenberg_editor' => [ 'id' => 'disable_gutenberg_editor', 'name' => __( 'Disable Gutenberg Editor', 'admin-optimizer' ), 'slug' => 'disable-gutenberg-editor', 'section' => 'content-media', 'tier' => 'free', 'toggle' => 'disable_gutenberg_editor', 'option' => null, 'category' => 'content', 'description' => __( 'Disable the Gutenberg Block Editor and restore the Classic editor as the default editor.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/disable-gutenberg-editor/', 'supports_ai' => false, ], 'page_ordering' => [ 'id' => 'page_ordering', 'name' => __( 'Page Ordering', 'admin-optimizer' ), 'slug' => 'page-ordering', 'section' => 'content-media', 'tier' => 'free', 'toggle' => 'enable_page_ordering', 'option' => null, 'category' => 'content', 'description' => __( 'Order your pages or hierarchical custom post types with drag and drop, right from the Pages list.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/page-ordering/', 'supports_ai' => false, ], 'user_login_tracking' => [ 'id' => 'user_login_tracking', 'name' => __( 'Track User Last Login Date', 'admin-optimizer' ), 'slug' => 'user-login-tracking', 'section' => 'users', 'tier' => 'free', 'toggle' => 'enable_user_login_tracking', 'option' => null, 'category' => 'users', 'description' => __( "Track and display the user's last login date.", 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/last-login-date/', 'supports_ai' => false, ], 'user_registration_date' => [ 'id' => 'user_registration_date', 'name' => __( 'Display User Registration Date', 'admin-optimizer' ), 'slug' => 'user-registration-date', 'section' => 'users', 'tier' => 'free', 'toggle' => 'display_user_registration_date', 'option' => null, 'category' => 'users', 'description' => __( 'Track and show the user registration date.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/display-user-registration-date/', 'supports_ai' => false, ], 'disable_user_signup_notification' => [ 'id' => 'disable_user_signup_notification', 'name' => __( 'Disable New User Signup Notification', 'admin-optimizer' ), 'slug' => 'disable-user-signup-notification', 'section' => 'users', 'tier' => 'free', 'toggle' => 'disable_user_signup_notification', 'option' => null, 'category' => 'users', 'description' => __( 'Disable the option to send the new user an email when their account was created.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/disable-new-user-signup-notification/', 'supports_ai' => false, ], 'user_access_manager' => [ 'id' => 'user_access_manager', 'name' => __( 'User Access Manager', 'admin-optimizer' ), 'slug' => 'user-access-manager', 'section' => 'users', 'tier' => 'free_with_pro_options', 'toggle' => 'enable_user_access_manager', 'option' => 'adminoptim_user_access_manager', 'category' => 'users', 'description' => __( 'Manage roles, custom capabilities, multiple-role assignment, admin toolbar visibility, admin menu access, and integrated admin access controls.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/user-access-manager/', 'supports_ai' => false, ], 'auto_open_advanced' => [ 'id' => 'auto_open_advanced', 'name' => __( 'Auto Open Advanced Field', 'admin-optimizer' ), 'slug' => 'auto-open-advanced', 'section' => 'content-media', 'tier' => 'free', 'toggle' => 'enable_auto_open_advanced', 'option' => null, 'category' => 'content', 'description' => __( 'Reduce mouse clicks and open the Advanced field in Gutenberg when you move your mouse over it.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/auto-open-advanced-field-in-gutenberg/', 'supports_ai' => false, ], /* ---------------------------------------------------------------- */ /* System Tools */ /* ---------------------------------------------------------------- */ 'ai_connectors' => [ 'id' => 'ai_connectors', 'name' => __( 'AI Connectors', 'admin-optimizer' ), 'slug' => 'ai-connectors', 'section' => 'tools', 'tier' => 'pro', 'toggle' => 'enable_ai_connectors', 'option' => 'adminoptim_ai_connectors', 'category' => 'system', 'description' => __( 'Connect to AI providers, manage API keys, and view usage cost and request logs.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/ai-connectors/', 'supports_ai' => true, ], 'smtp_mail' => [ 'id' => 'smtp_mail', 'name' => __( 'SMTP Mail', 'admin-optimizer' ), 'slug' => 'smtp-mail', 'section' => 'tools', 'tier' => 'free', 'toggle' => 'enable_smtp_mail', 'option' => 'adminoptim_smtp', 'category' => 'system', 'description' => __( 'Set up SMTP email delivery in WordPress, create routing rules, and send emails directly from the admin.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/smtp-mail/', 'supports_ai' => false, ], 'form_builder' => [ 'id' => 'form_builder', 'name' => __( 'Form Builder', 'admin-optimizer' ), 'slug' => 'form-builder', 'section' => 'tools', 'tier' => 'free_with_pro_options', 'toggle' => 'enable_form_builder', 'option' => 'adminoptim_forms_builder', 'category' => 'system', 'description' => __( 'Create forms to display on your website.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/form-builder/', 'supports_ai' => false, ], 'custom_code' => [ 'id' => 'custom_code', 'name' => __( 'Custom Code', 'admin-optimizer' ), 'slug' => 'custom-code', 'section' => 'tools', 'tier' => 'free_with_pro_options', 'toggle' => 'enable_custom_code', 'option' => null, 'category' => 'system', 'description' => __( 'Add custom CSS, HTML, JavaScript and PHP code snippets to your website.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/custom-code/', 'supports_ai' => false, ], /* ---------------------------------------------------------------- */ /* Pro Infrastructure */ /* ---------------------------------------------------------------- */ 'presets' => [ 'id' => 'presets', 'name' => __( 'Presets', 'admin-optimizer' ), 'slug' => 'presets', 'section' => 'presets', 'tier' => 'free', 'toggle' => null, 'option' => null, 'category' => 'infrastructure', 'description' => __( 'One-click module bundles for common site profiles.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/presets/', 'supports_ai' => false, ], 'export_import' => [ 'id' => 'export_import', 'name' => __( 'Export and Import', 'admin-optimizer' ), 'slug' => 'export-import', 'section' => 'export', 'tier' => 'pro_infrastructure', 'toggle' => null, 'option' => null, 'category' => 'infrastructure', 'description' => __( 'Export and import Admin Optimizer settings between sites.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/docs/export-import/', 'supports_ai' => false, ], 'license' => [ 'id' => 'license', 'name' => __( 'License', 'admin-optimizer' ), 'slug' => 'license', 'section' => 'license', 'tier' => 'pro_infrastructure', 'toggle' => null, 'option' => null, 'category' => 'infrastructure', 'description' => __( 'Activate and manage your Admin Optimizer Pro license.', 'admin-optimizer' ), 'requires' => [], 'website' => 'https://www.adminoptimizer.com/pricing/', 'supports_ai' => false, ], ]; } /** * Normalize and validate the raw module catalog. * * Ensures every module has the expected keys, sane defaults, and that its * `id` matches its array key, so malformed/partial entries (including * third-party additions via the registry filter) never break consumers. * * @param array $modules Raw module catalog keyed by module ID. * * @return array */ private static function normalize_modules( array $modules ): array { $normalized = []; foreach ( $modules as $module_id => $module ) { $module = is_array( $module ) ? $module : []; $tier = $module['tier'] ?? self::DEFAULT_TIER; if ( ! in_array( $tier, self::TIERS, true ) ) { $tier = self::DEFAULT_TIER; } $normalized[ $module_id ] = [ 'id' => $module_id, 'name' => $module['name'] ?? $module_id, 'slug' => $module['slug'] ?? $module_id, 'section' => $module['section'] ?? '', 'tier' => $tier, 'toggle' => $module['toggle'] ?? null, 'option' => $module['option'] ?? null, 'category' => $module['category'] ?? '', 'description' => $module['description'] ?? '', 'requires' => is_array( $module['requires'] ?? null ) ? $module['requires'] : [], 'website' => $module['website'] ?? '', 'supports_ai' => ! empty( $module['supports_ai'] ), ]; } return $normalized; } }