tracking ) { $this->tracking = new Tracking_Service( new Experiment_Repository(), new Goal_Repository(), new Bot_Detector() ); } return $this->tracking; } /** * Boot the plugin. */ public function boot(): void { try { Activator::install_loader(); ( new Migrator() )->maybe_upgrade(); $this->ensure_settings(); $this->ensure_capability(); $this->ensure_cron(); $experiments = new Experiment_Repository(); $variants = new Variant_Repository(); $goals = new Goal_Repository(); $history = new History_Repository(); $themes = new Theme_Resolver( get_theme_root() ); $index = new Runtime_Index( $experiments, $variants, $goals ); if ( ! is_readable( Runtime_Index::path() ) ) { $index->rebuild(); } $service = new Experiment_Service( $experiments, $variants, $goals, $index, new Traffic_Allocator(), new Dependency_Validator(), $themes ); $this->experiments = $service; $this->tracking = new Tracking_Service( $experiments, $goals, new Bot_Detector() ); $stats = new Stats_Service( new Aggregator( $goals ), new Winner_Detector(), $experiments, $variants ); $applier = new Winner_Applier( $experiments, $variants, $history ); $transfer = new Config_Transfer( $service ); $compatibility = new Compatibility_Checker(); $compatibility->boot(); ( new Post_Duplicator() )->register(); ( new Frontend_Runtime( $themes ) )->register(); if ( ! is_admin() ) { ( new Content_Override( $variants ) )->register(); ( new Tracking_View() )->register(); } ( new Cache_Manager() )->register(); ( new Seo_Guard() )->register(); ( new Privacy_Service() )->register(); ( new Rest_Controller( $service, $experiments, $history, $applier, $stats, $this->tracking, $transfer, $themes, $compatibility ) )->register(); if ( is_admin() ) { ( new Admin_Page() )->register(); ( new Notices( $experiments, $goals ) )->register(); ( new Csv_Export() )->register(); } ( new Admin_Bar() )->register(); add_action( 'wpab_cron_tick', array( $this, 'cron' ) ); add_action( 'init', array( $this, 'handle_preview' ), 1 ); add_action( 'save_post', array( $this, 'refresh_index_for_post' ), 20, 1 ); } catch ( \Throwable $error ) { Logger::exception( $error ); } } /** * Cron: schedules, aggregation, retention. */ public function cron(): void { try { if ( $this->experiments instanceof Experiment_Service ) { $this->experiments->sync_schedules(); } $goals = new Goal_Repository(); ( new Aggregator( $goals ) )->run( 2000 ); ( new Privacy_Service() )->prune(); } catch ( \Throwable $error ) { Logger::exception( $error ); } } /** * Authorized preview and assignment reset. Query parameters do nothing for other visitors. */ public function handle_preview(): void { $can = is_user_logged_in() && ( current_user_can( Settings::capability() ) || current_user_can( 'manage_options' ) ); if ( ! $can ) { return; } if ( isset( $_GET['wpab_preview'] ) ) { $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( (string) $_GET['_wpnonce'] ) ) : ''; if ( ! wp_verify_nonce( $nonce, 'wpab_preview' ) ) { wp_die( esc_html__( 'Invalid preview request.', 'jm-split-tester' ) ); } $raw = sanitize_text_field( wp_unslash( (string) $_GET['wpab_preview'] ) ); if ( preg_match( '/^(\d+):(\d+)$/', $raw, $matches ) ) { $token = \WPAB\Runtime\sign_preview( (int) $matches[1], (int) $matches[2], time() + 900, \WPAB\Runtime\runtime_secret() ); setcookie( \WPAB\Runtime\PREVIEW_COOKIE, $token, array( 'expires' => time() + 900, 'path' => '/', 'secure' => is_ssl(), 'httponly' => true, 'samesite' => 'Lax', ) ); } wp_safe_redirect( remove_query_arg( 'wpab_preview' ) ); exit; } if ( isset( $_GET['wpab_reset'] ) ) { $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( (string) $_GET['_wpnonce'] ) ) : ''; if ( ! wp_verify_nonce( $nonce, 'wpab_reset' ) ) { wp_die( esc_html__( 'Invalid reset request.', 'jm-split-tester' ) ); } $names = array( \WPAB\Runtime\VISITOR_COOKIE, \WPAB\Runtime\PREVIEW_COOKIE ); $experiment = absint( wp_unslash( $_GET['wpab_reset'] ) ); if ( $experiment > 0 ) { $names[] = \WPAB\Runtime\assignment_cookie_name( $experiment ); } foreach ( $names as $name ) { setcookie( $name, '', array( 'expires' => time() - 3600, 'path' => '/', 'secure' => is_ssl(), 'samesite' => 'Lax', ) ); } wp_safe_redirect( remove_query_arg( 'wpab_reset' ) ); exit; } } /** * Keep the runtime path index aligned with permalink changes. * * @param int $post_id Post ID. */ public function refresh_index_for_post( int $post_id ): void { if ( null === $this->experiments ) { return; } foreach ( ( new Experiment_Repository() )->live_candidates() as $experiment ) { if ( (int) $experiment['post_id'] === $post_id ) { ( new Runtime_Index( new Experiment_Repository(), new Variant_Repository(), new Goal_Repository() ) )->rebuild(); return; } } } /** * Default option. */ private function ensure_settings(): void { if ( false === get_option( Settings::OPTION, false ) ) { add_option( Settings::OPTION, Settings::defaults(), '', false ); } } /** * Grant the capability to administrators. */ private function ensure_capability(): void { $role = get_role( 'administrator' ); if ( $role instanceof \WP_Role && ! $role->has_cap( Settings::capability() ) ) { $role->add_cap( Settings::capability() ); } } /** * Five-minute cron for status flips, aggregation, and retention. */ private function ensure_cron(): void { add_filter( 'cron_schedules', static function ( array $schedules ): array { $schedules['wpab_fifteen_minutes'] = array( 'interval' => 15 * MINUTE_IN_SECONDS, 'display' => __( 'Every fifteen minutes', 'jm-split-tester' ), ); return $schedules; } ); $scheduled = wp_get_scheduled_event( 'wpab_cron_tick' ); if ( ! $scheduled instanceof \stdClass || 'wpab_fifteen_minutes' !== $scheduled->schedule ) { wp_clear_scheduled_hook( 'wpab_cron_tick' ); wp_schedule_event( time() + MINUTE_IN_SECONDS, 'wpab_fifteen_minutes', 'wpab_cron_tick' ); } } }