`. */ class ABDS_SEO_CLI_Commands { /** * Seconds to wait for Action Scheduler to drain the scan's batches * before giving up and reporting a partial result. Batches are * deliberately throttled ~15s apart (see ABDS_SEO_Scan_Scheduler) to * avoid hammering shared hosting, so a full scan of a small test site * (1-2 batches) typically finishes within this window. */ const MAX_WAIT_SECONDS = 60; const POLL_INTERVAL_SECONDS = 3; /** * Triggers a full site scan (the same abds_seo_run_site_scan action * the recurring schedule fires) and actively drains the Action * Scheduler queue until it completes, instead of just firing it and * hoping WP-Cron picks it up eventually. Convenience for local * testing/demoing — production scans still run purely asynchronously. * * ## EXAMPLES * * wp abds-seo scan * * @when after_wp_load */ public function scan(): void { global $wpdb; $scans_table = $wpdb->prefix . 'abds_seo_scans'; do_action( 'abds_seo_run_site_scan' ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $scans_table is built only from $wpdb->prefix plus a hardcoded literal, never user input. This class only runs under WP-CLI (see the WP_CLI guard below), never on a real visitor request, so uncached direct queries are fine. $scan_id = (int) $wpdb->get_var( "SELECT id FROM {$scans_table} ORDER BY id DESC LIMIT 1" ); // phpcs:enable if ( ! $scan_id ) { WP_CLI::error( 'Сканування не запустилось — перевірте, чи є опубліковані записи на сайті, і чи підключено Action Scheduler (vendor/woocommerce/action-scheduler).' ); return; } $waited = 0; $status = 'running'; while ( $waited < self::MAX_WAIT_SECONDS ) { if ( class_exists( 'ActionScheduler_QueueRunner' ) ) { ActionScheduler_QueueRunner::instance()->run(); } // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- see justification above; $scan_id is passed through $wpdb->prepare(). $status = (string) $wpdb->get_var( $wpdb->prepare( "SELECT status FROM {$scans_table} WHERE id = %d", $scan_id ) ); if ( 'completed' === $status ) { break; } WP_CLI::log( "Сканування ще триває (id={$scan_id})… чекаю чергу Action Scheduler." ); sleep( self::POLL_INTERVAL_SECONDS ); $waited += self::POLL_INTERVAL_SECONDS; } // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- see justification above; $scan_id is passed through $wpdb->prepare(). $scan = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$scans_table} WHERE id = %d", $scan_id ) ); if ( $scan && 'completed' === $scan->status ) { WP_CLI::success( sprintf( 'Сканування завершено: %d сторінок, середній технічний скор %s.', (int) $scan->pages_scanned, null !== $scan->avg_score ? round( (float) $scan->avg_score, 1 ) : 'н/д' ) ); return; } WP_CLI::warning( sprintf( 'Сканування ще у процесі (статус: %s, оброблено сторінок: %d) — черга Action Scheduler продовжить обробку через WP-Cron. Повторіть "wp abds-seo scan" за хвилину, щоб побачити фінальний стан.', $scan ? $scan->status : 'unknown', $scan ? (int) $scan->pages_scanned : 0 ) ); } } if ( defined( 'WP_CLI' ) && WP_CLI ) { WP_CLI::add_command( 'abds-seo', 'ABDS_SEO_CLI_Commands' ); }