ABTESTKIT_CUSTOM_JAVASCRIPT_MAX_BYTES ) { return new WP_Error( 'abtestkit_custom_javascript_too_large', __( 'Custom JavaScript source is larger than the allowed limit.', 'abtestkit' ) ); } if ( strpos( $source, "\0" ) !== false ) { return new WP_Error( 'abtestkit_custom_javascript_null_byte', __( 'Custom JavaScript source cannot contain null bytes.', 'abtestkit' ) ); } if ( 1 !== preg_match( '//u', $source ) ) { return new WP_Error( 'abtestkit_custom_javascript_invalid_utf8', __( 'Custom JavaScript source must be valid UTF-8.', 'abtestkit' ) ); } return $source; } /** * Accept only opaque references generated by this subsystem. */ function abtestkit_custom_javascript_normalize_code_ref( $reference ): string { $reference = strtolower( trim( (string) $reference ) ); return preg_match( '/^js_[a-f0-9]{32}$/D', $reference ) ? $reference : ''; } /** * Accept the existing short test IDs without allowing them to shape output. */ function abtestkit_custom_javascript_normalize_test_id( $test_id ): string { $test_id = trim( (string) $test_id ); return preg_match( '/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/D', $test_id ) ? $test_id : ''; } /** * Accept only a lowercase SHA-256 digest. */ function abtestkit_custom_javascript_normalize_hash( $hash ): string { $hash = strtolower( trim( (string) $hash ) ); return preg_match( '/^[a-f0-9]{64}$/D', $hash ) ? $hash : ''; } /** * Build the non-autoloaded option name for one immutable code record. */ function abtestkit_custom_javascript_code_option_name( $reference ): string { $reference = abtestkit_custom_javascript_normalize_code_ref( $reference ); return $reference === '' ? '' : ABTESTKIT_CUSTOM_JAVASCRIPT_CODE_OPTION_PREFIX . $reference; } /** * Create a new immutable code record. * * Updates write a new record first. A later metadata phase can switch the test * reference and delete the old record only after the registry save succeeds. * * @return array|WP_Error Bounded metadata only; never the source. */ function abtestkit_custom_javascript_code_create( $source ) { $source = abtestkit_custom_javascript_validate_source( $source ); if ( is_wp_error( $source ) ) { return $source; } $hash = hash( 'sha256', $source ); $length = strlen( $source ); for ( $attempt = 0; $attempt < 5; $attempt++ ) { $reference = 'js_' . str_replace( '-', '', strtolower( wp_generate_uuid4() ) ); $reference = abtestkit_custom_javascript_normalize_code_ref( $reference ); if ( $reference === '' ) { continue; } $option_name = abtestkit_custom_javascript_code_option_name( $reference ); $record = [ 'source' => $source, 'hash' => $hash, 'length' => $length, 'created_at' => time(), ]; if ( add_option( $option_name, $record, '', false ) ) { return [ 'javascript_code_ref' => $reference, 'javascript_code_hash' => $hash, 'javascript_code_length' => $length, ]; } } return new WP_Error( 'abtestkit_custom_javascript_store_failed', __( 'Custom JavaScript source could not be saved.', 'abtestkit' ) ); } /** * Read and verify one immutable code record. * * @return array|WP_Error */ function abtestkit_custom_javascript_code_get( $reference, $expected_hash = '' ) { $reference = abtestkit_custom_javascript_normalize_code_ref( $reference ); $option_name = abtestkit_custom_javascript_code_option_name( $reference ); if ( $option_name === '' ) { return new WP_Error( 'abtestkit_custom_javascript_invalid_reference', __( 'Custom JavaScript source reference is invalid.', 'abtestkit' ) ); } $record = get_option( $option_name, null ); if ( ! is_array( $record ) || ! isset( $record['source'], $record['hash'], $record['length'] ) ) { return new WP_Error( 'abtestkit_custom_javascript_missing', __( 'Custom JavaScript source could not be found.', 'abtestkit' ) ); } $source = abtestkit_custom_javascript_validate_source( $record['source'] ); if ( is_wp_error( $source ) ) { return new WP_Error( 'abtestkit_custom_javascript_corrupt', __( 'Custom JavaScript source did not pass integrity checks.', 'abtestkit' ) ); } $stored_hash = abtestkit_custom_javascript_normalize_hash( $record['hash'] ); $actual_hash = hash( 'sha256', $source ); $length = strlen( $source ); if ( $stored_hash === '' || ! hash_equals( $stored_hash, $actual_hash ) || (int) $record['length'] !== $length ) { return new WP_Error( 'abtestkit_custom_javascript_integrity', __( 'Custom JavaScript source did not pass integrity checks.', 'abtestkit' ) ); } $raw_expected_hash = trim( (string) $expected_hash ); $expected_hash = $raw_expected_hash === '' ? '' : abtestkit_custom_javascript_normalize_hash( $raw_expected_hash ); if ( ( $raw_expected_hash !== '' && $expected_hash === '' ) || ( $expected_hash !== '' && ! hash_equals( $stored_hash, $expected_hash ) ) ) { return new WP_Error( 'abtestkit_custom_javascript_hash_mismatch', __( 'Custom JavaScript source does not match the requested version.', 'abtestkit' ) ); } return [ 'source' => $source, 'hash' => $stored_hash, 'length' => $length, 'created_at' => max( 0, (int) ( $record['created_at'] ?? 0 ) ), ]; } /** * Delete one code record. Missing records are already clean. */ function abtestkit_custom_javascript_code_delete( $reference ): bool { $option_name = abtestkit_custom_javascript_code_option_name( $reference ); if ( $option_name === '' ) { return false; } if ( null === get_option( $option_name, null ) ) { return true; } return delete_option( $option_name ); } /** * Delete bounded runtime health for one future JavaScript test. */ function abtestkit_custom_javascript_health_delete( $test_id ): void { $test_id = abtestkit_custom_javascript_normalize_test_id( $test_id ); if ( $test_id === '' ) { return; } $stored = get_option( ABTESTKIT_CUSTOM_JAVASCRIPT_HEALTH_OPTION, [] ); if ( ! is_array( $stored ) || ! array_key_exists( $test_id, $stored ) ) { return; } unset( $stored[ $test_id ] ); if ( empty( $stored ) ) { delete_option( ABTESTKIT_CUSTOM_JAVASCRIPT_HEALTH_OPTION ); return; } update_option( ABTESTKIT_CUSTOM_JAVASCRIPT_HEALTH_OPTION, $stored, false ); } /** * Read bounded operational health for one JavaScript test. */ function abtestkit_custom_javascript_health_get( $test_id ): array { $test_id = abtestkit_custom_javascript_normalize_test_id( $test_id ); if ( $test_id === '' ) { return []; } $stored = get_option( ABTESTKIT_CUSTOM_JAVASCRIPT_HEALTH_OPTION, [] ); $record = is_array( $stored ) && isset( $stored[ $test_id ] ) && is_array( $stored[ $test_id ] ) ? $stored[ $test_id ] : []; if ( empty( $record ) ) { return []; } return [ 'delivery_count' => min( 1000000, max( 0, (int) ( $record['delivery_count'] ?? 0 ) ) ), 'last_delivery_at' => max( 0, (int) ( $record['last_delivery_at'] ?? 0 ) ), 'complete_count' => min( 1000000, max( 0, (int) ( $record['complete_count'] ?? 0 ) ) ), 'failed_count' => min( 1000000, max( 0, (int) ( $record['failed_count'] ?? 0 ) ) ), 'last_runtime_at' => max( 0, (int) ( $record['last_runtime_at'] ?? 0 ) ), 'last_status' => in_array( (string) ( $record['last_status'] ?? '' ), [ 'complete', 'failed' ], true ) ? (string) $record['last_status'] : '', ]; } /** * Record delivery/runtime counts only. No source, exception text or visitor * identifier is stored. */ function abtestkit_custom_javascript_health_record( $test_id, $event ): void { $test_id = abtestkit_custom_javascript_normalize_test_id( $test_id ); $event = sanitize_key( (string) $event ); if ( $test_id === '' || ! in_array( $event, [ 'delivery', 'complete', 'failed' ], true ) ) { return; } $stored = get_option( ABTESTKIT_CUSTOM_JAVASCRIPT_HEALTH_OPTION, [] ); if ( ! is_array( $stored ) ) { $stored = []; } $record = abtestkit_custom_javascript_health_get( $test_id ); $now = time(); if ( $event === 'delivery' ) { if ( (int) ( $record['last_delivery_at'] ?? 0 ) >= ( $now - MINUTE_IN_SECONDS ) ) { return; } $record['delivery_count'] = min( 1000000, (int) ( $record['delivery_count'] ?? 0 ) + 1 ); $record['last_delivery_at'] = $now; } else { $runtime_throttle = $event === 'failed' ? 10 : MINUTE_IN_SECONDS; if ( (string) ( $record['last_status'] ?? '' ) === $event && (int) ( $record['last_runtime_at'] ?? 0 ) >= ( $now - $runtime_throttle ) ) { return; } $count_key = $event . '_count'; $record[ $count_key ] = min( 1000000, (int) ( $record[ $count_key ] ?? 0 ) + 1 ); $record['last_runtime_at'] = $now; $record['last_status'] = $event; } $stored[ $test_id ] = $record; update_option( ABTESTKIT_CUSTOM_JAVASCRIPT_HEALTH_OPTION, $stored, false ); } /** * Remove JavaScript-owned state after its registry record has been deleted. * * Existing test kinds are explicit no-ops. */ function abtestkit_custom_javascript_cleanup_for_test( array $test ): void { if ( (string) ( $test['kind'] ?? '' ) !== 'custom_javascript' ) { return; } if ( ! empty( $test['javascript_code_ref'] ) ) { abtestkit_custom_javascript_code_delete( $test['javascript_code_ref'] ); } if ( ! empty( $test['id'] ) ) { abtestkit_custom_javascript_health_delete( $test['id'] ); } } /** * Wrap source for external delivery without using a string evaluator. */ function abtestkit_custom_javascript_wrap_source( $test_id, $source, $preview = false, $runtime_report_url = '' ) { $test_id = abtestkit_custom_javascript_normalize_test_id( $test_id ); $source = abtestkit_custom_javascript_validate_source( $source ); if ( $test_id === '' || is_wp_error( $source ) ) { return new WP_Error( 'abtestkit_custom_javascript_wrapper_invalid', __( 'Custom JavaScript could not be prepared for delivery.', 'abtestkit' ) ); } $encoded_test_id = wp_json_encode( $test_id ); $encoded_report_url = wp_json_encode( esc_url_raw( (string) $runtime_report_url ) ); return "(function (window, document) {\n" . " var testId = {$encoded_test_id};\n" . " var reportUrl = {$encoded_report_url};\n" . " var registryKey = '__abtestkitCustomJavascriptRuns';\n" . " var registry = window[registryKey];\n" . " if (!registry || typeof registry !== 'object') {\n" . " registry = Object.create(null);\n" . " try {\n" . " Object.defineProperty(window, registryKey, { value: registry, configurable: false, writable: false });\n" . " } catch (_) {\n" . " window[registryKey] = registry;\n" . " }\n" . " }\n" . " var run = function () {\n" . " if (registry[testId]) {\n" . " return;\n" . " }\n" . " registry[testId] = 'running';\n" . " var report = function (status) {\n" . " if (!reportUrl || typeof window.fetch !== 'function') {\n" . " return;\n" . " }\n" . " try {\n" . " window.fetch(reportUrl, {\n" . " method: 'POST',\n" . " credentials: 'same-origin',\n" . " keepalive: true,\n" . " headers: { 'Content-Type': 'application/json' },\n" . " body: JSON.stringify({ test_id: testId, status: status })\n" . " }).catch(function () {});\n" . " } catch (_) {}\n" . " };\n" . " try {\n" . " (function () {\n" . $source . "\n" . " }).call(window);\n" . " registry[testId] = 'complete';\n" . " report('complete');\n" . ( $preview ? " try {\n" . " if (window.parent && window.parent !== window) {\n" . " window.parent.postMessage({ type: 'abtestkit:custom-javascript-preview', previewId: testId, status: 'complete' }, '*');\n" . " }\n" . " } catch (_) {}\n" : '' ) . " } catch (_) {\n" . " registry[testId] = 'failed';\n" . " report('failed');\n" . " try {\n" . " window.console.error('[abtestkit] Custom JavaScript failed', { testId: testId, category: 'synchronous_exception' });\n" . " } catch (_) {}\n" . " try {\n" . " window.dispatchEvent(new CustomEvent('abtestkit:custom-javascript-error', { detail: { testId: testId, category: 'synchronous_exception' } }));\n" . " } catch (_) {}\n" . ( $preview ? " try {\n" . " if (window.parent && window.parent !== window) {\n" . " window.parent.postMessage({ type: 'abtestkit:custom-javascript-preview', previewId: testId, status: 'failed' }, '*');\n" . " }\n" . " } catch (_) {}\n" : '' ) . " }\n" . " };\n" . " if (document.readyState === 'loading') {\n" . " document.addEventListener('DOMContentLoaded', run, { once: true });\n" . " } else {\n" . " run();\n" . " }\n" . "})(window, document);\n"; } /** * Accept only opaque preview tokens generated by this subsystem. */ function abtestkit_custom_javascript_normalize_preview_token( $token ): string { $token = strtolower( trim( (string) $token ) ); return preg_match( '/^jsp_[a-f0-9]{32}$/D', $token ) ? $token : ''; } /** * Build the transient key for one short-lived unsaved preview. */ function abtestkit_custom_javascript_preview_transient_key( $token ): string { $token = abtestkit_custom_javascript_normalize_preview_token( $token ); return $token === '' ? '' : ABTESTKIT_CUSTOM_JAVASCRIPT_PREVIEW_TRANSIENT_PREFIX . $token; } /** * Read and integrity-check an administrator-owned preview record. * * @return array|WP_Error */ function abtestkit_custom_javascript_preview_get( $token, $requested_hash = '', $require_current_user = true ) { $token = abtestkit_custom_javascript_normalize_preview_token( $token ); $transient_key = abtestkit_custom_javascript_preview_transient_key( $token ); if ( $transient_key === '' || ( $require_current_user && ( ! is_user_logged_in() || ! current_user_can( 'manage_options' ) ) ) ) { return new WP_Error( 'abtestkit_custom_javascript_preview_forbidden', __( 'Custom JavaScript preview is not available.', 'abtestkit' ) ); } $record = get_transient( $transient_key ); if ( ! is_array( $record ) || (int) ( $record['user_id'] ?? 0 ) < 1 || ( $require_current_user && (int) $record['user_id'] !== get_current_user_id() ) || (int) ( $record['control_id'] ?? 0 ) < 1 || ! in_array( (string) ( $record['scope'] ?? '' ), [ 'page', 'post', 'product' ], true ) ) { return new WP_Error( 'abtestkit_custom_javascript_preview_missing', __( 'Custom JavaScript preview has expired.', 'abtestkit' ) ); } $source = abtestkit_custom_javascript_validate_source( $record['source'] ?? null ); $hash = abtestkit_custom_javascript_normalize_hash( $record['hash'] ?? '' ); if ( is_wp_error( $source ) || $hash === '' || ! hash_equals( $hash, hash( 'sha256', $source ) ) || (int) ( $record['length'] ?? -1 ) !== strlen( $source ) ) { return new WP_Error( 'abtestkit_custom_javascript_preview_integrity', __( 'Custom JavaScript preview did not pass integrity checks.', 'abtestkit' ) ); } $raw_requested_hash = trim( (string) $requested_hash ); $requested_hash = $raw_requested_hash === '' ? '' : abtestkit_custom_javascript_normalize_hash( $raw_requested_hash ); if ( ( $raw_requested_hash !== '' && $requested_hash === '' ) || ( $requested_hash !== '' && ! hash_equals( $hash, $requested_hash ) ) ) { return new WP_Error( 'abtestkit_custom_javascript_preview_hash', __( 'Custom JavaScript preview version is invalid.', 'abtestkit' ) ); } $record['source'] = $source; $record['hash'] = $hash; $record['token'] = $token; return $record; } /** * Create one short-lived preview token for an unsaved administrator draft. */ function abtestkit_custom_javascript_create_preview( WP_REST_Request $request ) { $control_id = absint( $request->get_param( 'control_id' ) ); $scope = sanitize_key( (string) $request->get_param( 'scope' ) ); $source = abtestkit_custom_javascript_validate_source( $request->get_param( 'source' ) ); $post = $control_id ? get_post( $control_id ) : null; if ( ! $post || ! in_array( $scope, [ 'page', 'post', 'product' ], true ) || (string) $post->post_type !== $scope ) { return new WP_Error( 'abtestkit_custom_javascript_preview_target', __( 'Choose a valid page, post or product for this preview.', 'abtestkit' ), [ 'status' => 400 ] ); } if ( is_wp_error( $source ) ) { $source->add_data( [ 'status' => 400 ] ); return $source; } $hash = hash( 'sha256', $source ); $length = strlen( $source ); for ( $attempt = 0; $attempt < 5; $attempt++ ) { $token = 'jsp_' . str_replace( '-', '', strtolower( wp_generate_uuid4() ) ); $token = abtestkit_custom_javascript_normalize_preview_token( $token ); if ( $token === '' ) { continue; } $stored = set_transient( abtestkit_custom_javascript_preview_transient_key( $token ), [ 'user_id' => get_current_user_id(), 'control_id' => $control_id, 'scope' => $scope, 'source' => $source, 'hash' => $hash, 'length' => $length, 'created_at' => time(), ], ABTESTKIT_CUSTOM_JAVASCRIPT_PREVIEW_TTL ); if ( $stored ) { return rest_ensure_response( [ 'ok' => true, 'token' => $token, 'hash' => $hash, 'preview_id' => 'preview-' . substr( $token, 4 ), 'expires_in' => ABTESTKIT_CUSTOM_JAVASCRIPT_PREVIEW_TTL, ] ); } } return new WP_Error( 'abtestkit_custom_javascript_preview_store', __( 'Custom JavaScript preview could not be prepared.', 'abtestkit' ), [ 'status' => 500 ] ); } /** * Return the preview record only for its exact authenticated B control view. * * @return array Empty when the current request is not an eligible preview. */ function abtestkit_custom_javascript_preview_assignment(): array { if ( is_admin() || ! is_user_logged_in() || ! current_user_can( 'manage_options' ) ) { return []; } // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Opaque short-lived token is bound to the current administrator. $preview_flag = isset( $_GET['abtestkit_preview'] ) ? sanitize_text_field( wp_unslash( (string) $_GET['abtestkit_preview'] ) ) : ''; $force_variant = isset( $_GET['abtestkit_force'] ) ? strtoupper( sanitize_text_field( wp_unslash( (string) $_GET['abtestkit_force'] ) ) ) : ''; $token = isset( $_GET['abtestkit_custom_javascript_preview'] ) ? sanitize_text_field( wp_unslash( (string) $_GET['abtestkit_custom_javascript_preview'] ) ) : ''; // phpcs:enable WordPress.Security.NonceVerification.Recommended if ( $preview_flag !== '1' || $force_variant !== 'B' || $token === '' ) { return []; } $record = abtestkit_custom_javascript_preview_get( $token ); if ( is_wp_error( $record ) || (int) get_queried_object_id() !== (int) $record['control_id'] || (string) get_post_type( (int) $record['control_id'] ) !== (string) $record['scope'] ) { return []; } return $record; } /** * Prevent an unsaved administrator preview and its bearer token being cached * or copied into a cross-origin referrer. */ function abtestkit_custom_javascript_protect_preview_request(): void { $record = abtestkit_custom_javascript_preview_assignment(); if ( empty( $record ) ) { return; } // Canonical interoperability constants used by common cache/minify tools. // phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound if ( ! defined( 'DONOTCACHEPAGE' ) ) { define( 'DONOTCACHEPAGE', true ); } if ( ! defined( 'DONOTCACHEDB' ) ) { define( 'DONOTCACHEDB', true ); } if ( ! defined( 'DONOTCACHEOBJECT' ) ) { define( 'DONOTCACHEOBJECT', true ); } if ( ! defined( 'DONOTMINIFY' ) ) { define( 'DONOTMINIFY', true ); } // phpcs:enable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound if ( ! headers_sent() ) { nocache_headers(); header( 'Cache-Control: private, no-store, no-cache, must-revalidate, max-age=0', true ); header( 'Referrer-Policy: no-referrer', true ); } } /** * Register the draft as a same-origin external script on its B preview only. */ function abtestkit_custom_javascript_enqueue_preview(): void { $record = abtestkit_custom_javascript_preview_assignment(); if ( empty( $record ) ) { return; } $handle = 'abtestkit-custom-javascript-preview'; $src = add_query_arg( [ 'abtestkit_custom_javascript_preview_script' => $record['token'], 'v' => $record['hash'], ], home_url( '/' ) ); wp_enqueue_script( $handle, $src, [], null, true ); } /** * Add common optimiser opt-out hints without weakening CSP. */ function abtestkit_custom_javascript_preview_script_tag( $tag, $handle ) { if ( 'abtestkit-custom-javascript-preview' !== $handle && strpos( (string) $handle, 'abtestkit-custom-javascript-live-' ) !== 0 ) { return $tag; } $marker = 'abtestkit-custom-javascript-preview' === $handle ? ' data-abtestkit-custom-javascript-preview="1"' : ' data-abtestkit-custom-javascript-live="1"'; return str_replace( ' src=', $marker . ' data-no-optimize="1" data-cfasync="false" data-pagespeed-no-defer="1" src=', $tag ); } /** * Serve an authorised preview script. Invalid requests reveal no details. */ function abtestkit_custom_javascript_maybe_serve_preview(): void { // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Opaque 128-bit bearer token and content hash are both required. $token = isset( $_GET['abtestkit_custom_javascript_preview_script'] ) ? sanitize_text_field( wp_unslash( (string) $_GET['abtestkit_custom_javascript_preview_script'] ) ) : ''; $hash = isset( $_GET['v'] ) ? sanitize_text_field( wp_unslash( (string) $_GET['v'] ) ) : ''; // phpcs:enable WordPress.Security.NonceVerification.Recommended if ( $token === '' ) { return; } // The iframe deliberately has an opaque sandbox origin, so its external // subresource request may omit the WordPress login cookie. The 128-bit // bearer token and content hash are short-lived; token issuance and the // embedding page both remain bound to the current administrator. $record = abtestkit_custom_javascript_preview_get( $token, $hash, false ); if ( is_wp_error( $record ) ) { status_header( 404 ); nocache_headers(); exit; } $wrapped = abtestkit_custom_javascript_wrap_source( 'preview-' . substr( $record['token'], 4 ), $record['source'], true ); if ( is_wp_error( $wrapped ) ) { status_header( 404 ); nocache_headers(); exit; } if ( ! headers_sent() ) { header( 'Content-Type: application/javascript; charset=UTF-8' ); header( 'X-Content-Type-Options: nosniff' ); header( 'Cache-Control: private, no-store, no-cache, must-revalidate, max-age=0' ); header( 'Referrer-Policy: no-referrer' ); } echo $wrapped; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JavaScript response, not HTML. exit; } /** * Resolve one running JavaScript record for external delivery. * * @return array|WP_Error */ function abtestkit_custom_javascript_resolve_delivery( $test_id, $requested_hash, $administrator_preview = false ) { $test_id = abtestkit_custom_javascript_normalize_test_id( $test_id ); $requested_hash = abtestkit_custom_javascript_normalize_hash( $requested_hash ); if ( $test_id === '' || $requested_hash === '' || ! function_exists( 'abtestkit_pt_get' ) ) { return new WP_Error( 'abtestkit_custom_javascript_delivery_invalid', __( 'Custom JavaScript delivery request is invalid.', 'abtestkit' ) ); } $test = abtestkit_pt_get( $test_id ); $status = is_array( $test ) ? (string) ( $test['status'] ?? '' ) : ''; $winner = is_array( $test ) ? strtoupper( (string) ( $test['winner'] ?? '' ) ) : ''; $is_live_delivery = $status === 'running'; $is_completed_preview = ( $administrator_preview && in_array( $status, [ 'winner', 'complete' ], true ) && $winner === 'B' ); if ( ! is_array( $test ) || (string) ( $test['kind'] ?? '' ) !== 'custom_javascript' || ( ! $is_live_delivery && ! $is_completed_preview ) || (string) ( $test['javascript_run_timing'] ?? '' ) !== 'dom_ready' || (string) ( $test['javascript_ajax_policy'] ?? '' ) !== 'once_per_document' ) { return new WP_Error( 'abtestkit_custom_javascript_delivery_ineligible', __( 'Custom JavaScript is not eligible for delivery.', 'abtestkit' ) ); } $stored_hash = abtestkit_custom_javascript_normalize_hash( $test['javascript_code_hash'] ?? '' ); if ( $stored_hash === '' || ! hash_equals( $stored_hash, $requested_hash ) ) { return new WP_Error( 'abtestkit_custom_javascript_delivery_hash', __( 'Custom JavaScript delivery version is invalid.', 'abtestkit' ) ); } $record = abtestkit_custom_javascript_code_get( $test['javascript_code_ref'] ?? '', $stored_hash ); if ( is_wp_error( $record ) ) { return $record; } return [ 'test_id' => $test_id, 'source' => $record['source'], 'hash' => $record['hash'], 'length' => $record['length'], ]; } /** * Build a content-addressed URL for later WordPress script registration. */ function abtestkit_custom_javascript_delivery_url( array $test, $administrator_preview = false ): string { $test_id = abtestkit_custom_javascript_normalize_test_id( $test['id'] ?? '' ); $hash = abtestkit_custom_javascript_normalize_hash( $test['javascript_code_hash'] ?? '' ); if ( $test_id === '' || $hash === '' ) { return ''; } $args = [ 'abtestkit_custom_javascript' => $test_id, 'v' => $hash, ]; if ( $administrator_preview && is_user_logged_in() && current_user_can( 'manage_options' ) ) { $args['preview_nonce'] = wp_create_nonce( 'abtestkit_custom_javascript_preview_' . $test_id ); } return add_query_arg( $args, home_url( '/' ) ); } /** * Read the existing assignment authority without creating or changing it. */ function abtestkit_custom_javascript_request_is_assigned_b( $test_id ): bool { $test_id = abtestkit_custom_javascript_normalize_test_id( $test_id ); if ( $test_id === '' || ! function_exists( 'abtestkit_safe_get_cookie_value' ) || ! function_exists( 'abtestkit_request_is_real_https' ) || ! abtestkit_request_is_real_https() ) { return false; } if ( function_exists( 'abtestkit_is_exempt_viewer' ) && abtestkit_is_exempt_viewer() ) { return false; } return 'B' === abtestkit_safe_get_cookie_value( 'abtestkit_pt_' . $test_id ); } /** * Enqueue the external script only after the central assignment path has made * this exact control-page request eligible for Version B. */ function abtestkit_custom_javascript_enqueue_live(): void { if ( ! empty( abtestkit_custom_javascript_preview_assignment() ) ) { return; } $assignment = $GLOBALS['abtestkit_current_pt_assignment'] ?? []; $test = is_array( $assignment ) && isset( $assignment['test'] ) && is_array( $assignment['test'] ) ? $assignment['test'] : []; $administrator_preview = ( ! empty( $assignment['forced_b'] ) && is_user_logged_in() && current_user_can( 'manage_options' ) ); $status = (string) ( $test['status'] ?? '' ); $status_is_eligible = $status === 'running' || ( $administrator_preview && in_array( $status, [ 'winner', 'complete' ], true ) && strtoupper( (string) ( $test['winner'] ?? '' ) ) === 'B' ); if ( empty( $test ) || (string) ( $assignment['variant'] ?? '' ) !== 'B' || ( ! empty( $assignment['no_track'] ) && ! $administrator_preview ) || (string) ( $test['kind'] ?? '' ) !== 'custom_javascript' || ! $status_is_eligible || (int) ( $assignment['post_id'] ?? 0 ) !== (int) ( $test['control_id'] ?? 0 ) ) { return; } $src = abtestkit_custom_javascript_delivery_url( $test, $administrator_preview ); if ( $src === '' ) { return; } wp_enqueue_script( 'abtestkit-custom-javascript-live-' . sanitize_key( (string) $test['id'] ), $src, [], null, true ); } /** * Serve the external delivery seam to an existing, non-exempt B assignment. */ function abtestkit_custom_javascript_maybe_serve(): void { // phpcs:disable WordPress.Security.NonceVerification.Recommended $test_id = isset( $_GET['abtestkit_custom_javascript'] ) ? sanitize_text_field( wp_unslash( (string) $_GET['abtestkit_custom_javascript'] ) ) : ''; $hash = isset( $_GET['v'] ) ? sanitize_text_field( wp_unslash( (string) $_GET['v'] ) ) : ''; $preview_nonce = isset( $_GET['preview_nonce'] ) ? sanitize_text_field( wp_unslash( (string) $_GET['preview_nonce'] ) ) : ''; // phpcs:enable WordPress.Security.NonceVerification.Recommended if ( $test_id === '' ) { return; } $test_id = abtestkit_custom_javascript_normalize_test_id( $test_id ); $administrator_preview = ( $test_id !== '' && $preview_nonce !== '' && is_user_logged_in() && current_user_can( 'manage_options' ) && wp_verify_nonce( $preview_nonce, 'abtestkit_custom_javascript_preview_' . $test_id ) ); if ( $test_id === '' || ( ! $administrator_preview && ! abtestkit_custom_javascript_request_is_assigned_b( $test_id ) ) ) { status_header( 404 ); nocache_headers(); exit; } $delivery = abtestkit_custom_javascript_resolve_delivery( $test_id, $hash, $administrator_preview ); if ( is_wp_error( $delivery ) ) { status_header( 404 ); nocache_headers(); exit; } $wrapped = abtestkit_custom_javascript_wrap_source( $delivery['test_id'], $delivery['source'], false, $administrator_preview ? '' : rest_url( 'abtestkit/v1/pt/custom-javascript-runtime-health' ) ); if ( is_wp_error( $wrapped ) ) { status_header( 404 ); nocache_headers(); exit; } if ( ! headers_sent() ) { header( 'Content-Type: application/javascript; charset=UTF-8' ); header( 'X-Content-Type-Options: nosniff' ); header( 'Cache-Control: private, no-store, no-cache, must-revalidate, max-age=0' ); } if ( ! $administrator_preview ) { abtestkit_custom_javascript_health_record( $delivery['test_id'], 'delivery' ); } echo $wrapped; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JavaScript response, not HTML. exit; } /** * Store a bounded runtime completion/failure signal from an eligible B request. */ function abtestkit_custom_javascript_record_runtime_health( WP_REST_Request $request ) { $test_id = abtestkit_custom_javascript_normalize_test_id( $request->get_param( 'test_id' ) ); $status = sanitize_key( (string) $request->get_param( 'status' ) ); $origin = isset( $_SERVER['HTTP_ORIGIN'] ) ? esc_url_raw( wp_unslash( (string) $_SERVER['HTTP_ORIGIN'] ) ) : ''; $origin_scheme = strtolower( (string) wp_parse_url( $origin, PHP_URL_SCHEME ) ); $origin_host = strtolower( (string) wp_parse_url( $origin, PHP_URL_HOST ) ); $origin_port = (int) wp_parse_url( $origin, PHP_URL_PORT ); $home_scheme = strtolower( (string) wp_parse_url( home_url( '/' ), PHP_URL_SCHEME ) ); $home_host = strtolower( (string) wp_parse_url( home_url( '/' ), PHP_URL_HOST ) ); $home_port = (int) wp_parse_url( home_url( '/' ), PHP_URL_PORT ); if ( $origin_port <= 0 ) { $origin_port = $origin_scheme === 'https' ? 443 : 80; } if ( $home_port <= 0 ) { $home_port = $home_scheme === 'https' ? 443 : 80; } if ( $test_id === '' || ! in_array( $status, [ 'complete', 'failed' ], true ) || $origin === '' || $origin_scheme !== $home_scheme || $origin_host !== $home_host || $origin_port !== $home_port || ! abtestkit_custom_javascript_request_is_assigned_b( $test_id ) ) { return new WP_Error( 'abtestkit_custom_javascript_runtime_forbidden', __( 'Custom JavaScript runtime health is not available.', 'abtestkit' ), [ 'status' => 403 ] ); } $test = function_exists( 'abtestkit_pt_get' ) ? abtestkit_pt_get( $test_id ) : null; $delivery = abtestkit_custom_javascript_resolve_delivery( $test_id, is_array( $test ) ? (string) ( $test['javascript_code_hash'] ?? '' ) : '' ); if ( is_wp_error( $delivery ) ) { return new WP_Error( 'abtestkit_custom_javascript_runtime_ineligible', __( 'Custom JavaScript runtime health is not available.', 'abtestkit' ), [ 'status' => 403 ] ); } abtestkit_custom_javascript_health_record( $test_id, $status ); return rest_ensure_response( [ 'ok' => true ] ); } /** * Return a completed Version B source only when an administrator explicitly * requests the permanent-implementation handoff from the performance screen. */ function abtestkit_custom_javascript_get_winner_source( WP_REST_Request $request ) { $test_id = abtestkit_custom_javascript_normalize_test_id( $request->get_param( 'test_id' ) ); $test = $test_id !== '' && function_exists( 'abtestkit_pt_get' ) ? abtestkit_pt_get( $test_id ) : null; if ( ! is_array( $test ) || (string) ( $test['kind'] ?? '' ) !== 'custom_javascript' || (string) ( $test['status'] ?? '' ) !== 'complete' || strtoupper( (string) ( $test['winner'] ?? '' ) ) !== 'B' ) { return new WP_Error( 'abtestkit_custom_javascript_winner_unavailable', __( 'The completed JavaScript winner is not available.', 'abtestkit' ), [ 'status' => 404 ] ); } $snapshot = isset( $test['snapshot_versions']['B'] ) && is_array( $test['snapshot_versions']['B'] ) ? $test['snapshot_versions']['B'] : []; $source_record = (string) ( $snapshot['kind'] ?? '' ) === 'custom_javascript' ? array_merge( $test, $snapshot ) : $test; $record = abtestkit_custom_javascript_code_get( $source_record['javascript_code_ref'] ?? '', $source_record['javascript_code_hash'] ?? '' ); if ( is_wp_error( $record ) ) { return $record; } return rest_ensure_response( [ 'ok' => true, 'source' => $record['source'], 'length' => $record['length'], ] ); } /** * Register preview and live delivery hooks. */ function abtestkit_custom_javascript_boot(): void { add_action( 'rest_api_init', 'abtestkit_custom_javascript_register_preview_route' ); add_action( 'template_redirect', 'abtestkit_custom_javascript_maybe_serve_preview', 0 ); add_action( 'template_redirect', 'abtestkit_custom_javascript_protect_preview_request', 0 ); add_action( 'wp_enqueue_scripts', 'abtestkit_custom_javascript_enqueue_preview', 999 ); add_action( 'wp_enqueue_scripts', 'abtestkit_custom_javascript_enqueue_live', 1000 ); add_filter( 'script_loader_tag', 'abtestkit_custom_javascript_preview_script_tag', 10, 2 ); add_action( 'template_redirect', 'abtestkit_custom_javascript_maybe_serve', 1 ); } /** * Register the nonce- and capability-protected preview-token route. */ function abtestkit_custom_javascript_register_preview_route(): void { register_rest_route( 'abtestkit/v1', '/pt/custom-javascript-preview', [ 'methods' => 'POST', 'permission_callback' => 'abtestkit_rest_permission', 'callback' => 'abtestkit_custom_javascript_create_preview', ] ); register_rest_route( 'abtestkit/v1', '/pt/custom-javascript-runtime-health', [ 'methods' => 'POST', 'permission_callback' => '__return_true', 'callback' => 'abtestkit_custom_javascript_record_runtime_health', ] ); register_rest_route( 'abtestkit/v1', '/pt/custom-javascript-winner-source', [ 'methods' => 'GET', 'permission_callback' => 'abtestkit_rest_permission', 'callback' => 'abtestkit_custom_javascript_get_winner_source', ] ); } abtestkit_custom_javascript_boot();