<?php if (!defined('ABSPATH')) { exit; } /** * Split broken image addresses into groups the improvement plan can act on. * A missing WebP/AVIF is not listed as "guess these 170" when the same picture loads as JPG or PNG. */ final class IComm_SH_Link_Groups { public const NOTE_ORIG = 'img_orig'; public const NOTE_GONE = 'img_gone'; public const NOTE_OTHER = 'link_other'; /** * Label broken rows that are not grouped yet. * Image rows are checked against the same filename in another format. */ public static function classify(int $budget_seconds = 18): void { $rows = self::unclassified_urls(); if ($rows === []) { return; } $other = []; $images = []; foreach ($rows as $row) { $url = (string) ($row['url'] ?? ''); $code = (int) ($row['status_code'] ?? 0); if ($url === '') { continue; } if (($code === 404 || $code === 410) && self::is_same_site_image($url)) { $images[] = $url; } else { $other[] = $url; } } self::set_note($other, self::NOTE_OTHER); if ($images === []) { return; } $deadline = microtime(true) + max(3, $budget_seconds); foreach (array_chunk($images, 8) as $chunk) { if (microtime(true) >= $deadline) { break; } $found = self::working_siblings($chunk); $orig = []; $gone = []; foreach ($chunk as $url) { if (!empty($found[$url])) { $orig[] = $url; } else { $gone[] = $url; } } self::set_note($orig, self::NOTE_ORIG); self::set_note($gone, self::NOTE_GONE); } } /** * @return array{img_orig:int, img_gone:int, link_other:int, pending:int} */ public static function counts(): array { global $wpdb; $t = IComm_SH_DB::tables(); $out = [ self::NOTE_ORIG => 0, self::NOTE_GONE => 0, self::NOTE_OTHER => 0, 'pending' => 0, ]; $rows = $wpdb->get_results( "SELECT issue_note, COUNT(DISTINCT url) AS n FROM {$t['links']} WHERE is_ignored = 0 AND status_code >= 400 AND status_code NOT IN (403, 429) GROUP BY issue_note", ARRAY_A ); if (!is_array($rows)) { return $out; } foreach ($rows as $row) { $note = (string) ($row['issue_note'] ?? ''); $n = (int) ($row['n'] ?? 0); if (isset($out[$note])) { $out[$note] = $n; } elseif ($note !== 'redirect') { $out['pending'] += $n; } } return $out; } /** * @return array{changed:int, left:int} */ public static function apply(string $group): array { if (!in_array($group, [self::NOTE_ORIG, self::NOTE_GONE], true)) { return ['changed' => 0, 'left' => 0]; } if (function_exists('set_time_limit')) { @set_time_limit(120); } $urls = self::urls_with_note($group); if ($urls === []) { return ['changed' => 0, 'left' => 0]; } $deadline = microtime(true) + 25; $changed = 0; $queue = $urls; while ($queue !== [] && microtime(true) < $deadline) { $slice = array_splice($queue, 0, 15); $map = []; $still_gone = []; if ($group === self::NOTE_ORIG) { $found = self::working_siblings($slice); foreach ($slice as $url) { $next = (string) ($found[$url] ?? ''); if ($next !== '' && $next !== $url) { $map[$url] = $next; } else { $still_gone[] = $url; } } self::set_note($still_gone, self::NOTE_GONE); } else { foreach ($slice as $url) { $map[$url] = ''; } } if ($map !== []) { IComm_SH_Link_Scanner::rewrite_url_map($map); self::ignore_urls(array_keys($map)); $changed += count($map); } } if (class_exists('IComm_SH_Scorer')) { IComm_SH_Scorer::bust_dashboard_cache(); } if (class_exists('IComm_SH_Improvement_Plan')) { IComm_SH_Improvement_Plan::bust_cache(); } return [ 'changed' => $changed, 'left' => count(self::urls_with_note($group)), ]; } /** * @return array<int, array{url:string, status_code:int}> */ private static function unclassified_urls(): array { global $wpdb; $t = IComm_SH_DB::tables(); $rows = $wpdb->get_results( $wpdb->prepare( "SELECT DISTINCT url, status_code FROM {$t['links']} WHERE is_ignored = 0 AND status_code >= 400 AND status_code NOT IN (403, 429) AND issue_note NOT IN (%s, %s, %s)", self::NOTE_ORIG, self::NOTE_GONE, self::NOTE_OTHER ), ARRAY_A ); return is_array($rows) ? $rows : []; } /** * @return string[] */ private static function urls_with_note(string $note): array { global $wpdb; $t = IComm_SH_DB::tables(); $col = $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT url FROM {$t['links']} WHERE is_ignored = 0 AND issue_note = %s AND status_code >= 400 AND status_code NOT IN (403, 429)", $note ) ); if (!is_array($col)) { return []; } $out = []; foreach ($col as $url) { $url = (string) $url; if ($url !== '') { $out[] = $url; } } return $out; } /** * @param string[] $urls */ private static function set_note(array $urls, string $note): void { global $wpdb; if ($urls === []) { return; } $t = IComm_SH_DB::tables(); foreach (array_chunk($urls, 30) as $chunk) { $placeholders = implode(',', array_fill(0, count($chunk), '%s')); $args = array_merge([$note], $chunk); $sql = "UPDATE {$t['links']} SET issue_note = %s WHERE is_ignored = 0 AND url IN ($placeholders)"; $wpdb->query($wpdb->prepare($sql, $args)); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared } } /** * @param string[] $urls */ private static function ignore_urls(array $urls): void { global $wpdb; if ($urls === []) { return; } $t = IComm_SH_DB::tables(); foreach (array_chunk($urls, 30) as $chunk) { $placeholders = implode(',', array_fill(0, count($chunk), '%s')); $sql = "UPDATE {$t['links']} SET is_ignored = 1 WHERE url IN ($placeholders)"; $wpdb->query($wpdb->prepare($sql, $chunk)); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared } } private static function is_same_site_image(string $url): bool { $host = strtolower((string) wp_parse_url($url, PHP_URL_HOST)); $site = strtolower((string) wp_parse_url(home_url('/'), PHP_URL_HOST)); if ($host === '' || $site === '') { return false; } $bare = static function (string $h): string { return strpos($h, 'www.') === 0 ? substr($h, 4) : $h; }; if ($bare($host) !== $bare($site)) { return false; } $path = (string) (wp_parse_url($url, PHP_URL_PATH) ?? ''); return (bool) preg_match('/\.(webp|avif|jpe?g|png|gif)$/i', $path); } /** * First candidate that returns a real image, in preference order. * * @param string[] $urls * @return array<string, string> broken url => working url */ private static function working_siblings(array $urls): array { $order = []; $probe = []; foreach ($urls as $url) { $cands = self::candidate_urls($url); $order[$url] = $cands; foreach ($cands as $cand) { $probe[$cand] = true; } } if ($probe === []) { return []; } $ok = self::urls_that_load(array_keys($probe)); $found = []; foreach ($order as $url => $cands) { foreach ($cands as $cand) { if (!empty($ok[$cand])) { $found[$url] = $cand; break; } } } return $found; } /** * Same filename in another format. A missing resized file can fall back to the full-size file. * * @return string[] */ private static function candidate_urls(string $url): array { $out = self::extension_swaps($url); $path = (string) (wp_parse_url($url, PHP_URL_PATH) ?? ''); if (preg_match('/-\d+x\d+\.[a-zA-Z0-9]+$/', $path)) { $full = (string) preg_replace('/-\d+x\d+(\.[a-zA-Z0-9]+)$/', '$1', $url); if ($full !== '' && $full !== $url) { foreach (self::extension_swaps($full) as $swap) { $out[] = $swap; } } } $seen = []; $clean = []; foreach ($out as $cand) { if ($cand === '' || $cand === $url || isset($seen[$cand])) { continue; } $seen[$cand] = true; $clean[] = $cand; } return $clean; } /** * @return string[] */ private static function extension_swaps(string $url): array { $path = (string) (wp_parse_url($url, PHP_URL_PATH) ?? ''); if (!preg_match('/\.([a-zA-Z0-9]+)$/', $path, $m)) { return []; } $current = strtolower($m[1]); $prefer = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif']; $out = []; foreach ($prefer as $ext) { if ($ext === $current) { continue; } $swapped = (string) preg_replace('/\.[a-zA-Z0-9]+$/', '.' . $ext, $url); if ($swapped !== '' && $swapped !== $url) { $out[] = $swapped; } } return $out; } /** * @param string[] $urls * @return array<string, bool> */ private static function urls_that_load(array $urls): array { $ok = []; foreach (array_chunk($urls, 12) as $chunk) { foreach (self::head_codes($chunk) as $url => $meta) { $code = (int) ($meta['code'] ?? 0); $type = strtolower((string) ($meta['type'] ?? '')); if ($code < 200 || $code >= 300) { continue; } if (strpos($type, 'text/html') !== false || strpos($type, 'application/xhtml') !== false) { continue; } $ok[$url] = true; } } return $ok; } /** * @param string[] $urls * @return array<string, array{code:int, type:string}> */ private static function head_codes(array $urls): array { $out = []; if ($urls === []) { return $out; } if (class_exists('\WpOrg\Requests\Requests', false) || self::load_requests()) { $requests = []; foreach ($urls as $url) { $requests[$url] = [ 'url' => $url, 'type' => 'HEAD', 'timeout' => 8, 'connect_timeout' => 4, 'redirection' => 3, 'headers' => ['User-Agent' => 'icomm site health'], ]; } try { $responses = \WpOrg\Requests\Requests::request_multiple($requests); } catch (\Throwable $e) { $responses = []; } if (is_array($responses)) { foreach ($responses as $url => $response) { if (!$response instanceof \WpOrg\Requests\Response) { continue; } $type = ''; if (isset($response->headers['content-type'])) { $h = $response->headers['content-type']; $type = is_array($h) ? (string) reset($h) : (string) $h; } $out[(string) $url] = [ 'code' => (int) $response->status_code, 'type' => $type, ]; } return $out; } } foreach ($urls as $url) { $res = wp_remote_head($url, [ 'timeout' => 8, 'redirection' => 3, 'user-agent' => 'icomm site health', ]); if (is_wp_error($res)) { continue; } $out[$url] = [ 'code' => (int) wp_remote_retrieve_response_code($res), 'type' => (string) wp_remote_retrieve_header($res, 'content-type'), ]; } return $out; } private static function load_requests(): bool { if (class_exists('\WpOrg\Requests\Requests', false)) { return true; } if (!defined('ABSPATH') || !defined('WPINC')) { return false; } $file = ABSPATH . WPINC . '/Requests/src/Requests.php'; if (is_readable($file)) { require_once $file; } return class_exists('\WpOrg\Requests\Requests', false); } } העמוד לא נמצא - Doron Screen
דלג לתוכן
X