$names, 'paths' => $paths, ); $stored = get_option( self::OPTION, null ); if ( $stored === $state ) { return; } if ( ! self::write_rules() ) { return; } update_option( self::OPTION, $state, false ); $previous = array(); if ( is_array( $stored ) && isset( $stored['paths'] ) && is_array( $stored['paths'] ) ) { $previous = array_map( 'strval', $stored['paths'] ); } foreach ( array_unique( array_merge( $previous, $paths ) ) as $path ) { do_action( 'litespeed_purge_url', $path ); } } /** * Drop cached copies when the original post or its variant copy is saved. * * @param int $post_id Post ID. */ public function purge_for_post( int $post_id ): void { if ( ! self::is_active() || wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) { return; } $original = (int) get_post_meta( $post_id, Variant_Copy::META, true ); $target = $original > 0 ? $original : $post_id; foreach ( self::index_experiments() as $experiment ) { if ( (int) ( $experiment['post_id'] ?? 0 ) !== $target ) { continue; } $path = (string) ( $experiment['path'] ?? '' ); if ( '' !== $path ) { do_action( 'litespeed_purge_url', $path ); } } } /** * Remove assignment cookies from LiteSpeed's rules. */ public static function uninstall(): void { if ( ! self::is_active() ) { delete_option( self::OPTION ); return; } self::$suspend = true; self::write_rules(); delete_option( self::OPTION ); } /** * Assignment cookie names for experiments that are inside their schedule. * * @param array> $experiments Experiment records. * @param int $now Unix timestamp. * @return string[] */ public static function cookie_names_for( array $experiments, int $now ): array { $names = array(); foreach ( $experiments as $experiment ) { if ( ! is_array( $experiment ) ) { continue; } $id = (int) ( $experiment['id'] ?? 0 ); if ( $id < 1 ) { continue; } $start = isset( $experiment['start'] ) ? (int) $experiment['start'] : null; $end = isset( $experiment['end'] ) ? (int) $experiment['end'] : null; if ( ! Runtime\is_within_schedule( (string) ( $experiment['status'] ?? '' ), $start, $end, $now ) ) { continue; } $names[] = Runtime\assignment_cookie_name( $id ); } $names = array_values( array_unique( $names ) ); sort( $names ); return $names; } /** * Whether the cookie selects a variant that belongs to the experiment. * * @param string|null $cookie Raw cookie value. * @param int[] $variant_ids Variant IDs. * @return bool */ public static function assignment_matches( ?string $cookie, array $variant_ids ): bool { $parsed = Runtime\parse_assignment_cookie( $cookie ); if ( null === $parsed ) { return false; } return in_array( $parsed['variant_id'], array_map( 'intval', $variant_ids ), true ); } /** * Ask LiteSpeed to rewrite its rules with the current vary filter. * * @return bool */ private static function write_rules(): bool { if ( ! class_exists( '\LiteSpeed\Conf' ) || ! class_exists( '\LiteSpeed\Htaccess' ) || ! class_exists( '\LiteSpeed\Base' ) ) { return false; } $conf = \LiteSpeed\Conf::cls(); if ( ! $conf->has_conf( \LiteSpeed\Base::O_CACHE ) ) { return false; } try { $options = $conf->get_options(); if ( ! is_array( $options ) ) { return false; } \LiteSpeed\Htaccess::cls()->update( $options ); } catch ( \Throwable $error ) { Logger::exception( $error ); return false; } return true; } /** * Live experiment whose public path is this request, if any. * * @return array|null */ private static function experiment_for_request(): ?array { $uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( (string) $_SERVER['REQUEST_URI'] ) ) : ''; $path = Runtime\normalize_path( (string) wp_parse_url( $uri, PHP_URL_PATH ) ); foreach ( self::index_experiments() as $experiment ) { $experiment_path = (string) ( $experiment['path'] ?? '' ); if ( $experiment_path !== $path && $experiment_path . '/' !== $path ) { continue; } $names = self::cookie_names_for( array( $experiment ), time() ); if ( array() === $names ) { return null; } return $experiment; } return null; } /** * Variant IDs stored on an index record. * * @param array $experiment Experiment record. * @return int[] */ private static function variant_ids( array $experiment ): array { $ids = array(); if ( ! isset( $experiment['variants'] ) || ! is_array( $experiment['variants'] ) ) { return $ids; } foreach ( $experiment['variants'] as $variant ) { if ( is_array( $variant ) ) { $ids[] = (int) ( $variant['id'] ?? 0 ); } } return $ids; } /** * Experiments stored in the runtime index. * * @return array> */ private static function index_experiments(): array { $file = Runtime_Index::path(); if ( ! is_readable( $file ) ) { return array(); } $index = json_decode( Local_File::read( $file ), true ); if ( ! is_array( $index ) || ! isset( $index['experiments'] ) || ! is_array( $index['experiments'] ) ) { return array(); } $experiments = array(); foreach ( $index['experiments'] as $experiment ) { if ( is_array( $experiment ) ) { $experiments[] = $experiment; } } return $experiments; } /** * Read one cookie. * * @param string $name Cookie name. * @return string */ private static function cookie_value( string $name ): string { if ( ! isset( $_COOKIE[ $name ] ) ) { return ''; } return sanitize_text_field( wp_unslash( (string) $_COOKIE[ $name ] ) ); } /** * Preview requests must not be stored. * * @return bool */ private static function is_preview_request(): bool { $context = Experiment_Context::current(); if ( $context instanceof Experiment_Context && $context->is_preview() ) { return true; } return '' !== self::cookie_value( Runtime\PREVIEW_COOKIE ); } /** * Preview and reset query arguments stay dynamic. * * @return bool */ private static function has_plugin_query(): bool { $query = isset( $_SERVER['QUERY_STRING'] ) ? sanitize_text_field( wp_unslash( (string) $_SERVER['QUERY_STRING'] ) ) : ''; return 1 === preg_match( '/(^|&)wpab_/', $query ); } }