matcher = $matcher ? $matcher : new AssetRuleMatcher(); $this->graph = $graph ? $graph : new AssetDependencyGraph(); $this->store = $store ? $store : new ScriptManagerRuleStore(); } public function register() { add_action( 'wp_enqueue_scripts', array( $this, 'apply_rules' ), PHP_INT_MAX ); add_filter( 'nbh_performance_script_loading_strategy', array( $this, 'loading_strategy' ), 30, 3 ); add_filter( 'script_loader_src', array( $this, 'filter_asset_version' ), 999, 2 ); add_filter( 'style_loader_src', array( $this, 'filter_asset_version' ), 999, 2 ); add_filter( 'script_loader_tag', array( $this, 'filter_script_tag' ), 999, 3 ); add_filter( 'style_loader_tag', array( $this, 'filter_style_tag' ), 999, 4 ); add_filter( 'wp_resource_hints', array( $this, 'resource_hints' ), 20, 2 ); add_action( 'wp_head', array( $this, 'print_preloads' ), 2 ); add_action( 'rest_api_init', array( $this, 'routes' ) ); add_action( 'shutdown', array( $this, 'persist_warnings' ), 1 ); } public function routes() { nbh_performance_register_rest_route( 'nbh-performance/v1', '/script-manager/status', array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array( $this, 'status' ), 'permission_callback' => array( $this, 'allowed' ), ) ); nbh_performance_register_rest_route( 'nbh-performance/v1', '/script-manager/rules', array( array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array( $this, 'read_rules' ), 'permission_callback' => array( $this, 'allowed' ), ), array( 'methods' => \WP_REST_Server::EDITABLE, 'callback' => array( $this, 'save_rules' ), 'permission_callback' => array( $this, 'allowed' ), ), ) ); nbh_performance_register_rest_route( 'nbh-performance/v1', '/script-manager/diagnostics', array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array( $this, 'diagnostics' ), 'permission_callback' => array( $this, 'allowed' ), ) ); nbh_performance_register_rest_route( 'nbh-performance/v1', '/script-manager/analyze', array( 'methods' => \WP_REST_Server::CREATABLE, 'callback' => array( $this, 'analyze_rule' ), 'permission_callback' => array( $this, 'allowed' ), ) ); nbh_performance_register_rest_route( 'nbh-performance/v1', '/script-manager/export', array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array( $this, 'export_rules' ), 'permission_callback' => array( $this, 'allowed' ), ) ); nbh_performance_register_rest_route( 'nbh-performance/v1', '/script-manager/import', array( 'methods' => \WP_REST_Server::CREATABLE, 'callback' => array( $this, 'import_rules' ), 'permission_callback' => array( $this, 'allowed' ), ) ); nbh_performance_register_rest_route( 'nbh-performance/v1', '/script-manager/history', array( array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array( $this, 'history' ), 'permission_callback' => array( $this, 'allowed' ), ), array( 'methods' => \WP_REST_Server::CREATABLE, 'callback' => array( $this, 'restore_history' ), 'permission_callback' => array( $this, 'allowed' ), ), ) ); } public function status() { $enabled = (bool) get_option( 'nbh_performance_script_manager_enabled', 0 ); $testing = (bool) get_option( 'nbh_performance_script_manager_testing_mode', 1 ); $safe_mode = (bool) get_option( 'nbh_performance_safe_mode', 1 ); $rules = $this->store->all(); $warnings = (array) get_option( 'nbh_performance_script_manager_warnings', array() ); $active = 0; foreach ( $rules as $rule ) { if ( ! isset( $rule['enabled'] ) || $rule['enabled'] ) { ++$active; } } $snapshot = get_option( 'nbh_performance_asset_inventory_snapshot', array() ); $snapshot = is_array( $snapshot ) ? $snapshot : array(); $scripts = isset( $snapshot['scripts'] ) && is_array( $snapshot['scripts'] ) ? $snapshot['scripts'] : array(); $styles = isset( $snapshot['styles'] ) && is_array( $snapshot['styles'] ) ? $snapshot['styles'] : array(); $assets = array_values( array_merge( $scripts, $styles ) ); return rest_ensure_response( array( 'enabled' => $enabled, 'testing_mode' => $testing, 'safe_mode' => $safe_mode, 'runtime_ready' => $enabled && ! $safe_mode, 'rule_count' => count( $rules ), 'active_rules' => $active, 'warning_count' => count( $warnings ), 'warnings' => array_values( $warnings ), 'rules' => $rules, 'protected' => $this->protected_handles(), 'history' => $this->history_summary(), 'schema' => 3, 'inventory' => array( 'enabled' => (bool) get_option( 'nbh_performance_asset_inventory_enabled', 0 ) || $enabled, 'captured_at' => isset( $snapshot['captured_at'] ) ? (int) $snapshot['captured_at'] : 0, 'url' => isset( $snapshot['url'] ) ? (string) $snapshot['url'] : '', 'script_count' => count( $scripts ), 'style_count' => count( $styles ), 'asset_count' => count( $assets ), ), 'assets' => $assets, ) ); } public function allowed( $request ) { return current_user_can( 'manage_options' ) && \NBH\Performance\Modules\Security\NonceGuard::verify_rest( $request ); } public function read_rules() { return rest_ensure_response( array( 'rules' => $this->store->all(), 'protected' => $this->protected_handles(), 'testing_mode' => (bool) get_option( 'nbh_performance_script_manager_testing_mode', 1 ), 'warnings' => get_option( 'nbh_performance_script_manager_warnings', array() ), 'history' => $this->history_summary(), 'schema' => 3, ) ); } public function diagnostics() { return rest_ensure_response( array( 'scripts' => $this->registry_diagnostics( wp_scripts(), 'script' ), 'styles' => $this->registry_diagnostics( wp_styles(), 'style' ), 'warnings' => get_option( 'nbh_performance_script_manager_warnings', array() ), 'matched_rules' => array_values( $this->matched_rules ), 'context' => $this->request_context(), ) ); } public function save_rules( $request ) { $rules = $request->get_param( 'rules' ); $clean = $this->normalize_rules( $rules ); $this->store->replace( $clean, 'manual_save' ); if ( null !== $request->get_param( 'testing_mode' ) ) { update_option( 'nbh_performance_script_manager_testing_mode', rest_sanitize_boolean( $request->get_param( 'testing_mode' ) ) ? 1 : 0, false ); } do_action( 'nbh_performance_script_manager_rules_updated', $clean ); return $this->read_rules(); } public function analyze_rule( WP_REST_Request $request ) { $rule = $this->normalize_rule( $request->get_param( 'rule' ) ); if ( ! $rule ) { return new \WP_Error( 'nbh_invalid_rule', __( 'The proposed rule is invalid.', 'nbh-performance' ), array( 'status' => 400 ) ); } $registry = 'script' === $rule['type'] ? wp_scripts() : wp_styles(); $item = $registry && isset( $registry->registered[ $rule['handle'] ] ) ? $registry->registered[ $rule['handle'] ] : null; $meta = $registry ? $this->graph->metadata( $registry, $rule['handle'] ) : array( 'dependencies' => array(), 'dependents' => array(), ); $risk = $this->risk_level( $rule, $meta ); return rest_ensure_response( array( 'rule' => $rule, 'registered' => (bool) $item, 'protected' => in_array( $rule['handle'], $this->protected_handles(), true ), 'dependencies' => $meta['dependencies'], 'dependents' => $meta['dependents'], 'risk' => $risk, 'explanation' => $this->matcher->explain( $rule ), ) ); } public function export_rules() { return new WP_REST_Response( $this->store->export_payload(), 200 ); } public function import_rules( WP_REST_Request $request ) { $payload = $request->get_param( 'payload' ); if ( is_string( $payload ) ) { $payload = json_decode( $payload, true ); } if ( ! is_array( $payload ) || empty( $payload['rules'] ) || ! is_array( $payload['rules'] ) ) { return new \WP_Error( 'nbh_invalid_import', __( 'The imported Script Manager file is invalid.', 'nbh-performance' ), array( 'status' => 400 ) ); } $clean = $this->normalize_rules( $payload['rules'] ); $this->store->replace( $clean, 'import' ); return $this->read_rules(); } public function history() { return rest_ensure_response( array( 'history' => $this->history_summary() ) ); } public function restore_history( WP_REST_Request $request ) { $rules = $this->store->restore( $request->get_param( 'snapshot_id' ) ); if ( null === $rules ) { return new \WP_Error( 'nbh_history_not_found', __( 'The requested rules snapshot was not found.', 'nbh-performance' ), array( 'status' => 404 ) ); } return $this->read_rules(); } public function apply_rules() { if ( ! $this->runtime_enabled() ) { return; } foreach ( $this->matching_rules() as $rule ) { $this->matched_rules[ $rule['id'] ] = array( 'id' => $rule['id'], 'handle' => $rule['handle'], 'action' => $rule['action'], ); $registry = 'script' === $rule['type'] ? wp_scripts() : wp_styles(); if ( ! $registry || ! isset( $registry->registered[ $rule['handle'] ] ) ) { continue; } if ( 'unload' === $rule['action'] ) { $this->apply_unload( $registry, $rule ); } elseif ( 'move_footer' === $rule['action'] && 'script' === $rule['type'] ) { $registry->add_data( $rule['handle'], 'group', 1 ); if ( isset( $registry->registered[ $rule['handle'] ]->extra ) ) { $registry->registered[ $rule['handle'] ]->extra['group'] = 1; } } } } public function loading_strategy( $strategy, $handle, $src ) { unset( $src ); if ( ! $this->runtime_enabled() ) { return $strategy; } foreach ( $this->matching_rules() as $rule ) { if ( 'script' === $rule['type'] && $handle === $rule['handle'] && in_array( $rule['action'], array( 'delay', 'defer', 'async' ), true ) ) { return $rule['action']; } } return $strategy; } public function filter_asset_version( $src, $handle ) { if ( ! $this->runtime_enabled() || ! $this->has_matching_action( $handle, 'remove_version' ) ) { return $src; } return remove_query_arg( 'ver', $src ); } public function filter_script_tag( $tag, $handle, $src ) { unset( $src ); if ( ! $this->runtime_enabled() ) { return $tag; } if ( $this->has_matching_action( $handle, 'move_footer' ) ) { $tag = str_replace( '