PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.7.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.7.0
2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.10.0 All 48 releases
← All changes | includes/api/traits/trait-api-cache.php +40 -11 1.0.22.7.0 View file →
@@ -141,11 +141,11 @@
141 141 // Get matching transient keys
142 142 $transient_pattern = "_transient_{$pattern}";
143 143 $cache_prefix_pattern = "_transient_{$this->cache_prefix}%";
144 144
145 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Cache invalidation requires direct database access
145 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Cache invalidation requires direct database access
146 146 $transients = $wpdb->get_col(
147 - // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- wpdb->options is WordPress core table
147 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- wpdb->options is WordPress core table
148 148 $wpdb->prepare(
149 149 "SELECT option_name FROM {$wpdb->options}
150 150 WHERE option_name LIKE %s
151 151 AND option_name LIKE %s",
@@ -174,9 +174,9 @@
174 174 protected function get_cache_stats(): array {
175 175 global $wpdb;
176 176
177 177 // Count total cache entries for this endpoint
178 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Cache statistics require direct database access
178 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Cache statistics require direct database access
179 179 $total_entries = $wpdb->get_var(
180 180 $wpdb->prepare(
181 181 "SELECT COUNT(*) FROM {$wpdb->options}
182 182 WHERE option_name LIKE %s",
@@ -184,9 +184,9 @@
184 184 )
185 185 );
186 186
187 187 // Get cache size (approximate)
188 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Cache size calculation requires direct database access
188 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Cache size calculation requires direct database access
189 189 $cache_size = $wpdb->get_var(
190 190 $wpdb->prepare(
191 191 "SELECT SUM(LENGTH(option_value)) FROM {$wpdb->options}
192 192 WHERE option_name LIKE %s",
@@ -216,13 +216,16 @@
216 216 private function generate_cache_key(string $endpoint, array $params = [], ?int $user_id = null): string {
217 217 // Sort parameters for consistent key generation
218 218 ksort($params);
219 219
220 - // Build key components
220 + // Build key components. The prefix is stored with a trailing underscore
221 + // so the "_transient_{$this->cache_prefix}%" LIKE patterns above keep a
222 + // word boundary; implode() supplies the separator here, so trim it off
223 + // rather than emitting keys like `thinkrank_seo_analytics__totals_<md5>`.
221 224 $key_parts = [
222 - $this->cache_prefix,
225 + rtrim($this->cache_prefix, '_'),
223 226 $endpoint,
224 - md5(serialize($params))
227 + md5(wp_json_encode($params))
225 228 ];
226 229
227 230 if ($user_id !== null) {
228 231 $key_parts[] = "user_{$user_id}";
@@ -306,11 +309,37 @@
306 309 }
307 310
308 311 // Generate fresh response
309 312 $response = call_user_func($callback);
310 -
311 - // Cache the response
312 - $this->set_cached_response($endpoint, $response, $params, $duration, $user_id);
313 -
313 +
314 + // Cache the response — but never cache a failure payload. Caching
315 + // `success => false` / error responses pins a transient upstream
316 + // failure (e.g. a momentary Google API error) for the full TTL, which
317 + // shows up to users as screens that stay empty long after the
318 + // underlying issue resolved.
319 + if ($this->is_cacheable_response($response)) {
320 + $this->set_cached_response($endpoint, $response, $params, $duration, $user_id);
321 + }
322 +
314 323 return array_merge($response, ['cached' => false]);
324 + }
325 +
326 + /**
327 + * Whether a response payload represents a success that is safe to cache.
328 + *
329 + * @since 1.0.0
330 + *
331 + * @param array $response Response payload from the endpoint callback
332 + * @return bool True when the payload should be cached
333 + */
334 + private function is_cacheable_response(array $response): bool {
335 + if (array_key_exists('success', $response) && $response['success'] === false) {
336 + return false;
337 + }
338 +
339 + if (!empty($response['error'])) {
340 + return false;
341 + }
342 +
343 + return true;
315 344 }
316 345 }