'GET', 'callback' => [ $this, 'get_post_ids' ], 'permission_callback' => fn() => \Clusterlane\Core\Capabilities::can_use(), ] ); // Scan a BATCH of posts in one request (fast). register_rest_route( 'clusterlane/v1', '/scan/batch', [ 'methods' => 'POST', 'callback' => [ $this, 'scan_batch' ], 'permission_callback' => fn() => \Clusterlane\Core\Capabilities::can_use(), ] ); // Published pages waiting to be read after a scan (0.98.178): where it // stands (GET), or read the next ~20 s of them (POST). register_rest_route( 'clusterlane/v1', '/scan/pages', [ [ 'methods' => 'GET', 'callback' => fn() => rest_ensure_response( \Clusterlane\Core\PageReader::status() ), 'permission_callback' => fn() => \Clusterlane\Core\Capabilities::can_use(), ], [ 'methods' => 'POST', 'callback' => fn() => rest_ensure_response( \Clusterlane\Core\PageReader::run_queue() ), 'permission_callback' => fn() => \Clusterlane\Core\Capabilities::can_use(), ], ] ); // Scan a single post (kept for compatibility). register_rest_route( 'clusterlane/v1', '/scan/post/(?P\d+)', [ 'methods' => 'POST', 'callback' => [ $this, 'scan_post' ], 'permission_callback' => fn() => \Clusterlane\Core\Capabilities::can_use(), 'args' => [ 'id' => [ 'validate_callback' => fn( $v ) => is_numeric( $v ), 'sanitize_callback' => 'absint', ], ], ] ); } public function get_post_ids(): \WP_REST_Response { $settings = $this->plugin->get('settings'); // A full scan starts here: drop data for posts that left the scope // (unpublished, deleted or password-protected since the last scan). \Clusterlane\Core\Scope::purge_out_of_scope(); \Clusterlane\Core\PageReader::reset_log(); // this scan's page-reading report starts empty \Clusterlane\Core\PageReader::reset_queue(); // and so does the queue of pages to read $ids = $this->plugin->get('scanner')->get_all_post_ids( $settings->post_types() ); return rest_ensure_response( [ 'ids' => $ids, 'total' => count( $ids ), 'reads_pages' => \Clusterlane\Core\PageReader::any_enabled() ] ); } /** * Scan a batch of posts in one request. * Expects JSON body: { "ids": [1, 2, 3, ...] } * Returns: { "scanned": N, "results": [{post_id, links_found}, ...] } */ /** Maximum post IDs accepted per batch request. */ const MAX_BATCH_SIZE = 100; /** Seconds a batch may run before it hands the rest back (0.98.177). */ const TIME_BUDGET = 20; public function scan_batch( \WP_REST_Request $request ): \WP_REST_Response { $raw = $request->get_json_params()['ids'] ?? []; if ( empty( $raw ) || ! is_array( $raw ) ) { return new \WP_REST_Response( [ 'error' => 'Missing ids array' ], 400 ); } // Cast every element to int and enforce an upper limit to prevent // a single request from scanning thousands of posts and timing out. $ids = array_slice( array_map( 'intval', $raw ), 0, self::MAX_BATCH_SIZE ); // A batch of 25 posts can exceed a strict host max_execution_time // (often 30s on shared hosting) because link extraction resolves // every URL. Without this guard the request dies mid-batch and // the whole batch is lost, which shows up as "only N of M scanned". if ( function_exists( 'set_time_limit' ) ) @set_time_limit( 300 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, Squiz.PHP.DiscouragedFunctions.Discouraged -- one scan batch on an admin's request; harmless where disabled ignore_user_abort( true ); $scanner = $this->plugin->get('scanner'); $results = []; $start = microtime( true ); $left = []; // 'later' (the Scan button, 0.98.178): read the text now, queue the // published pages. 'now' (onboarding, older screens): read them here. $later = ( $request->get_json_params()['pages'] ?? 'now' ) === 'later'; \Clusterlane\Core\PageReader::defer( $later ); foreach ( $ids as $i => $post_id ) { $post_id = (int) $post_id; if ( ! $post_id ) continue; // Reading published pages takes a second or so each (0.98.177): stop after // ~20 s and hand the rest back, well inside hosts' and proxies' time limits. if ( $results && microtime( true ) - $start > self::TIME_BUDGET ) { $left = array_values( array_slice( $ids, $i ) ); break; } $links_found = $scanner->scan_post( $post_id ); $results[] = [ 'post_id' => $post_id, 'links_found' => $links_found ]; } \Clusterlane\Core\PageReader::defer( false ); return rest_ensure_response( [ 'scanned' => count( $results ), 'results' => $results, 'remaining' => $left, // not scanned yet: the client sends them again 'pages' => \Clusterlane\Core\PageReader::flush_log(), // published pages read / failed in this batch 'queued' => \Clusterlane\Core\PageReader::flush_queue(), // published pages left for later (pages: 'later') ] ); } public function scan_post( \WP_REST_Request $request ): \WP_REST_Response { $post_id = (int) $request['id']; $count = $this->plugin->get('scanner')->scan_post( $post_id ); return rest_ensure_response( [ 'post_id' => $post_id, 'links_found' => $count ] ); } }