settings->credential('api_key'); $siteId = $this->settings->all()['site_id']; if (!$this->settings->connected() || !preg_match('/^kmt_pub_[A-Za-z0-9]{64}$/D', $key) || $page < 1 || $page > 10000) { return null; } $generation = get_option('kommentti_cache_generation', '0'); $postGeneration = get_post_meta($postId, '_kommentti_cache_generation', true); $cacheKey = 'kommentti_'.hash('sha256', implode('|', [$siteId, hash('sha256', $key), $thread, $page, $this->settings->locale(), $generation, $postGeneration])); $cached = get_transient($cacheKey); if (!$force && is_array($cached) && ($cached['expires'] ?? 0) > time() && $this->valid($cached['snapshot'] ?? null, $thread, $page)) { return $cached['snapshot']; } if (!$force && get_transient('kommentti_api_backoff')) { return null; } $lock = wp_json_encode(['until' => time() + 10, 'nonce' => wp_generate_uuid4()]); if (!$this->lock($lock)) { return null; } try { $headers = ['Accept' => 'application/json', 'Authorization' => 'Bearer '.$key, 'X-Kommentti-Locale' => $this->settings->locale()]; if (is_array($cached) && preg_match('/^"[a-f0-9]{64}"$/D', $cached['etag'] ?? '')) { $headers['If-None-Match'] = $cached['etag']; } $url = Settings::API_URL.'/v1/publisher/sites/'.rawurlencode($siteId).'/comments?'.http_build_query(['thread' => $thread, 'page' => $page]); $response = wp_remote_get($url, $this->request_options($headers)); $status = is_wp_error($response) ? 0 : (int) wp_remote_retrieve_response_code($response); if ($status === 304 && is_array($cached) && $this->valid($cached['snapshot'] ?? null, $thread, $page)) { $snapshot = $cached['snapshot']; $etag = $cached['etag'] ?? ''; } elseif ($status === 200) { $snapshot = json_decode(wp_remote_retrieve_body($response), true, 64); $etag = (string) wp_remote_retrieve_header($response, 'etag'); } else { $snapshot = null; $etag = ''; } if (!$this->valid($snapshot, $thread, $page)) { delete_transient($cacheKey); $retryAfter = is_wp_error($response) ? 20 : (int) wp_remote_retrieve_header($response, 'retry-after'); set_transient('kommentti_api_backoff', true, max(20, min(60, $retryAfter))); $this->status($status); return null; } if ($page === 1 || $snapshot['comments']) { set_transient($cacheKey, ['snapshot' => $snapshot, 'etag' => preg_match('/^"[a-f0-9]{64}"$/D', $etag) ? $etag : '', 'expires' => time() + 60], 600); } $this->status(200); return $snapshot; } finally { $this->unlock($lock); } } public function diagnose(): array { if (!$this->settings->connected()) { return ['code' => 0, 'api_code' => null, 'checked_at' => time()]; } $siteId = $this->settings->all()['site_id']; $url = Settings::API_URL.'/v1/sites/'.rawurlencode($siteId).'/comments?'.http_build_query(['origin' => $this->settings->origin(), 'thread' => '__wordpress_connection_check__', 'lang' => $this->settings->locale()]); $response = wp_remote_get($url, $this->request_options(['Accept' => 'application/json'])); $code = is_wp_error($response) ? 0 : (int) wp_remote_retrieve_response_code($response); $apiCode = null; if ($code === 200 && $this->settings->all()['mode'] === 'server') { delete_transient('kommentti_api_backoff'); $this->comments(0, '__wordpress_connection_check__', 1, true); $apiCode = (get_transient('kommentti_api_status') ?: [])['code'] ?? 0; } $status = ['code' => $code, 'api_code' => $apiCode, 'checked_at' => time()]; set_transient('kommentti_connection_status', $status, 900); return $status; } private function request_options(array $headers): array { return ['headers' => $headers, 'timeout' => 2, 'redirection' => 0, 'sslverify' => true, 'reject_unsafe_urls' => true, 'limit_response_size' => 1048576, 'user-agent' => 'Kommentti-WordPress/'.KOMMENTTI_VERSION]; } private function status(int $code): void { set_transient('kommentti_api_status', ['code' => $code, 'checked_at' => time()], 900); } private function valid(mixed $snapshot, string $thread, int $page): bool { if (!is_array($snapshot) || ($snapshot['version'] ?? null) !== 1 || ($snapshot['site'] ?? null) !== $this->settings->all()['site_id'] || ($snapshot['thread'] ?? null) !== $thread || ($snapshot['page'] ?? null) !== $page || ($snapshot['per_page'] ?? null) !== 20 || !is_int($snapshot['total'] ?? null) || $snapshot['total'] < 0 || !is_bool($snapshot['has_more'] ?? null) || !is_bool($snapshot['locked'] ?? null) || !is_array($snapshot['comments'] ?? null) || count($snapshot['comments']) > 20) { return false; } foreach ($snapshot['comments'] as $comment) { if (!is_array($comment) || !Settings::uuid($comment['id'] ?? null) || !is_string($comment['name'] ?? null) || strlen($comment['name']) > 1000 || !is_string($comment['body'] ?? null) || strlen($comment['body']) > 50000 || !is_string($comment['created_at'] ?? null) || strtotime($comment['created_at']) === false || (isset($comment['parent_id']) && !Settings::uuid($comment['parent_id']))) { return false; } } return true; } private function lock(string $value): bool { global $wpdb; $existing = get_option('kommentti_fetch_lock', ''); if (is_string($existing) && $existing !== '' && (json_decode($existing, true)['until'] ?? 0) <= time()) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Atomic compare-and-delete cannot be expressed with delete_option(). $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->options} WHERE option_name = %s AND option_value = %s", 'kommentti_fetch_lock', $existing)); wp_cache_delete('kommentti_fetch_lock', 'options'); } return add_option('kommentti_fetch_lock', $value, '', false); } private function unlock(string $value): void { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Release only the lock value owned by this request. $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->options} WHERE option_name = %s AND option_value = %s", 'kommentti_fetch_lock', $value)); wp_cache_delete('kommentti_fetch_lock', 'options'); } }