PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.10.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.10.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 1.0.0 1.0.1 All 51 releases
thinkrank / includes / abilities / maintenance / class-purge-caches.php

class-purge-caches.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 2.10.0, at includes/abilities/maintenance/class-purge-caches.php

259 lines 8.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Purge caches ability.
4 *
5 * @package ThinkRank\Abilities\Maintenance
6 */
7
8 declare(strict_types=1);
9
10 namespace ThinkRank\Abilities\Maintenance;
11
12 use ThinkRank\Abilities\Ability_Base;
13
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit; // Exit if accessed directly.
16 }
17
18 /**
19 * Clears ThinkRank's own caches.
20 *
21 * ThinkRank caches Google and analytics responses, schema output and the site
22 * audit, so "it is stale, clear it" is a real question — and it had no
23 * supported answer at all: no ability, and no admin control either (#676).
24 *
25 * The recurrence is the argument for it rather than any single report. #629 was
26 * sitemap regeneration silently deferred to cron, and #235 was the Search
27 * Console property list cached for thirty minutes with no way to invalidate it;
28 * both would have been self-service with this.
29 *
30 * Scoped rather than a single blunt flush, so clearing a stale Search Console
31 * list does not also throw away an hour-old site audit that costs real work to
32 * rebuild.
33 */
34 class Purge_Caches extends Ability_Base {
35
36 /**
37 * Scope => human label, and the order they are reported in.
38 *
39 * @var array<string, string>
40 */
41 private const SCOPES = [
42 'analyzer' => 'Site SEO Analyzer audit',
43 'schema' => 'Schema output cache',
44 'ai' => 'AI response cache',
45 'integrations' => 'Google integration responses',
46 'content_type' => 'Content Type Matrix resolution',
47 'transients' => 'ThinkRank transients',
48 // Not a cache in the same sense — it rebuilds a published file rather
49 // than dropping a stored value — but it is what a caller looking at a
50 // stale sitemap reaches for, and the docblock above already named that
51 // problem as a reason this ability exists (#764).
52 'sitemap' => 'Served XML sitemap (rebuilt, not just cleared)',
53 ];
54
55 /**
56 * Constructor.
57 */
58 public function __construct() {
59 $this->id = 'thinkrank/purge-caches';
60 $this->label = __( 'Purge ThinkRank Caches', 'thinkrank' );
61 $this->description = __( 'Clear ThinkRank\'s cached data when it is showing something stale — the site audit, schema output, AI responses, Google integration responses, or all of them. Pass scopes to clear only what you need; omit it to clear everything. Nothing is deleted except caches: settings and content are untouched, and each cache rebuilds on the next request. After clearing the analyzer scope, use run-seo-analyzer to rebuild the audit immediately rather than waiting for the next request. The "sitemap" scope is the exception to "rebuilds on the next request": the sitemap is a published file, so that scope rebuilds it there and then and reports whether it succeeded.', 'thinkrank' );
62 }
63
64 /**
65 * {@inheritDoc}
66 *
67 * @return array<string, bool|float|string>
68 */
69 public function get_annotations() {
70 return [
71 'readonly' => false,
72 // Caches only. Nothing a user authored is lost, and everything
73 // cleared is rebuilt on demand — the cost is latency, not data.
74 'destructive' => false,
75 'idempotent' => true,
76 'priority' => 0.5,
77 'openWorldHint' => false,
78 ];
79 }
80
81 /**
82 * {@inheritDoc}
83 *
84 * @return array<string, mixed>
85 */
86 public function get_input_schema() {
87 return [
88 'type' => 'object',
89 'additionalProperties' => false,
90 'properties' => [
91 'scopes' => [
92 'type' => 'array',
93 'items' => [
94 'type' => 'string',
95 'enum' => array_keys( self::SCOPES ),
96 ],
97 'description' => __( 'Which caches to clear. Omit to clear all of them.', 'thinkrank' ),
98 ],
99 ],
100 ];
101 }
102
103 /**
104 * {@inheritDoc}
105 *
106 * @return array<string, mixed>
107 */
108 public function get_output_schema() {
109 return [
110 'type' => 'object',
111 'properties' => [
112 'success' => [ 'type' => 'boolean' ],
113 'cleared' => [
114 'type' => 'array',
115 'items' => [ 'type' => 'string' ],
116 'description' => __( 'Scopes that were cleared.', 'thinkrank' ),
117 ],
118 'skipped' => [
119 'type' => 'array',
120 'items' => [ 'type' => 'string' ],
121 'description' => __( 'Scopes whose subsystem is not present on this install, so there was nothing to clear.', 'thinkrank' ),
122 ],
123 'failed' => [
124 'type' => 'array',
125 'items' => [ 'type' => 'string' ],
126 'description' => __( 'Scopes that exist but could not be cleared or rebuilt. For "sitemap" this means the served sitemap did not change, because another rebuild held the lock or the files could not be written or removed. Try again shortly; the Sitemap settings screen shows any recorded reason.', 'thinkrank' ),
127 ],
128 ],
129 ];
130 }
131
132 /**
133 * Execute ability.
134 *
135 * @param array<string, mixed> $input Ability input payload.
136 * @return array<string, mixed>
137 */
138 public function execute( $input ) {
139 $input = (array) $input;
140 $requested = isset( $input['scopes'] ) && is_array( $input['scopes'] ) && ! empty( $input['scopes'] )
141 ? array_values( array_intersect( array_keys( self::SCOPES ), array_map( 'strval', $input['scopes'] ) ) )
142 : array_keys( self::SCOPES );
143
144 $results = [
145 'cleared' => [],
146 'skipped' => [],
147 'failed' => [],
148 ];
149
150 foreach ( $requested as $scope ) {
151 $results[ $this->purge( $scope ) ][] = $scope;
152 }
153
154 // `success` stays true when a scope failed: the call itself ran, and
155 // every other requested scope was still cleared. The failure is
156 // reported per scope in `failed`, which is where a caller has to look
157 // to know which cache is still stale.
158 return [ 'success' => true ] + $results;
159 }
160
161 /**
162 * Clear one scope.
163 *
164 * Three outcomes rather than a bool. A bool had only "cleared" and
165 * "skipped" to map onto, and skipped is documented as "the subsystem is
166 * not present", so a sitemap rebuild that lost the lock or failed to write
167 * was reported to the caller as a sitemap that does not exist.
168 *
169 * @since 2.10.0 Returns 'cleared', 'skipped' or 'failed' instead of a bool.
170 *
171 * @param string $scope Scope key.
172 * @return string 'cleared', 'skipped' (subsystem absent) or 'failed'.
173 */
174 private function purge( string $scope ): string {
175 switch ( $scope ) {
176 case 'analyzer':
177 if ( ! class_exists( 'ThinkRank\\SEO\\SEO_Analyzer' ) ) {
178 return 'skipped';
179 }
180 ( new \ThinkRank\SEO\SEO_Analyzer() )->flush_cache();
181 return 'cleared';
182
183 case 'schema':
184 if ( ! class_exists( 'ThinkRank\\SEO\\Schema_Cache_Manager' ) ) {
185 return 'skipped';
186 }
187 ( new \ThinkRank\SEO\Schema_Cache_Manager() )->clear_all();
188 return 'cleared';
189
190 case 'ai':
191 if ( ! class_exists( 'ThinkRank\\AI\\Cache_Manager' ) ) {
192 return 'skipped';
193 }
194 ( new \ThinkRank\AI\Cache_Manager() )->clear_all();
195 return 'cleared';
196
197 case 'integrations':
198 if ( ! method_exists( 'ThinkRank\\API\\Integrations_Endpoint', 'purge_search_console_sites_cache' ) ) {
199 return 'skipped';
200 }
201 \ThinkRank\API\Integrations_Endpoint::purge_search_console_sites_cache();
202 return 'cleared';
203
204 case 'content_type':
205 if ( ! method_exists( 'ThinkRank\\SEO\\Content_Type_Settings', 'flush_cache' ) ) {
206 return 'skipped';
207 }
208 \ThinkRank\SEO\Content_Type_Settings::flush_cache();
209 return 'cleared';
210
211 case 'transients':
212 $this->purge_transients();
213 return 'cleared';
214
215 case 'sitemap':
216 if ( ! class_exists( 'ThinkRank\\SEO\\Sitemap_Generator' ) ) {
217 return 'skipped';
218 }
219
220 // Rebuilds rather than invalidates: the sitemap is a static file
221 // on most installs, so there is no next request that would
222 // regenerate it. Lock-guarded, and reports false when another
223 // process holds the lock or the write fails, so the caller is
224 // told the served file did not change.
225 return ( new \ThinkRank\SEO\Sitemap_Generator( false ) )
226 ->regenerate_sitemap_from_settings() ? 'cleared' : 'failed';
227 }
228
229 return 'skipped';
230 }
231
232 /**
233 * Delete ThinkRank's prefixed transients.
234 *
235 * Mirrors the sweep Deactivator runs, so the two cannot disagree about what
236 * counts as a ThinkRank transient.
237 *
238 * @return void
239 */
240 private function purge_transients(): void {
241 global $wpdb;
242
243 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- deleting the cache rows themselves; there is no core API for a prefix sweep.
244 $wpdb->query(
245 $wpdb->prepare(
246 "DELETE FROM {$wpdb->options}
247 WHERE option_name LIKE %s
248 OR option_name LIKE %s",
249 $wpdb->esc_like( '_transient_thinkrank_' ) . '%',
250 $wpdb->esc_like( '_transient_timeout_thinkrank_' ) . '%'
251 )
252 );
253
254 if ( function_exists( 'wp_cache_flush_group' ) ) {
255 wp_cache_flush_group( 'thinkrank' );
256 }
257 }
258 }
259