PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.12.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.12.0
2.12.0 2.11.0 2.10.0 2.9.0 2.8.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 All 53 releases
thinkrank / includes / seo / class-cache-purger.php

class-cache-purger.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 2.12.0, at includes/seo/class-cache-purger.php

295 lines 10.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Cache purging for content ThinkRank rewrites underneath a cache.
5 *
6 * ThinkRank writes values that other software has already rendered into HTML
7 * and stored. Alt text is the clearest case: `update-image-alt-text` writes
8 * `_wp_attachment_image_alt` and returns success, while the page a visitor
9 * loads still carries the old words because Elementor kept the rendered widget
10 * in post meta and a page cache kept the whole document. The write is correct
11 * and invisible, which is the worst possible combination for an agent that
12 * reports back to a user (#763).
13 *
14 * Two layers are dealt with here:
15 *
16 * - **Builder output caches.** Elementor stores rendered elements per document
17 * in `_elementor_element_cache` and reuses them for up to 24 hours. Element
18 * caching is on unless the site turns it off: `Document::print_elements()`
19 * treats any value of `elementor_element_cache_ttl` except the literal
20 * `disable` as enabled, and the option does not exist on a default install.
21 * Elementor's own public API for this is `files_manager->clear_cache()`,
22 * which also deletes every generated CSS file on the site. Deleting the one
23 * meta key on the affected documents is the narrow equivalent, and it is
24 * what Elementor's protected `Document::delete_cache()` does.
25 * - **Page and CDN caches.** The same set `LLMs_Txt_Manager` has purged since
26 * 2.1.0, moved here so more than one feature can reach it.
27 *
28 * What cannot be purged is reported rather than ignored. A site behind Varnish,
29 * a CDN, or a caching plugin with no per-URL API will keep serving the old
30 * markup no matter what this class does, and the caller needs to be told that
31 * so it can tell the user to clear it by hand instead of insisting the change
32 * has gone live.
33 *
34 * @package ThinkRank
35 * @subpackage SEO
36 * @since 2.12.0
37 */
38
39 declare(strict_types=1);
40
41 namespace ThinkRank\SEO;
42
43 // Prevent direct access
44 if (!defined('ABSPATH')) {
45 exit;
46 }
47
48 /**
49 * Drops cached copies of pages ThinkRank has just changed.
50 *
51 * @since 2.12.0
52 */
53 class Cache_Purger {
54
55 /**
56 * Elementor's per-document rendered-element cache.
57 *
58 * Mirrors `Elementor\Core\Base\Document::CACHE_META_KEY`. The literal is
59 * used rather than the constant because Elementor may not be loaded, and
60 * because reaching into another plugin's class for a meta key is a harder
61 * dependency than repeating the string with this note attached.
62 *
63 * @since 2.12.0
64 * @var string
65 */
66 public const ELEMENTOR_CACHE_META_KEY = '_elementor_element_cache';
67
68 /**
69 * Post IDs already purged during this request.
70 *
71 * A library-wide alt fill touches hundreds of attachments that between them
72 * appear on a handful of pages, so without this the same document is
73 * cleared once per image. Per-request only: nothing here needs to survive
74 * into the next one, and a stale memory of "already purged" across requests
75 * would be a correctness bug rather than an optimisation.
76 *
77 * @since 2.12.0
78 * @var array<int, bool>
79 */
80 private static array $purged = [];
81
82 /**
83 * Caching plugins ThinkRank knows about, and whether it can purge them.
84 *
85 * Detection is a presence check on the plugin's own constant or class, not
86 * on its purge function: a plugin that loads after this one would report as
87 * absent, and the plugins that matter most for the warning are the ones
88 * that expose no purge function at all.
89 *
90 * @since 2.12.0
91 * @return array<string, array{detect: callable, purgeable: bool}>
92 */
93 private static function known_caches(): array {
94 return [
95 'WP Rocket' => [
96 'detect' => static fn(): bool => defined('WP_ROCKET_VERSION'),
97 'purgeable' => true,
98 ],
99 'W3 Total Cache' => [
100 'detect' => static fn(): bool => defined('W3TC'),
101 'purgeable' => true,
102 ],
103 'WP Super Cache' => [
104 'detect' => static fn(): bool => defined('WPCACHEHOME'),
105 'purgeable' => true,
106 ],
107 'LiteSpeed Cache' => [
108 'detect' => static fn(): bool => defined('LSCWP_V'),
109 'purgeable' => true,
110 ],
111 'Nginx Helper' => [
112 'detect' => static fn(): bool => class_exists('Nginx_Helper'),
113 'purgeable' => true,
114 ],
115 // Everything below is detected but not purgeable from here. Each
116 // one either exposes no per-URL API or needs credentials ThinkRank
117 // does not hold, so the honest answer is a warning to the caller.
118 'WP Fastest Cache' => [
119 'detect' => static fn(): bool => defined('WPFC_MAIN_PATH') || class_exists('WpFastestCache'),
120 'purgeable' => false,
121 ],
122 'SiteGround Optimizer' => [
123 'detect' => static fn(): bool => class_exists('SiteGround_Optimizer\\Supercacher\\Supercacher'),
124 'purgeable' => false,
125 ],
126 'Cache Enabler' => [
127 'detect' => static fn(): bool => class_exists('Cache_Enabler'),
128 'purgeable' => false,
129 ],
130 'Breeze' => [
131 'detect' => static fn(): bool => defined('BREEZE_VERSION'),
132 'purgeable' => false,
133 ],
134 'Hummingbird' => [
135 'detect' => static fn(): bool => defined('WPHB_VERSION'),
136 'purgeable' => false,
137 ],
138 'Cloudflare' => [
139 'detect' => static fn(): bool => class_exists('CF\\WordPress\\Hooks'),
140 'purgeable' => false,
141 ],
142 ];
143 }
144
145 /**
146 * Ask every cache layer to drop its copy of the given URLs.
147 *
148 * Every integration is guarded, so a site running none of them simply gets
149 * the action hook, which other integrations can listen on.
150 *
151 * @since 2.12.0
152 * @param string[] $urls Absolute URLs to purge.
153 * @return void
154 */
155 public static function purge_urls(array $urls): void {
156 $urls = array_values(array_unique(array_filter(array_map('strval', $urls))));
157
158 if ([] === $urls) {
159 return;
160 }
161
162 /**
163 * Fires when ThinkRank changes what a URL serves.
164 *
165 * Cache layers ThinkRank does not know about can listen here and drop
166 * their copy.
167 *
168 * @since 2.12.0
169 *
170 * @param string[] $urls Absolute URLs whose content has changed.
171 */
172 do_action('thinkrank_purge_urls', $urls);
173
174 // Each integration is guarded by its own API rather than by the
175 // detector above, so a plugin whose constant this file guesses wrong
176 // still gets purged. The detector decides only what to warn about.
177 // These are third-party hook names ThinkRank fires, not ours to prefix.
178 foreach ($urls as $url) {
179 do_action('litespeed_purge_url', $url); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
180
181 if (function_exists('w3tc_flush_url')) {
182 w3tc_flush_url($url);
183 }
184 if (function_exists('wpsc_delete_url_cache')) {
185 wpsc_delete_url_cache($url);
186 }
187 }
188
189 // Nginx Helper only exposes a purge-everything hook, and WP Rocket
190 // takes the whole list at once, so both run once per call.
191 do_action('rt_nginx_helper_purge_all'); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
192
193 if (function_exists('rocket_clean_files')) {
194 rocket_clean_files($urls);
195 }
196 }
197
198 /**
199 * Drop cached renderings of specific posts, then their public URLs.
200 *
201 * @since 2.12.0
202 * @param int[] $post_ids Posts whose rendered output is now out of date.
203 * @return int[] The post IDs actually purged in this request.
204 */
205 public static function purge_posts(array $post_ids): array {
206 $purged = [];
207 $urls = [];
208
209 foreach ($post_ids as $post_id) {
210 $post_id = (int) $post_id;
211
212 if ($post_id <= 0 || isset(self::$purged[$post_id])) {
213 continue;
214 }
215
216 self::$purged[$post_id] = true;
217 $purged[] = $post_id;
218
219 // Elementor rebuilds the document on the next front-end request
220 // once the meta is gone. Deleting it is safe on a post Elementor
221 // never touched: the key simply is not there.
222 delete_post_meta($post_id, self::ELEMENTOR_CACHE_META_KEY);
223
224 clean_post_cache($post_id);
225
226 $permalink = get_permalink($post_id);
227 if (is_string($permalink) && '' !== $permalink) {
228 $urls[] = $permalink;
229 }
230 }
231
232 if ([] !== $urls) {
233 self::purge_urls($urls);
234 }
235
236 return $purged;
237 }
238
239 /**
240 * Caching layers that are installed but cannot be purged from here.
241 *
242 * @since 2.12.0
243 * @return string[] Plugin names, in the order they are declared.
244 */
245 public static function unpurgeable_caches(): array {
246 $names = [];
247
248 foreach (self::known_caches() as $name => $cache) {
249 if (!$cache['purgeable'] && ($cache['detect'])()) {
250 $names[] = $name;
251 }
252 }
253
254 return $names;
255 }
256
257 /**
258 * Sentences describing what a caller still has to clear by hand.
259 *
260 * Returned to the MCP abilities so an agent can pass the caveat on instead
261 * of reporting an unqualified success the visitor cannot see yet.
262 *
263 * @since 2.12.0
264 * @return string[] Empty when everything detected was purged.
265 */
266 public static function warnings(): array {
267 $unpurgeable = self::unpurgeable_caches();
268
269 if ([] === $unpurgeable) {
270 return [];
271 }
272
273 return [
274 sprintf(
275 /* translators: %s: comma-separated list of caching plugin names. */
276 __('%s is active and offers no way to clear a single page from here. Clear its cache to see the change on the front end.', 'thinkrank'),
277 implode(', ', $unpurgeable)
278 ),
279 ];
280 }
281
282 /**
283 * Forget which posts were purged this request.
284 *
285 * Only the test suite needs this: a single web request never purges the
286 * same post twice on purpose.
287 *
288 * @since 2.12.0
289 * @return void
290 */
291 public static function reset(): void {
292 self::$purged = [];
293 }
294 }
295