PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.0.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.0.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
thinkrank / includes / seo / class-seo-analyzer-fixer.php

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

248 lines 9.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * One-click fixes for Site SEO Analyzer findings.
4 *
5 * Deliberately conservative. A check is only fixable when the remedy is an
6 * unambiguous SETTINGS change we can make correctly and the user can undo.
7 * Three categories are excluded on principle:
8 *
9 * - **Content.** Site title, tagline and meta descriptions are the user's
10 * words. Writing them for them (even with AI) is authorship, not a fix.
11 * - **Destructive.** Permalink structure is the classic example: switching it
12 * is the textbook SEO recommendation AND it 404s every existing URL. That
13 * needs redirects and a human decision, so it stays advisory.
14 * - **Out of reach.** HTTPS, PHP version, object cache and the wp-config
15 * constants (DISALLOW_FILE_EDIT, WP_DEBUG_DISPLAY) are server or file-system
16 * concerns. Pretending to fix them would be worse than explaining them.
17 *
18 * Everything here is user-initiated: a fix only runs when someone clicks it on
19 * a finding the analyzer already explained.
20 *
21 * @package ThinkRank\SEO
22 * @since 1.28.0
23 */
24
25 declare(strict_types=1);
26
27 namespace ThinkRank\SEO;
28
29 if (!defined('ABSPATH')) {
30 exit;
31 }
32
33 /**
34 * Applies safe, reversible fixes for analyzer checks.
35 */
36 class SEO_Analyzer_Fixer {
37
38 /**
39 * The fixable checks, keyed by the analyzer's check id.
40 *
41 * `warning` is shown in the UI before the user commits when a fix has a
42 * consequence worth naming.
43 *
44 * @return array<string, array{label: string, warning: string}>
45 */
46 public static function fixable(): array {
47 $fixable = [
48 'search_visibility' => [
49 'label' => __('Allow search engines to index this site', 'thinkrank'),
50 // Worth stating plainly: this is exactly the switch people use
51 // to keep a staging site out of Google.
52 'warning' => __('This makes your site visible to search engines. Do not apply it on a staging or private site.', 'thinkrank'),
53 ],
54 'xml_sitemap' => [
55 'label' => __('Enable the XML sitemap', 'thinkrank'),
56 'warning' => '',
57 ],
58 'schema' => [
59 'label' => __('Turn on automatic structured data', 'thinkrank'),
60 'warning' => '',
61 ],
62 'image_alt_text' => [
63 'label' => __('Fill in missing image alt text', 'thinkrank'),
64 'warning' => __('Runs in batches over your media library. With AI alt text enabled this uses your AI provider key.', 'thinkrank'),
65 ],
66 ];
67
68 /**
69 * Filter the checks the analyzer can fix automatically.
70 *
71 * Pro/add-ons registering their own checks through
72 * `thinkrank_seo_analyzer_checks` can register their fixes here.
73 *
74 * @since 1.28.0
75 *
76 * @param array $fixable Check id => ['label' => string, 'warning' => string].
77 */
78 return apply_filters('thinkrank_seo_analyzer_fixable', $fixable);
79 }
80
81 /**
82 * Whether a check can be fixed automatically.
83 *
84 * @param string $check_id Analyzer check id.
85 * @return bool
86 */
87 public static function can_fix(string $check_id): bool {
88 return isset(self::fixable()[$check_id]);
89 }
90
91 /**
92 * Apply the fix for a check.
93 *
94 * @param string $check_id Analyzer check id.
95 * @return array{fixed: bool, message: string, data: array} Outcome.
96 * @throws \Exception When the check is unknown or unfixable.
97 */
98 public function fix(string $check_id): array {
99 if (!self::can_fix($check_id)) {
100 throw new \Exception(esc_html__('This issue cannot be fixed automatically.', 'thinkrank'));
101 }
102
103 switch ($check_id) {
104 case 'search_visibility':
105 return $this->fix_search_visibility();
106 case 'xml_sitemap':
107 return $this->fix_sitemap();
108 case 'schema':
109 return $this->fix_schema();
110 case 'image_alt_text':
111 return $this->fix_image_alt_text();
112 }
113
114 /**
115 * Let a third party handle a fix it registered.
116 *
117 * @since 1.28.0
118 *
119 * @param array|null $result ['fixed' => bool, 'message' => string, 'data' => array].
120 * @param string $check_id Check id being fixed.
121 */
122 $result = apply_filters('thinkrank_seo_analyzer_apply_fix', null, $check_id);
123
124 if (is_array($result)) {
125 return array_merge(['fixed' => false, 'message' => '', 'data' => []], $result);
126 }
127
128 throw new \Exception(esc_html__('No handler is registered for this fix.', 'thinkrank'));
129 }
130
131 /**
132 * Untick "Discourage search engines from indexing this site".
133 *
134 * @return array
135 */
136 private function fix_search_visibility(): array {
137 update_option('blog_public', 1);
138
139 return [
140 'fixed' => true,
141 'message' => __('Search engines can now index this site.', 'thinkrank'),
142 'data' => [],
143 ];
144 }
145
146 /**
147 * Enable XML sitemap output.
148 *
149 * @return array
150 * @throws \Exception When the sitemap setting cannot be saved.
151 */
152 private function fix_sitemap(): array {
153 // 'site' is the context every real save path uses; 'global' is not a
154 // supported context and Abstract_SEO_Manager rejects it — which this
155 // fixer originally ignored, reporting success while saving nothing.
156 $generator = new Sitemap_Generator();
157 $settings = $generator->get_settings('site');
158 $settings = is_array($settings) ? $settings : [];
159
160 $settings['enabled'] = true;
161
162 if (!$generator->save_settings('site', null, $settings)) {
163 throw new \Exception(esc_html__('The sitemap setting could not be saved. Check the PHP error log for the ThinkRank line naming the cause.', 'thinkrank'));
164 }
165
166 return [
167 'fixed' => true,
168 'message' => __('Your XML sitemap is now enabled.', 'thinkrank'),
169 'data' => [],
170 ];
171 }
172
173 /**
174 * Turn on automatic schema generation.
175 *
176 * @return array
177 * @throws \Exception When the schema module is unavailable or the setting cannot be saved.
178 */
179 private function fix_schema(): array {
180 if (!class_exists('ThinkRank\\SEO\\Schema_Management_System')) {
181 throw new \Exception(esc_html__('The schema module is unavailable.', 'thinkrank'));
182 }
183
184 // 'site' is the storage context; 'schema_management_system' is the
185 // MANAGER type, not a context — passing it made save_settings() reject
186 // the write while this fixer reported success.
187 $schema = new Schema_Management_System();
188 $settings = $schema->get_settings('site');
189 $settings = is_array($settings) ? $settings : [];
190
191 $settings['auto_generate_schema'] = true;
192
193 // Seed the two types that apply to virtually every site, so enabling
194 // the toggle actually produces output rather than an empty config.
195 if (empty($settings['enabled_schema_types']) || !is_array($settings['enabled_schema_types'])) {
196 $settings['enabled_schema_types'] = ['Article', 'WebPage'];
197 }
198
199 if (!$schema->save_settings('site', null, $settings)) {
200 throw new \Exception(esc_html__('The schema setting could not be saved. Check the PHP error log for the ThinkRank line naming the cause.', 'thinkrank'));
201 }
202
203 return [
204 'fixed' => true,
205 'message' => __('Automatic structured data is now enabled.', 'thinkrank'),
206 'data' => [],
207 ];
208 }
209
210 /**
211 * Fill missing alt text across one batch of the media library.
212 *
213 * Returns progress rather than looping to completion: a large library
214 * would exceed the request timeout, and in AI mode each image is a paid
215 * call. The client re-invokes while `remaining` is above zero, so the user
216 * sees progress and can stop.
217 *
218 * @return array
219 */
220 private function fix_image_alt_text(): array {
221 $manager = new Image_SEO_Manager();
222 $result = $manager->bulk_fill_missing_alt(['limit' => 50, 'overwrite' => false]);
223
224 $updated = (int) ($result['updated'] ?? 0);
225 $remaining = (int) ($result['remaining'] ?? 0);
226
227 return [
228 'fixed' => true,
229 'message' => $remaining > 0
230 ? sprintf(
231 /* translators: 1: images updated in this batch, 2: images still to process. */
232 __('Added alt text to %1$d images. %2$d still to go — run the fix again to continue.', 'thinkrank'),
233 $updated,
234 $remaining
235 )
236 : sprintf(
237 /* translators: %d: number of images updated. */
238 __('Added alt text to %d images. Your media library is done.', 'thinkrank'),
239 $updated
240 ),
241 'data' => [
242 'updated' => $updated,
243 'remaining' => $remaining,
244 ],
245 ];
246 }
247 }
248