ownership = $ownership; $this->placement = $placement; $this->releases = $releases; $this->storage = $storage; $this->lifecycle = $lifecycle; $this->diagnostics = $diagnostics; $this->management_diagnostics = $management_diagnostics; } public function register() { add_action( 'rest_api_init', array( $this, 'routes' ) ); } public function routes() { // Public signed callback: bounded POST, replay-protected and verified in the handler. register_rest_route( 'achla/v1', '/ownership-callback', array( 'methods' => 'POST', 'callback' => array( $this, 'ownership_callback' ), 'permission_callback' => '__return_true', ) ); foreach ( array( '/connect' => 'connect', '/resume' => 'resume', '/reconnect' => 'reconnect', '/disconnect' => 'disconnect', '/placement' => 'save_placement', '/selector/test' => 'test_selector', '/release/refresh' => 'refresh_release', ) as $route => $callback ) { register_rest_route( 'achla/v1', $route, array( 'methods' => 'POST', 'callback' => array( $this, $callback ), 'permission_callback' => array( $this, 'can_manage' ), ) ); } register_rest_route( 'achla/v1', '/status', array( 'methods' => 'GET', 'callback' => array( $this, 'status' ), 'permission_callback' => array( $this, 'can_manage' ), ) ); register_rest_route( 'achla/v1', '/release-decision', array( 'methods' => 'GET', 'callback' => array( $this, 'release_decision' ), 'permission_callback' => '__return_true', ) ); // Visitor-facing mutation: nonce authorization here; bounded same-origin checks in the handler. register_rest_route( 'achla/v1', '/runtime-diagnostic', array( 'methods' => 'POST', 'callback' => array( $this, 'runtime_diagnostic' ), 'permission_callback' => array( $this, 'can_report_diagnostic' ), ) ); } /** @return bool|\WP_Error */ public function can_manage( $request ) { if ( ! current_user_can( 'manage_options' ) ) { return new \WP_Error( 'rest_forbidden', __( 'You are not allowed to manage Achla.', 'achlaai' ), array( 'status' => 403 ) ); } $nonce = $request->get_header( 'x_wp_nonce' ); if ( ! is_string( $nonce ) || ! wp_verify_nonce( $nonce, 'wp_rest' ) ) { return new \WP_Error( 'rest_cookie_invalid_nonce', __( 'The security token expired. Reload this page.', 'achlaai' ), array( 'status' => 403 ) ); } return true; } /** @return bool|\WP_Error */ public function can_report_diagnostic( $request ) { $nonce = $request->get_header( 'x_achla_nonce' ); if ( ! is_string( $nonce ) || '' === $nonce || ! wp_verify_nonce( $nonce, 'achlaai_runtime_diagnostic' ) ) { return new \WP_Error( 'rest_forbidden', __( 'Invalid diagnostic token.', 'achlaai' ), array( 'status' => 403 ) ); } return true; } public function ownership_callback( $request ) { $content_type = strtolower( (string) $request->get_header( 'content_type' ) ); $body = $request->get_body(); $query = method_exists( $request, 'get_query_params' ) ? $request->get_query_params() : array(); if ( 1 !== preg_match( '/^application\/json(?:\s*;[^\r\n]*)?$/', $content_type ) || ! empty( $query ) || ! is_string( $body ) || strlen( $body ) > Protocol::MAX_CALLBACK_BYTES ) { return new \WP_Error( 'invalid_callback', 'Ownership callback rejected.', array( 'status' => 400 ) ); } $headers = array( 'x-aip-key-id' => (string) $request->get_header( 'x_aip_key_id' ), 'x-aip-timestamp' => (string) $request->get_header( 'x_aip_timestamp' ), 'x-aip-signature' => (string) $request->get_header( 'x_aip_signature' ), ); $result = $this->ownership->handle_callback( $body, $headers ); if ( is_wp_error( $result ) ) { return $result; } if ( empty( $result['replayed'] ) ) { wp_schedule_single_event( time() + 5, Lifecycle::CRON_HOOK ); wp_schedule_single_event( time() + 10, Lifecycle::HEARTBEAT_CRON_HOOK ); } return new \WP_REST_Response( null, 204 ); } public function connect( $request ) { return $this->response( $this->ownership->begin( false ) ); } public function reconnect( $request ) { return $this->response( $this->ownership->begin( true ) ); } public function resume( $request ) { return $this->response( $this->ownership->resume() ); } public function disconnect( $request ) { $result = $this->ownership->disconnect(); return $this->response( true === $result ? array( 'ok' => true ) : $result ); } public function save_placement( $request ) { $body = $this->json_body( $request ); if ( is_wp_error( $body ) ) { return $body; } $result = $this->placement->save( ! empty( $body['enabled'] ), isset( $body['mode'] ) && is_string( $body['mode'] ) ? $body['mode'] : '', isset( $body['selector'] ) && is_string( $body['selector'] ) ? $body['selector'] : '' ); return $this->response( $result ); } public function test_selector( $request ) { $body = $this->json_body( $request ); if ( is_wp_error( $body ) ) { return $body; } $selector = isset( $body['selector'] ) && is_string( $body['selector'] ) ? $body['selector'] : ''; return $this->response( $this->placement->test_selector( $selector ) ); } public function refresh_release( $request ) { return $this->response( $this->releases->maybe_refresh( 'manual', true ) ); } public function runtime_diagnostic( $request ) { $content_type = strtolower( (string) $request->get_header( 'content_type' ) ); $fetch_site = strtolower( (string) $request->get_header( 'sec_fetch_site' ) ); $raw = $request->get_body(); $query = method_exists( $request, 'get_query_params' ) ? $request->get_query_params() : array(); if ( 1 !== preg_match( '/^application\/json(?:\s*;[^\r\n]*)?$/', $content_type ) || 'same-origin' !== $fetch_site || ! empty( $query ) || ! is_string( $raw ) || strlen( $raw ) > 1024 ) { return new \WP_Error( 'invalid_runtime_diagnostic', 'Runtime diagnostic rejected.', array( 'status' => 403 ) ); } $body = $request->get_json_params(); if ( ! $this->has_exact_keys( $body, array( 'code' ) ) || ! is_string( $body['code'] ) ) { return new \WP_Error( 'invalid_runtime_diagnostic', 'Runtime diagnostic rejected.', array( 'status' => 403 ) ); } $result = $this->diagnostics->record( $body['code'], (string) $request->get_header( 'origin' ) ); if ( is_wp_error( $result ) ) { return $result; } $state = $this->management_diagnostics->observe( 'ATTACH_TARGET_MISSING', 'attach_target_missing' === $body['code'] ); if ( ! is_wp_error( $state ) ) { $next = $this->management_diagnostics->next_flush_at(); if ( false !== $next ) { Lifecycle::schedule_diagnostics( $next ); } } return $this->public_response( null, 204 ); } public function release_decision( $request ) { $query = method_exists( $request, 'get_query_params' ) ? $request->get_query_params() : array(); $raw = method_exists( $request, 'get_body' ) ? $request->get_body() : ''; $fetch_site = strtolower( (string) $request->get_header( 'sec_fetch_site' ) ); if ( ! empty( $query ) || ( is_string( $raw ) && '' !== $raw ) || ( '' !== $fetch_site && 'same-origin' !== $fetch_site ) ) { return $this->public_response( array( 'error' => 'invalid_release_decision_request' ), 400 ); } $settings = $this->storage->get(); if ( empty( $settings['runtime_active'] ) || empty( $settings['placement']['enabled'] ) || ! $this->storage->has_management_binding( $settings ) ) { return $this->public_response( null, 204 ); } $manifest = $this->releases->frontend_release(); if ( ! $this->storage->frontend_rendering_allowed( $settings ) || ! $manifest ) { Lifecycle::schedule_maintenance(); return $this->public_response( null, 204 ); } $settings = $this->storage->get(); return $this->public_response( array( 'schemaVersion' => 'achla.release-decision.v1', 'controlSequence' => (int) $settings['release']['control_sequence'], 'controlExpiresAt' => $settings['release']['control_expires_at'], 'release' => array( 'version' => $manifest['release']['version'], 'assetUrl' => $manifest['release']['assetUrl'], 'sri' => $manifest['release']['sri'], 'expiresAt' => $manifest['expiresAt'], 'minBridgeVersion' => $manifest['release']['minBridgeVersion'], 'bridgeContract' => $manifest['release']['bridgeContract'], ), ) ); } public function status( $request ) { Lifecycle::schedule_maintenance( $this->storage ); $settings = $this->storage->get(); $ownership = $this->ownership->status(); return rest_ensure_response( array_merge( $ownership, array( 'runtimeActive' => (bool) $settings['runtime_active'], 'projectStatus' => $settings['lifecycle']['project_status'], 'indexStatus' => $settings['lifecycle']['index_status'], 'placement' => array( 'enabled' => (bool) $settings['placement']['enabled'], 'mode' => $settings['placement']['mode'], 'selector' => $settings['placement']['attach_selector'], 'fallbackWarning' => $settings['placement']['fallback_warning'], ), 'release' => array( 'status' => $settings['release']['status'], 'errorCode' => $settings['release']['error_code'], 'errorMessage' => 'release_control_expired' === $settings['release']['error_code'] ? __( 'The release was not re-signed by the vendor.', 'achlaai' ) : '', 'sequence' => (int) $settings['release']['control_sequence'], 'verifiedAt' => (int) $settings['release']['verified_at'], 'controlExpiresAt' => $settings['release']['control_expires_at'], ), ) ) ); } /** @return array|\WP_Error */ private function json_body( $request ) { $raw = $request->get_body(); if ( ! is_string( $raw ) || strlen( $raw ) > 16384 ) { return new \WP_Error( 'payload_too_large', __( 'The request is too large.', 'achlaai' ), array( 'status' => 413 ) ); } $body = $request->get_json_params(); return is_array( $body ) ? $body : new \WP_Error( 'bad_request', __( 'The request body is invalid.', 'achlaai' ), array( 'status' => 400 ) ); } /** @return mixed */ private function response( $result ) { return is_wp_error( $result ) ? $result : rest_ensure_response( $result ); } /** @return \WP_REST_Response */ private function public_response( $data, $status = 200 ) { $response = new \WP_REST_Response( $data, $status ); $response->header( 'Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0' ); $response->header( 'Pragma', 'no-cache' ); $response->header( 'Expires', '0' ); $response->header( 'X-Robots-Tag', 'noindex, nofollow' ); return $response; } }