prefix plus a fixed suffix from a private helper (never user input); all runtime values go through $wpdb->prepare() placeholders. /** * External broken-link scanner. * * URLs come from the lbraid_external_links table (filled during the content scan). * Statuses are stored in lbraid_url_status. A single URL is not queried more often * than once a day; snooze and ignore keep it out of the report and out of the queue. */ class Linkbraid_Broken { private static function table() { global $wpdb; return $wpdb->prefix . 'lbraid_url_status'; } private static function external() { global $wpdb; return $wpdb->prefix . 'lbraid_external_links'; } /** * Syncs the status table with the detected URLs and returns the IDs to check. * Skipped: ignored, snoozed and anything checked within the last day. */ // Nazwa inna niż "prepare": sniff AbstractEscapingCheck Plugin Checka traktuje // każdy token `prepare` jak $wpdb->prepare() i wysypuje się na wywołaniu bez parametrów. public static function prepare_queue() { global $wpdb; $status = self::table(); $ext = self::external(); $wpdb->query("INSERT IGNORE INTO $status (url, url_hash, host) SELECT DISTINCT e.url, MD5(e.url), e.host FROM $ext e WHERE e.url <> ''"); $day_ago = gmdate('Y-m-d H:i:s', strtotime(current_time('mysql')) - DAY_IN_SECONDS); $today = current_time('Y-m-d'); $ids = $wpdb->get_col($wpdb->prepare( "SELECT s.id FROM $status s WHERE s.ignored = 0 AND (s.snoozed_until IS NULL OR s.snoozed_until < %s) AND (s.last_checked IS NULL OR s.last_checked < %s) AND EXISTS (SELECT 1 FROM $ext e WHERE e.url = s.url) ORDER BY s.last_checked IS NULL DESC, s.last_checked ASC", $today, $day_ago)); return array_map('intval', $ids); } /** Label of the current item for the job panel. */ public static function label($id) { global $wpdb; $url = $wpdb->get_var($wpdb->prepare('SELECT url FROM ' . self::table() . ' WHERE id=%d', $id)); /* translators: %s, %d: dynamic values in display order. */ return $url ? sprintf( __( 'Checking: %s', 'linkbraid' ), $url ) : sprintf( __( 'Checking URL #%d', 'linkbraid' ), $id ); } /** First hop without following: is this URL a redirect, and where to? */ private static function detect_redirect($url) { $args = ['timeout' => 10, 'redirection' => 0, 'sslverify' => true]; $response = wp_remote_head($url, $args); $code = is_wp_error($response) ? 0 : (int) wp_remote_retrieve_response_code($response); // Some servers reject HEAD — confirm with a tiny GET before giving up. if (!in_array($code, [200, 301, 302, 307, 308], true)) { $get = wp_remote_get($url, $args + ['limit_response_size' => 32768]); if (!is_wp_error($get)) { $response = $get; $code = (int) wp_remote_retrieve_response_code($get); } } if (!in_array($code, [301, 302, 307, 308], true)) return ['code' => 0, 'url' => '']; $location = wp_remote_retrieve_header($response, 'location'); if (is_array($location)) $location = reset($location); $location = trim((string) $location); if ($location === '') return ['code' => 0, 'url' => '']; // A relative Location resolves against the checked URL. if (strpos($location, 'http') !== 0) { $parts = wp_parse_url($url); $base = ($parts['scheme'] ?? 'https') . '://' . ($parts['host'] ?? '') . (isset($parts['port']) ? ':' . $parts['port'] : ''); $location = $location[0] === '/' ? $base . $location : $base . '/' . $location; } return ['code' => $code, 'url' => mb_substr($location, 0, 500)]; } /** Checks a single URL: HEAD, then a limited GET when the server refuses it. */ public static function check($id) { global $wpdb; $row = $wpdb->get_row($wpdb->prepare('SELECT * FROM ' . self::table() . ' WHERE id=%d', $id)); if (!$row) return ['error' => __( 'The URL was removed from the queue.', 'linkbraid' )]; $redirect = self::detect_redirect($row->url); $args = ['timeout' => 10, 'redirection' => 3, 'sslverify' => true]; $response = wp_remote_head($row->url, $args); $code = is_wp_error($response) ? 0 : (int) wp_remote_retrieve_response_code($response); // Some servers reject HEAD - confirm with a limited GET before calling a link dead. if (is_wp_error($response) || $code >= 400 || $code === 0) { $get = wp_remote_get($row->url, $args + ['limit_response_size' => 131072]); if (!is_wp_error($get)) { $response = $get; $code = (int) wp_remote_retrieve_response_code($get); } elseif (is_wp_error($response)) { $response = $get; $code = 0; } } $error = is_wp_error($response) ? mb_substr($response->get_error_message(), 0, 500) : ''; $ok = !is_wp_error($response) && $code >= 200 && $code < 400; $wpdb->update(self::table(), [ 'status_code' => $code, 'ok' => $ok ? 1 : 0, 'redirect_code'=> (int) $redirect['code'], 'redirect_url' => $redirect['url'], 'error_message'=> $error, 'last_checked' => current_time('mysql'), 'check_count' => (int) $row->check_count + 1, ], ['id' => $id]); return ['success' => true, 'count' => 1]; } /** Number of confirmed broken links (for the Overview tasks). */ public static function broken_count() { global $wpdb; $table = self::table(); if (!$wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $wpdb->esc_like($table)))) return 0; return (int) $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $table WHERE check_count > 0 AND ok = 0 AND ignored = 0 AND (snoozed_until IS NULL OR snoozed_until < %s)", current_time('Y-m-d'))); } /** URL of a status row (for content-level actions). */ public static function url($id) { global $wpdb; return (string) $wpdb->get_var($wpdb->prepare('SELECT url FROM ' . self::table() . ' WHERE id=%d', (int) $id)); } public static function set_ignored($id, $ignored) { global $wpdb; return false !== $wpdb->update(self::table(), ['ignored' => $ignored ? 1 : 0], ['id' => (int) $id]); } public static function snooze($id, $days) { global $wpdb; $days = max(1, min(365, (int) $days)); $until = gmdate('Y-m-d', strtotime(current_time('Y-m-d')) + $days * DAY_IN_SECONDS); return false !== $wpdb->update(self::table(), ['snoozed_until' => $until], ['id' => (int) $id]); } }