*/ private array $callbacks = array(); /** * Known sensitive plugins and a short reason. * * @var array */ private const WATCHLIST = array( 'woocommerce/woocommerce.php' => 'WooCommerce carts and sessions can diverge when plugins or themes change per visitor.', 'elementor/elementor.php' => 'Elementor output is tied to theme locations and may not follow a per-request theme change.', 'js_composer/js_composer.php' => 'WPBakery stores layout in content that may assume the original theme.', 'divi-builder/divi-builder.php' => 'Divi layouts can depend on the Divi theme being the active theme.', 'wordpress-seo/wp-seo.php' => 'SEO plugins should keep seeing one canonical URL. Confirm crawlers are excluded.', 'seo-by-rank-math/rank-math.php' => 'Confirm the SEO plugin does not emit a variant-specific canonical.', 'wp-rocket/wp-rocket.php' => 'Page cache must vary on WPAB cookies or bypass experiment URLs.', 'litespeed-cache/litespeed-cache.php' => 'LiteSpeed Cache keeps a separate public copy for each variant, using the assignment cookie. The first visit and previews are not cached.', 'wp-fastest-cache/wpFastestCache.php' => 'Logged-out visitors receive a separate WP Fastest Cache file for each variant. Logged-in visitors are not served from that cache.', 'w3-total-cache/w3-total-cache.php' => 'Page cache must vary on the assignment cookie.', 'sitepress-multilingual-cms/sitepress.php' => 'Language plugins may resolve content before the experiment context exists.', 'polylang/polylang.php' => 'Language negotiation can run before the variant is known.', 'wordfence/wordfence.php' => 'Security plugins may treat variant traffic or missing plugins as suspicious.', ); /** * Register a compatibility callback. * * @param string $plugin_file Plugin file. * @param callable $callback Callback receiving the environment array. */ public function register( string $plugin_file, callable $callback ): void { $this->callbacks[ $plugin_file ] = $callback; } /** * Let other plugins register during wpab_register_compatibility. */ public function boot(): void { do_action( 'wpab_register_compatibility', $this ); } /** * Check one theme. * * @param string $stylesheet Stylesheet slug. * @return array> */ public function check_theme( string $stylesheet ): array { $notes = array(); if ( '' === $stylesheet ) { return $notes; } $path = get_theme_root() . '/' . $stylesheet . '/style.css'; if ( ! is_readable( $path ) ) { $notes[] = array( 'level' => 'error', 'message' => sprintf( 'Theme "%s" is not available.', $stylesheet ), ); return $notes; } $contents = Local_File::read( $path ); if ( str_contains( $contents, 'WPAB-compatible' ) || str_contains( $contents, 'WPAB-Compatible' ) ) { $notes[] = array( 'level' => 'info', 'message' => sprintf( 'Theme "%s" declares WPAB-compatible.', $stylesheet ), ); } return $notes; } /** * Check one plugin file. * * @param string $plugin_file Plugin file. * @return array> */ public function check_plugin( string $plugin_file ): array { if ( isset( self::WATCHLIST[ $plugin_file ] ) ) { $handled = in_array( $plugin_file, array( 'wp-fastest-cache/wpFastestCache.php', 'litespeed-cache/litespeed-cache.php', ), true ); $level = $handled || $this->declares_compatible( $plugin_file ) ? 'info' : 'warning'; return array( array( 'level' => $level, 'message' => self::WATCHLIST[ $plugin_file ], ), ); } if ( $this->declares_compatible( $plugin_file ) ) { return array( array( 'level' => 'info', 'message' => sprintf( '%s declares WPAB-compatible.', $plugin_file ), ), ); } return array(); } /** * Check a whole experiment environment. * * @param array $environment Environment: theme, disabled_plugins, and active plugin files. * @return array> */ public function check_experiment_environment( array $environment ): array { $notes = $this->check_theme( (string) ( $environment['theme'] ?? '' ) ); $active = isset( $environment['active_plugins'] ) && is_array( $environment['active_plugins'] ) ? $environment['active_plugins'] : array(); $disabled = isset( $environment['disabled_plugins'] ) && is_array( $environment['disabled_plugins'] ) ? $environment['disabled_plugins'] : array(); foreach ( $active as $file ) { $file = (string) $file; if ( in_array( $file, $disabled, true ) ) { continue; } $notes = array_merge( $notes, $this->check_plugin( $file ) ); if ( isset( $this->callbacks[ $file ] ) ) { $result = call_user_func( $this->callbacks[ $file ], $environment ); if ( is_array( $result ) ) { $notes = array_merge( $notes, $result ); } } } return $notes; } /** * Whether the plugin header declares compatibility. * * @param string $plugin_file Plugin file. * @return bool */ private function declares_compatible( string $plugin_file ): bool { $path = Locations::plugin_file( $plugin_file ); if ( '' === $path || ! is_readable( $path ) ) { return false; } $contents = Local_File::read( $path, 8192 ); return str_contains( $contents, 'WPAB-Compatible:' ) || str_contains( $contents, 'WPAB-compatible:' ); } }