*/ private $captured_notices = array(); /** * Register the boundary before normal notices and render the collected group * after the AI-O navigation shell. */ public function __construct() { foreach ( array( 'admin_notices', 'network_admin_notices', 'user_admin_notices', 'all_admin_notices' ) as $hook ) { add_action( $hook, array( $this, 'capture_foreign_notice_callbacks' ), -9999 ); } add_action( 'all_admin_notices', array( $this, 'render_captured_notices' ), 9999 ); } /** * Whether this request belongs to the AI-O Optimiser admin surface. * * @return bool */ public static function is_ai_o_page() { // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Screen detection: reads the admin page slug only. $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; if ( '' === $page || 0 !== strpos( $page, 'ai-o-optimiser-' ) ) { return false; } return (bool) apply_filters( 'ai_o_optimiser_admin_notice_boundary_enabled', true, $page ); // phpcs:enable WordPress.Security.NonceVerification.Recommended } /** * Replace known third-party plugin/theme notice callbacks with capture wrappers. * WordPress core notices and AI-O Optimiser notices continue to render normally. * * @return void */ public function capture_foreign_notice_callbacks() { if ( ! self::is_ai_o_page() ) { return; } $hook_name = current_filter(); if ( ! is_string( $hook_name ) || '' === $hook_name ) { return; } global $wp_filter; if ( ! isset( $wp_filter[ $hook_name ] ) || ! is_object( $wp_filter[ $hook_name ] ) || ! isset( $wp_filter[ $hook_name ]->callbacks ) || ! is_array( $wp_filter[ $hook_name ]->callbacks ) ) { return; } $callbacks = $wp_filter[ $hook_name ]->callbacks; foreach ( $callbacks as $priority => $entries ) { if ( -9999 === (int) $priority || 9999 === (int) $priority ) { continue; } foreach ( (array) $entries as $entry ) { if ( ! isset( $entry['function'] ) ) { continue; } $callback = $entry['function']; if ( ! $this->callback_is_foreign_plugin_or_theme( $callback ) ) { continue; } $allow = (bool) apply_filters( 'ai_o_optimiser_allow_external_admin_notice_callback', false, $callback, $hook_name, (int) $priority ); if ( $allow ) { continue; } $accepted_args = isset( $entry['accepted_args'] ) ? max( 0, (int) $entry['accepted_args'] ) : 1; remove_action( $hook_name, $callback, (int) $priority ); add_action( $hook_name, function () use ( $callback ) { // Opened and closed in this one scope. The finally block // closes the buffer even when the callback throws, so a // third-party notice that fails cannot leave the output // buffer stack one level deeper than it found it. $level = ob_get_level(); ob_start(); try { call_user_func( $callback ); } finally { $html = ob_get_level() > $level ? trim( (string) ob_get_clean() ) : ''; } if ( '' !== $html ) { $this->captured_notices[] = $html; } }, (int) $priority, $accepted_args ); } } } /** * Render third-party notices in one compact disclosure below the AI-O shell. * The container is always present so the client-side guard can also move late * JavaScript-injected banners into it without disturbing the workspace layout. * * @return void */ public function render_captured_notices() { if ( ! self::is_ai_o_page() ) { return; } $count = count( $this->captured_notices ); $hidden = 0 === $count ? ' hidden' : ''; ?>
data-ai-o-optimiser-notice-label="">
captured_notices as $notice_html ) : ?>
callback_file( $callback ); if ( '' === $file ) { return false; } $file = wp_normalize_path( $file ); $plugin_dir = trailingslashit( wp_normalize_path( AI_O_Optimiser_PLUGIN_DIR ) ); if ( 0 === strpos( $file, $plugin_dir ) ) { return false; } $foreign_roots = array(); if ( defined( 'WP_PLUGIN_DIR' ) ) { $foreign_roots[] = WP_PLUGIN_DIR; } if ( defined( 'WPMU_PLUGIN_DIR' ) ) { $foreign_roots[] = WPMU_PLUGIN_DIR; } if ( function_exists( 'get_theme_root' ) ) { $foreign_roots[] = get_theme_root(); } foreach ( array_filter( $foreign_roots ) as $root ) { $root = trailingslashit( wp_normalize_path( $root ) ); if ( 0 === strpos( $file, $root ) ) { return true; } } return false; } /** * Resolve the source file that owns a callback. * * @param callable $callback Registered callback. * @return string */ private function callback_file( $callback ) { try { if ( $callback instanceof Closure ) { $reflection = new ReflectionFunction( $callback ); return (string) $reflection->getFileName(); } if ( is_array( $callback ) && 2 === count( $callback ) ) { $reflection = new ReflectionMethod( $callback[0], $callback[1] ); return (string) $reflection->getFileName(); } if ( is_string( $callback ) ) { if ( false !== strpos( $callback, '::' ) ) { list( $class, $method ) = explode( '::', $callback, 2 ); $reflection = new ReflectionMethod( $class, $method ); return (string) $reflection->getFileName(); } if ( function_exists( $callback ) ) { $reflection = new ReflectionFunction( $callback ); return (string) $reflection->getFileName(); } } if ( is_object( $callback ) && is_callable( $callback ) ) { $reflection = new ReflectionMethod( $callback, '__invoke' ); return (string) $reflection->getFileName(); } } catch ( ReflectionException $exception ) { return ''; } return ''; } }