20, 'status' => 'publish', 'return' => 'ids' ) ); foreach ( (array) $ids as $id ) { $p = wc_get_product( $id ); if ( $p && $p->get_image_id() && count( $p->get_gallery_image_ids() ) > 0 ) { return (int) $id; } } return $ids ? (int) $ids[0] : 0; } public static function sample_urls() { $urls = array( home_url( '/' ) ); // A page the owner reported as broken goes first (the service checks at most four). $reported = get_transient( 'zeroconfig_issue_url' ); // Then the pages where a switched-off feature broke: a feature only comes back after a // check that looked at those pages again. foreach ( array_reverse( (array) ZeroConfig_Options::get( 'disabled_pages' ) ) as $pages ) { foreach ( array_reverse( (array) $pages ) as $p ) { array_unshift( $urls, $p ); } } if ( $reported ) { array_unshift( $urls, $reported ); } $post = get_posts( array( 'numberposts' => 1, 'post_status' => 'publish', 'fields' => 'ids' ) ); if ( $post ) { $urls[] = get_permalink( $post[0] ); } $page = get_posts( array( 'numberposts' => 1, 'post_type' => 'page', 'post_status' => 'publish', 'fields' => 'ids', 'exclude' => array( (int) get_option( 'page_on_front' ) ) ) ); // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- one id, one result if ( $page ) { $urls[] = get_permalink( $page[0] ); } if ( function_exists( 'wc_get_products' ) ) { $prod = self::sample_product(); if ( $prod ) { $urls[] = get_permalink( $prod ); } } return array_values( array_unique( array_filter( $urls ) ) ); } public static function run( $mode = 'normal' ) { ZeroConfig_Page_Cache::purge_all( 'safety_start' ); // the check is about to load pages itself $res = ZeroConfig_API::request_selftest( self::sample_urls(), $mode ); if ( is_wp_error( $res ) || empty( $res['job_id'] ) || ! preg_match( '/^[A-Za-z0-9_-]{1,64}$/', (string) $res['job_id'] ) ) { ZeroConfig_Options::set( 'last_selftest', array( 'status' => 'error', 'at' => time(), 'error' => is_wp_error( $res ) ? $res->get_error_message() : 'no job id' ) ); return; } delete_transient( 'zeroconfig_issue_url' ); ZeroConfig_Options::update( array( 'selftest_job' => (string) $res['job_id'], 'last_selftest' => array( 'status' => 'queued', 'at' => time(), 'started' => time() ) ) ); wp_schedule_single_event( time() + 60, self::EVENT_POLL, array( 1 ) ); } const MAX_WAIT = 2 * HOUR_IN_SECONDS; // a busy queue is not a failure; keep waiting this long /** Back-off between polls: 1, 2, 3 … minutes, at most 15. */ public static function poll_delay( $attempt ) { return min( 15, max( 1, (int) $attempt ) ) * MINUTE_IN_SECONDS; } public static function poll( $attempt = 1 ) { $job = ZeroConfig_Options::get( 'selftest_job' ); if ( ! $job ) { return; } $res = ZeroConfig_API::get_selftest( $job ); if ( ! is_wp_error( $res ) && isset( $res['status'] ) && 'error' === $res['status'] ) { // The check could not run (edge busy, Chrome unavailable). Nothing is changed; say so. ZeroConfig_Options::update( array( 'selftest_job' => '', 'last_selftest' => array( 'status' => 'error', 'at' => time(), 'error' => isset( $res['error'] ) ? sanitize_text_field( $res['error'] ) : '' ) ) ); return; } if ( is_wp_error( $res ) || empty( $res['status'] ) || 'done' !== $res['status'] ) { $st = (array) ZeroConfig_Options::get( 'last_selftest' ); $started = isset( $st['started'] ) ? (int) $st['started'] : time(); if ( time() - $started < self::MAX_WAIT ) { // Still queued or running on the service (or it was briefly unreachable): show it and wait. if ( ! is_wp_error( $res ) ) { self::track( $res ); } wp_schedule_single_event( time() + self::poll_delay( $attempt ), self::EVENT_POLL, array( $attempt + 1 ) ); } else { ZeroConfig_Options::update( array( 'selftest_job' => '', 'last_selftest' => array( 'status' => 'timeout', 'at' => time() ) ) ); } return; } if ( 'blocked' === ( $res['verdict'] ?? '' ) ) { // We could not see the site's own pages, so nothing was judged and nothing changes. ZeroConfig_Options::update( array( 'selftest_job' => '', 'last_selftest' => array( 'status' => 'blocked', 'at' => time(), 'error' => sanitize_text_field( (string) ( $res['error'] ?? '' ) ) ), ) ); return; } self::apply_verdict( $res ); } /** * Verdict shape: { status: 'done', verdict: 'pass'|'rollback', disable: { js: 'reason', ... }, report: {...} } */ public static function apply_verdict( array $res ) { $known = array( 'images', 'assets', 'js', 'delay', 'critical' ); $disabled = (array) ZeroConfig_Options::get( 'disabled' ); $pages = (array) ZeroConfig_Options::get( 'disabled_pages' ); $changed = false; // A check that tested a switched-off feature and saw nothing wrong gives it back: // one flaky run (a slow third-party script, an update mid-check) must not cost the site a // feature for good. Only when the pages where it broke were among the pages compared. $checked = array(); foreach ( isset( $res['report'] ) ? (array) $res['report'] : array() as $row ) { if ( is_array( $row ) && ! empty( $row['url'] ) ) { $checked[ (string) $row['url'] ] = true; } } foreach ( isset( $res['cleared'] ) ? (array) $res['cleared'] : array() as $feature ) { if ( ! in_array( $feature, $known, true ) || empty( $disabled[ $feature ] ) ) { continue; } $seen = true; foreach ( isset( $pages[ $feature ] ) ? (array) $pages[ $feature ] : array() as $p ) { $seen = $seen && isset( $checked[ $p ] ); } if ( $seen ) { unset( $disabled[ $feature ], $pages[ $feature ] ); $changed = true; } } if ( ! empty( $res['disable'] ) && is_array( $res['disable'] ) ) { foreach ( $res['disable'] as $feature => $reason ) { if ( in_array( $feature, $known, true ) && empty( $disabled[ $feature ] ) ) { $disabled[ $feature ] = sanitize_text_field( (string) $reason ); $pages[ $feature ] = self::failed_pages( $res, $feature ); $changed = true; } } } ZeroConfig_Options::update( array( 'disabled' => $disabled, 'disabled_pages' => $pages, 'selftest_job' => '', 'last_selftest' => array( 'status' => 'done', 'verdict' => isset( $res['verdict'] ) ? sanitize_text_field( $res['verdict'] ) : 'pass', 'at' => time(), 'pages' => isset( $res['report'] ) ? count( (array) $res['report'] ) : 0, // how many page checks ran 'disabled' => array_keys( $disabled ), ), ) ); if ( $changed ) { ZeroConfig_Page_Cache::purge_all( 'safety' ); } } /** Pages of this site where the check saw the feature break (at most two). */ private static function failed_pages( array $res, $feature ) { $out = array(); foreach ( isset( $res['report'] ) ? (array) $res['report'] : array() as $row ) { if ( is_array( $row ) && empty( $row['ok'] ) && isset( $row['feature'], $row['url'] ) && $feature === $row['feature'] ) { $p = self::own_page( (string) $row['url'] ); if ( '' !== $p && ! in_array( $p, $out, true ) ) { $out[] = $p; } } } return array_slice( $out, 0, 2 ); } /** * Remember where a queued/running check stands (queue position, steps done) for the admin card. * Returns the stored state, or null when the answer was not a waiting state. */ public static function track( array $res ) { if ( ! in_array( $res['status'] ?? '', array( 'queued', 'running' ), true ) ) { return null; } $st = (array) ZeroConfig_Options::get( 'last_selftest' ); $st['status'] = $res['status']; $st['position'] = isset( $res['queue_position'] ) ? (int) $res['queue_position'] : 0; $st['eta'] = isset( $res['eta_seconds'] ) ? max( 0, (int) $res['eta_seconds'] ) : 0; // the service's estimate $p = isset( $res['progress'] ) && is_array( $res['progress'] ) ? $res['progress'] : null; $st['progress'] = $p ? array( 'done' => max( 0, (int) ( $p['done'] ?? 0 ) ), 'total' => max( 1, (int) ( $p['total'] ?? 1 ) ), 'url' => esc_url_raw( (string) ( $p['url'] ?? '' ) ), 'viewport' => sanitize_key( (string) ( $p['viewport'] ?? '' ) ), 'feature' => sanitize_key( (string) ( $p['feature'] ?? '' ) ), ) : null; ZeroConfig_Options::set( 'last_selftest', $st ); return $st; } /** User pressed "Something looks off?": strict check + diagnostic ticket. */ public static function report_issue( $message = '', $url = '' ) { $url = self::own_page( $url ); if ( $url ) { set_transient( 'zeroconfig_issue_url', $url, DAY_IN_SECONDS ); } ZeroConfig_API::report_issue( $message, $url ); self::schedule( 5, 'strict' ); } /** A full URL or a path on this site, or '' for anything else (other hosts are never checked). */ public static function own_page( $url ) { $url = trim( (string) $url ); if ( '' === $url ) { return ''; } if ( '/' === $url[0] && ( ! isset( $url[1] ) || '/' !== $url[1] ) ) { $url = home_url( $url ); } $home = wp_parse_url( home_url() ); $page = wp_parse_url( $url ); if ( ! $page || empty( $page['host'] ) || ! in_array( $page['scheme'] ?? '', array( 'http', 'https' ), true ) ) { return ''; } $strip = function ( $h ) { return preg_replace( '/^www[.]/', '', strtolower( $h ) ); }; return $strip( $page['host'] ) === $strip( $home['host'] ?? '' ) ? esc_url_raw( $url ) : ''; } /** Re-enable everything; the next check decides again. */ public static function reset() { ZeroConfig_Options::update( array( 'disabled' => array(), 'disabled_pages' => array() ) ); ZeroConfig_Page_Cache::purge_all( 'safety' ); self::schedule( 30 ); } }