p’Är=VÿÿÿÿÃ+p«Ãr=VÀphÄ=(Är=V`PÄAðNÇr=V°P ÄQïÄr=V `ÄAàÈÃr=Vÿÿÿÿÿÿÿÿ0Ä< Är=V0@Ä@«Är=V@ð PÄP_Är=VPÿÿÿÿÿÿÿÿĈµÆr=VKÀ pÅpAÄr=VÀ PÅtAÄr=V° `ÅtÐÈr=VPpÅB ÅÃr=VÿÿÿÿÿÿÿÿÿÿÿÿÅ<p«Ãr=V€` €Æ=(Är=VP PÇAp«Ãr=VÀ@ ˆÈ=ðNÇr=V°0 pÈQïÄr=VpPÈAðNÇr=V° €ÈQïÄr=V€`ÈAðNÇr=V°Ð ÈQïÄr=VpÈAðNÇr=V°   ÈQïÄr=V €ÈAàÈÃr=Vÿÿÿÿÿÿÿÿ°È<AÆr=V°`ÈuðNÇr=V°0 ÀÉQïÄr=VÀpÉAp»Ãr=VÿÿÿÿÿÿÿÿÿÿÿÿƵÆr=VKà ËpAÄr=Và PËtAÄr=VÐ `ËtÐÈr=VPpËB ÅÃr=VÿÿÿÿÿÿÿÿÿÿÿÿË<µÆr=VK€  ÌpAÄr=V€ PÌtÐÈr=VP`ÌB ÅÃr=VÿÿÿÿÿÿÿÿÿÿÿÿÌ<0Är=V0 ÿÿÿÿÿÿÿÿ͈p«Ãr=VÀ °Ñ=(Är=V PÑAðNÇr=VÀ ÑQïÄr=V`ÑAàÈÃr=VÿÿÿÿÿÿÿÿÑ< Är=V°  Ñ@«Är=V   0ÑP_Är=V0ÿÿÿÿÿÿÿÿшµÆr=Vdp ¸ÒpAÄr=Vp PÒtAÄr=V` `ÒtÐÈr=VPpÒB ÅÃr=VÿÿÿÿÿÿÿÿÿÿÿÿÒ<p«Ãr=V€ ÈÓ=(Är=V PÔAp«Ãr=VÀð ÐÕ=ðNÇr=VÀà PÕQïÄr=VPPÕAðNÇr=VÀ° `ÕQïÄr=V``ÕAðNÇr=VÀ€ pÕQïÄr=VppÕAðNÇr=VÀP €ÕQïÄr=V€€ÕAàÈÃr=VÿÿÿÿÿÿÿÿÕ<AÆr=V`ÕuðNÇr=VÀà ÖQïÄr=V pÖAp»Ãr=VÿÿÿÿÿÿÿÿÿÿÿÿÓµÆr=VdØØpAÄr=VPØtAÄr=V€`ØtÐÈr=VPpØB ÅÃr=VÿÿÿÿÿÿÿÿÿÿÿÿØ<µÆr=Vd0èÙpAÄr=V0PÙtÐÈr=VP`ÙB ÅÃr=VÿÿÿÿÿÿÿÿÿÿÿÿÙ<0Är=VàÿÿÿÿÿÿÿÿÚˆ0Är=VÐÿÿÿÿÿÿÿÿ܈àèÃr=VÀÿÿÿÿÿÿÿÿÿÿÿÿß>Ø<‹@臑@Ø<‹@@!•Lü~P%@+¨@h{¥@P%@+¨@h{¥@P‚‰@P%@+¨@ˆž@P%@+¨@ˆž@P‚‰@P%@+¨@ ¡ Nü~P%@+¨@àNü~P‚‰@P%@+¨@°Ÿ@P%@+¨@°Ÿ@P‚‰@ á¿@P4Š@ÐôLü~ð‘Lü~ˆä‹@袉@°è‹@8¢š@àžš@ø†‘@+“@p“Lü~艑@P‘Lü~ ‡@¨Ô¬@0Љ@€‘Lü~€‘Lü~ˆž@¨ê‹@P4Š@õLü~°‘Lü~ˆä‹@袉@°è‹@8¢š@àžš@à‘Lü~à‘Lü~ˆž@Ðê‹@‘Lü~‘Lü~ˆž@€‘@@‘Lü~p‘Lü~ ‡@¨Ô¬@0Љ@ ‘Lü~ ‘Lü~ ½åMü~¨ê‹@P4Š@@õLü~БLü~ˆä‹@袉@°è‹@8¢š@àžš@ ‘Lü~ ‘Lü~`‘Nü~Ðê‹@0 ‘Lü~0 ‘Lü~ 'permission_callback' => [ $this, 'create_item_permissions_check' ], 'args' => [ 'page_url' => [ 'required' => true, 'validate_callback' => function ( $param ) { if ( empty( $param ) ) { return false; } $url = untrailingslashit( trim( $param ) ); $url = rocket_add_url_protocol( $url ); return wp_http_validate_url( $url ); }, 'sanitize_callback' => function ( $param ) { $url = untrailingslashit( trim( $param ) ); return rocket_add_url_protocol( $url ); }, ], ], ], ] ); register_rest_route( self::ROUTE_NAMESPACE, self::ROUTE_BASE . '/pages/progress', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_progress' ], 'permission_callback' => [ $this, 'get_progress_permissions_check' ], 'args' => [ 'ids' => [ 'required' => true, 'validate_callback' => function ( $param ) { if ( ! is_array( $param ) ) { return false; } foreach ( $param as $id ) { if ( ! is_numeric( $id ) ) { return false; } } return true; }, 'sanitize_callback' => function ( $param ) { $ids = array_map( 'intval', $param ); // Remove anything that is not a valid integer > 0. $ids = array_filter( $ids ); // Keep index clean. $ids = array_values( $ids ); return $ids; }, ], ], ] ); register_rest_route( self::ROUTE_NAMESPACE, self::ROUTE_BASE . '/pages/(?P\d+)', [ [ 'methods' => WP_REST_Server::DELETABLE, 'callback' => [ $this, 'delete_item' ], 'permission_callback' => [ $this, 'delete_item_permissions_check' ], 'args' => [ 'id' => [ 'required' => true, 'validate_callback' => function ( $param ) { return is_numeric( $param ); }, 'sanitize_callback' => function ( $param ) { return intval( $param ); }, ], ], ], [ 'methods' => WP_REST_Server::EDITABLE, 'callback' => [ $this, 'update_item' ], 'permission_callback' => [ $this, 'update_item_permissions_check' ], 'args' => [ 'id' => [ 'required' => true, 'validate_callback' => function ( $param ) { return is_numeric( $param ); }, 'sanitize_callback' => function ( $param ) { return intval( $param ); }, ], ], ], ] ); } /** * Retrieves one item from the collection. * * @param WP_REST_Request $request Full details about the request. * * @return WP_REST_Response|WP_Error */ public function get_item( $request ) { $html = $this->render->get_rocket_insights_column( $request['url'], $request['post_id'] ); $payload = [ 'success' => true, 'html' => $html, ]; return rest_ensure_response( $payload ); } /** * Checks if a given request has access to get a specific item. * * @param WP_REST_Request $request Full details about the request. * * @return true|WP_Error */ public function get_item_permissions_check( $request ) { if ( ! $this->context->is_allowed() ) { return new WP_Error( 'rest_forbidden', __( 'You are not allowed to access this item.', 'rocket' ), [ 'status' => 403 ] ); } return true; } /** * Creates one item from the collection. * * @param WP_REST_Request $request Full details about the request. * * @return WP_REST_Response|WP_Error */ public function create_item( $request ) { // Check if adding a page is allowed based on URL limits. if ( ! $this->context->is_adding_page_allowed() ) { $error = new WP_Error( 'rest_forbidden', $this->get_page_limit_error_message(), [ 'status' => 403, 'remaining_urls' => 0, 'can_add_pages' => false, ] ); return rest_ensure_response( $error ); } $payload = $this->get_url_validation_payload( $request['page_url'] ); if ( $payload['error'] ) { return rest_ensure_response( $payload ); } $url = $payload['processed_url']; if ( Utils::is_home( $url ) ) { $page_title = __( 'Homepage', 'rocket' ); } else { $page_title = $this->get_page_title( $payload['message'] ); } $additional_details = [ 'title' => $page_title, ]; // Handle synchronous submission using shared method. $row_id = $this->handle_sync_submission( $url, true, $additional_details ); if ( empty( $row_id ) ) { $error = new WP_Error( 'rest_invalid_input', esc_html__( 'Not valid inputs', 'rocket' ), [ 'status' => 500 ] ); return rest_ensure_response( $error ); } // Check URL limit again after insertion to handle race conditions. // If the limit is exceeded, remove the newly added URL and return an error. if ( $this->query->get_total_count() > $this->plan->max_urls() ) { // Delete the newly added URL. $this->query->delete_item( $row_id ); $error = new WP_Error( 'rest_forbidden', __( 'Maximum number of URLs reached for your license.', 'rocket' ), [ 'status' => 403, 'remaining_urls' => 0, 'can_add_pages' => false, ] ); return rest_ensure_response( $error ); } $urls_count = $this->query->get_total_count(); $current_plan = $this->plan->get_current_plan(); /** * Fires when a performance monitoring job is added. * * @since 3.20 * * @param string $url The URL that was added for monitoring. * @param string $plan Plan name. * @param int $urls_count The current number of URLs being monitored. */ do_action( 'rocket_rocket_insights_job_added', $url, $current_plan, $urls_count ); $row_data = $this->query->get_row_by_id( (int) $row_id ); // Remove message from the response payload. unset( $payload['message'] ); $payload['success'] = true; $payload['id'] = $row_id; $payload['html'] = $this->render->get_performance_monitoring_list_row( $row_data ); $payload['global_score_data'] = $this->get_global_score_payload(); $payload['remaining_urls'] = $this->get_remaining_url_count(); $payload['has_credit'] = $this->plan->has_credit(); $payload['can_add_pages'] = $this->context->is_adding_page_allowed(); // Add disabled button html data to payload. if ( 0 === $this->get_remaining_url_count() ) { $data = $payload['global_score_data']['data']; $data['reach_max_url'] = true; $payload['global_score_data']['disabled_btn_html'] = [ 'global_score_widget' => $this->render->get_add_page_btn( 'global-score-widget', $data ), 'rocket_insights' => $this->render->get_add_page_btn( 'rocket-insights', $data ), ]; } return rest_ensure_response( $payload ); } /** * Checks if a given request has access to create items. * * @param WP_REST_Request $request Full details about the request. * * @return true|WP_Error */ public function create_item_permissions_check( $request ) { if ( ! $this->context->is_allowed() ) { return new WP_Error( 'rest_forbidden', __( 'You are not allowed to create this item.', 'rocket' ), [ 'status' => 403 ] ); } return true; } /** * Deletes one item from the collection. * * @param WP_REST_Request $request Full details about the request. * * @return WP_REST_Response|WP_Error */ public function delete_item( $request ) { if ( empty( $request['id'] ) ) { $error = new WP_Error( 'rest_invalid_param', __( 'Invalid item ID.', 'rocket' ), [ 'status' => 400 ] ); return rest_ensure_response( $error ); } $result = $this->query->delete_item( $request['id'] ); /** * Fires when a performance monitoring job is deleted. * * @since 3.20 * * @param int $id The ID of the deleted performance monitoring job. */ do_action( 'rocket_rocket_insights_job_deleted', $request['id'] ); return rest_ensure_response( $result ); } /** * Checks if a given request has access to delete a specific item. * * @param WP_REST_Request $request Full details about the request. * * @return true|WP_Error */ public function delete_item_permissions_check( $request ) { if ( ! $this->context->is_allowed() ) { return new WP_Error( 'rest_forbidden', __( 'You are not allowed to delete this item.', 'rocket' ), [ 'status' => 403 ] ); } return true; } /** * Updates one item from the collection. * * @param WP_REST_Request $request Full details about the request. * * @return WP_REST_Response|WP_Error */ public function update_item( $request ) { if ( empty( $request['id'] ) ) { $error = new WP_Error( 'rest_invalid_param', __( 'No ID was provided.', 'rocket' ), [ 'status' => 400 ] ); return rest_ensure_response( $error ); } $row = $this->query->get_row_by_id( $request['id'] ); if ( ! $row ) { $error = new WP_Error( 'rest_not_found', __( 'Item not found.', 'rocket' ), [ 'status' => 404 ] ); return rest_ensure_response( $error ); } // Check if adding a page is allowed based on URL limits. if ( ! $this->plan->has_credit() ) { $error = new WP_Error( 'rest_forbidden', esc_html__( 'Upgrade your plan to get access to re-test performance or run new tests', 'rocket' ), [ 'status' => 403, 'remaining_urls' => 0, 'can_add_pages' => false, ] ); return rest_ensure_response( $error ); } $additional_details = [ 'data' => [ 'is_retest' => true, ], 'score' => '', 'report_url' => '', 'is_blurred' => 0, ]; // Handle synchronous submission using shared method. $row_id = $this->handle_sync_submission( $row->url, true, $additional_details ); // @phpstan-ignore-line if ( empty( $row_id ) ) { $error = new WP_Error( 'rest_not_found', __( 'Unable to reset performance test. Please try again.', 'rocket' ), [ 'status' => 404 ] ); return rest_ensure_response( $error ); } /** * Fires when a performance monitoring job is reset/retested. * * @since 3.20 * * @param int $id The database row ID of the reset job. */ do_action( 'rocket_rocket_insights_job_retest', $request['id'] ); $row = $this->query->get_row_by_id( $request['id'] ); $data = [ 'success' => true, 'id' => $request['id'], 'html' => $this->render->get_performance_monitoring_list_row( $row ), 'global_score_data' => $this->get_global_score_payload(), 'remaining_urls' => $this->get_remaining_url_count(), 'has_credit' => $this->plan->has_credit(), 'can_add_pages' => $this->context->is_adding_page_allowed(), ]; return rest_ensure_response( $data ); } /** * Handle synchronous submission of Rocket Insights job. * * This method centralizes the logic for attempting synchronous job submission * and falling back to async queuing when needed. It uses JobProcessor's send_api * for the actual API call, then adds Rocket Insights-specific validation and logging. * * @since 3.20 * * @param string $url The URL to test. * @param bool $is_mobile Whether this is a mobile test. * @param array $additional_details Optional additional data to store with the job. * * @return bool|null Row ID on success, false on failure, null if not allowed. */ private function handle_sync_submission( string $url, bool $is_mobile, array $additional_details = [] ) { // Attempt synchronous API submission. $sync_response = $this->job_processor->send_api( $url, $is_mobile, 'rocket_insights', true ); // If sync submission failed or returned WP_Error, fall back to async queue. if ( false === $sync_response || empty( $sync_response['uuid'] ) ) { return $this->manager->add_to_the_queue( $url, $is_mobile, $additional_details ); } // Success! Save with the new data. $row_id = $this->manager->add_to_the_queue( $url, $is_mobile, $additional_details ); if ( empty( $row_id ) ) { // DB insert failed after successful API submission - log orphaned job. Logger::error( 'Rocket Insights: Database insert failed after successful sync submission', [ 'url' => $url, 'job_id' => $sync_response['uuid'], ] ); return false; } // Update to pending status immediately with job ID. $this->query->make_status_pending( $url, $sync_response['uuid'], '', $is_mobile ); return $row_id; } /** * Checks if a given request has access to update a specific item. * * @param WP_REST_Request $request Full details about the request. * * @return true|WP_Error */ public function update_item_permissions_check( $request ) { if ( ! $this->context->is_allowed() ) { return new WP_Error( 'rest_forbidden', __( 'You are not allowed to update this item.', 'rocket' ), [ 'status' => 403 ] ); } return true; } /** * Retrieves a collection of items. * * @param WP_REST_Request $request Full details about the request. * * @return WP_REST_Response|WP_Error */ public function get_items( $request ) { $items = $this->query->query(); if ( empty( $items ) ) { $error = new WP_Error( 'rest_not_found', 'No items found.', [ 'status' => 404 ] ); return rest_ensure_response( $error ); } return rest_ensure_response( $items ); } /** * Checks if a given request has access to get items. * * @param WP_REST_Request $request Full details about the request. * * @return true|WP_Error */ public function get_items_permissions_check( $request ) { if ( ! $this->context->is_allowed() ) { return new WP_Error( 'rest_forbidden', __( 'You are not allowed to access items.', 'rocket' ), [ 'status' => 403 ] ); } return true; } /** * Retrieves the progress of one or more items. * * @param WP_REST_Request $request Full details about the request. * * @return WP_REST_Response|WP_Error */ public function get_progress( $request ) { $payload = []; if ( empty( $request['ids'] ) ) { $error = new WP_Error( 'rest_invalid_param', 'ids empty', [ 'status' => 400 ] ); return rest_ensure_response( $error ); } $query_params = [ 'id__in' => $request['ids'], ]; $results = $this->query->query( $query_params ); // Result is empty. if ( empty( $results ) ) { $error = new WP_Error( 'rest_not_found', 'No rows found in DB for ids: ' . implode( ',', $request['ids'] ), [ 'status' => 404 ] ); return rest_ensure_response( $error ); } foreach ( $results as $result ) { $result->html = $this->render->get_performance_monitoring_list_row( $result ); } $payload['success'] = true; $payload['results'] = $results; $payload['global_score_data'] = $this->get_global_score_payload(); $payload['has_credit'] = $this->plan->has_credit(); $payload['can_add_pages'] = $this->context->is_adding_page_allowed(); return rest_ensure_response( $payload ); } /** * Checks if a given request has access to get progress. * * @param WP_REST_Request $request Full details about the request. * * @return true|WP_Error */ public function get_progress_permissions_check( $request ) { if ( ! $this->context->is_allowed() ) { return new WP_Error( 'rest_forbidden', __( 'You are not allowed to access items.', 'rocket' ), [ 'status' => 403 ] ); } return true; } /** * Validates a given URL for performance monitoring eligibility. * * @param string $url The URL to validate. * * @return array { * @type bool $error Whether an error occurred during validation. * @type string $message The error message, or an empty string if no error. * @type string $processed_url The URL with protocol added if validation passes. * } */ protected function get_url_validation_payload( string $url ): array { $payload = [ 'error' => false, 'message' => '', 'processed_url' => '', 'data' => [ 'status' => 400, ], ]; if ( 'local' === wp_get_environment_type() ) { $payload['error'] = true; $payload['message'] = 'Performance monitoring is disabled for local environment'; return $payload; } // Validate that performance monitoring is not disabled. if ( ! $this->context->is_allowed() ) { $payload['error'] = true; $payload['message'] = 'Performance monitoring is disabled.'; return $payload; } // Validate that url is not empty. if ( '' === $url ) { $payload['error'] = true; $payload['message'] = 'No url provided.'; return $payload; } // Check if URL has protocol, add if needed. $url = rocket_add_url_protocol( $url ); $payload['processed_url'] = $url; $response = $this->get_page_content( $url ); if ( ! $response ) { $payload['error'] = true; $payload['message'] = 'Url does not resolve to a valid page.'; return $payload; } // check if url is not from admin. if ( strpos( $url, admin_url() ) === 0 ) { $payload['error'] = true; $payload['message'] = 'Url is an admin page.'; return $payload; } // Check if url has not been submited. if ( false !== $this->manager->get_single_job( $url, true ) ) { $payload['error'] = true; return $payload; } // Fetch url body and send to payload. $payload['message'] = $response; return $payload; } /** * Retrieves the global performance score payload for AJAX responses. * * Gets the global score data, determines the status color, and generates the HTML * for the global score widget. * * @return array { * @type array $data Global score data including score, pages_num, status, and status-color. * @type string $html Rendered HTML for the global score widget. * } */ private function get_global_score_payload() { $payload = $this->global_score->get_global_score_data(); $payload['status-color'] = $this->render->get_score_color_status( (int) $payload['score'] ); $payload['remaining_urls'] = $this->get_remaining_url_count(); return [ 'data' => $payload, 'html' => $this->render->get_global_score_widget_content( $payload ), 'row_html' => $this->render->get_global_score_row( $payload ), ]; } /** * Get the remaining number of URLs that can be added based on user's plan limit. * * @return int Number of URLs that can still be added. */ private function get_remaining_url_count(): int { return max( 0, $this->plan->max_urls() - (int) $this->query->get_total_count() ); } /** * Get the error message for when page limit is reached. * * @since 3.17 * * @return string The formatted error message. */ private function get_page_limit_error_message(): string { if ( $this->context->is_free_user() ) { $upgrade_url = admin_url( 'options-general.php?page=' . WP_ROCKET_PLUGIN_SLUG . '#rocket_insights' ); return sprintf( /* translators: %1$s: opening tag, %2$s: closing tag, %3$s: opening link tag, %4$s: closing link tag */ __( "You've %1\$sreached your free limit%2\$s. %3\$sUpgrade to continue%4\$s.", 'rocket' ), '', '', '', '' ); } return sprintf( /* translators: %1$s: opening tag, %2$s: closing tag */ __( "You've %1\$sreached the page limit%2\$s. Please remove at least one page to continue.", 'rocket' ), '', '' ); } }