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/core/class-activator.php +266 -20 1.0.12.7.0 View file →
@@ -1,5 +1,6 @@
1 1 <?php
2 +
2 3 /**
3 4 * Plugin Activator Class
4 5 *
5 6 * Handles plugin activation tasks
@@ -26,24 +27,93 @@
26 27 *
27 28 * @since 1.0.0
28 29 */
29 30 class Activator {
30 -
31 +
31 32 /**
33 + * Option the uninstaller sets to record a deliberate removal.
34 + *
35 + * Pro's Free_Plugin_Installer skips its silent auto-install while this is
36 + * set, so activating again — the user asking for the plugin back — has to
37 + * clear it. Keep in sync with uninstall.php.
38 + */
39 + public const UNINSTALLED_OPTION = 'thinkrank_uninstalled';
40 +
41 + /**
32 42 * Plugin activation tasks
33 - *
43 + *
34 44 * @return void
35 45 * @throws \Exception If activation fails
36 46 */
37 47 public function activate(): void {
48 + delete_option(self::UNINSTALLED_OPTION);
49 +
38 50 $this->check_requirements();
39 51 $this->create_database_tables();
52 + // Both must precede set_default_options(): they read
53 + // `thinkrank_version`, which that method creates.
54 + $this->retire_sitemap_legacy_fallback();
55 + $this->seed_feed_defaults();
40 56 $this->set_default_options();
57 + $this->setup_indexnow_key();
41 58 $this->schedule_cron_jobs();
59 + $this->restore_webroot_artifacts();
42 60 $this->set_activation_flag();
61 +
62 + // Grant the admin capabilities here rather than waiting for the `init`
63 + // hook Role_Manager registers, so the menu is reachable on the very
64 + // first admin request after activation.
65 + Capability_Manager::ensure();
43 66 }
44 -
67 +
45 68 /**
69 + * Setup IndexNow API Key
70 + *
71 + * Generates a unique 128-bit key and creates the key file in the root directory.
72 + *
73 + * @return void
74 + */
75 + private function setup_indexnow_key(): void {
76 + $option_name = 'thinkrank_instant_indexing_settings';
77 + $settings = get_option($option_name, []);
78 +
79 + // Check if key exists
80 + if (empty($settings['api_key'])) {
81 + try {
82 + // Generate 128-bit key (32 hex characters)
83 + // Using bin2hex(random_bytes(16)) as requested
84 + $key = bin2hex(random_bytes(16));
85 +
86 + // Save to options
87 + $settings['api_key'] = $key;
88 +
89 + // Initialize default post types if not set
90 + if (!isset($settings['auto_submit_post_types'])) {
91 + $settings['auto_submit_post_types'] = ['post', 'page'];
92 + }
93 +
94 + update_option($option_name, $settings);
95 +
96 + // Create the key file in WordPress root using WP_Filesystem
97 + $file_path = ABSPATH . $key . '.txt';
98 + global $wp_filesystem;
99 + if (!function_exists('WP_Filesystem')) {
100 + require_once ABSPATH . 'wp-admin/includes/file.php';
101 + }
102 + WP_Filesystem();
103 + if ($wp_filesystem && $wp_filesystem->is_writable(ABSPATH)) {
104 + $wp_filesystem->put_contents($file_path, $key, FS_CHMOD_FILE);
105 + }
106 + } catch (\Exception $e) {
107 + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
108 + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
109 + error_log('ThinkRank: Failed to create IndexNow key file: ' . $e->getMessage());
110 + }
111 + }
112 + }
113 + }
114 +
115 + /**
46 116 * Check system requirements
47 117 *
48 118 * @return void
49 119 * @throws \Exception If requirements not met
@@ -49,10 +119,10 @@
49 119 * @throws \Exception If requirements not met
50 120 */
51 121 private function check_requirements(): void {
52 122 // PHP version check
53 - if (version_compare(PHP_VERSION, '8.0', '<')) {
54 - throw new \Exception('ThinkRank requires PHP 8.0 or higher');
123 + if (version_compare(PHP_VERSION, '7.4', '<')) {
124 + throw new \Exception('ThinkRank requires PHP 7.4 or higher');
55 125 }
56 126
57 127 // WordPress version check
58 128 if (version_compare(get_bloginfo('version'), '6.0', '<')) {
@@ -57,9 +127,9 @@
57 127 // WordPress version check
58 128 if (version_compare(get_bloginfo('version'), '6.0', '<')) {
59 129 throw new \Exception('ThinkRank requires WordPress 6.0 or higher');
60 130 }
61 -
131 +
62 132 // Required PHP extensions
63 133 $required_extensions = ['curl', 'json', 'mbstring'];
64 134 foreach ($required_extensions as $extension) {
65 135 if (!extension_loaded($extension)) {
@@ -65,15 +135,20 @@
65 135 if (!extension_loaded($extension)) {
66 136 throw new \Exception(sprintf("Required PHP extension '%s' is not loaded", esc_html($extension)));
67 137 }
68 138 }
69 -
139 +
70 140 // Check if we can write to WordPress root directory (for robots.txt, llms.txt, sitemaps)
71 141 if (!wp_is_writable(ABSPATH)) {
72 - throw new \Exception('WordPress root directory is not writable');
142 + // Log warning but don't block activation — some hosts restrict ABSPATH writes
143 + // and file-writing features will gracefully degrade via WP_Filesystem checks
144 + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
145 + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
146 + error_log('ThinkRank: WordPress root directory is not writable. Some features (robots.txt, llms.txt, sitemaps) may not work.');
147 + }
73 148 }
74 149 }
75 -
150 +
76 151 /**
77 152 * Create database tables
78 153 *
79 154 * Uses the consolidated Database_Schema class to create all 11 ThinkRank tables:
@@ -94,12 +169,11 @@
94 169 throw new \Exception($error_message);
95 170 }
96 171
97 172 // Add performance indexes for Phase 2 optimization
98 - $index_results = $schema->add_performance_indexes();
99 - if (!$index_results) {
100 - // Performance indexes could not be created - activation continues
101 - }
173 + // Best effort: activation continues if the performance indexes
174 + // cannot be created.
175 + $schema->add_performance_indexes();
102 176
103 177 // Database tables created successfully
104 178
105 179 } catch (\Exception $e) {
@@ -107,10 +181,102 @@
107 181 }
108 182 }
109 183
110 184
111 -
185 +
112 186 /**
187 + * On a brand-new install, close the pre-2.1.1 sitemap ownership fallback
188 + * before it can ever open.
189 + *
190 + * {@see thinkrank_webroot_sitemap_is_ours()} keeps one narrow escape hatch:
191 + * a sitemap written before 2.1.1 with `enable_styling` off carries neither
192 + * the marker nor our XSL href, so it can only be recognised by the name the
193 + * stored settings derive. That fallback is gated on this install never
194 + * having written a marked sitemap — but "never written one" describes two
195 + * completely different sites:
196 + *
197 + * - a pre-2.1.1 install that has not regenerated since upgrading, which
198 + * is exactly what the fallback exists to recover; and
199 + * - a fresh install that simply has not generated yet, which cannot have
200 + * a legacy file of ours on disk at all.
201 + *
202 + * On the second, the fallback has nothing to recover and can only delete
203 + * somebody else's sitemap from one of the canonical names — #515 again, in
204 + * a site that never had the problem the fallback addresses. It is not a
205 + * narrow window either: `regenerate_sitemap_from_settings()` returns early
206 + * while the master `enabled` flag is off, so a site with sitemaps disabled
207 + * and styling saved off never records a marked write, and stays exposed for
208 + * as long as it stays in that configuration.
209 + *
210 + * Recording the marker here on a fresh install separates the two cases. An
211 + * upgrade does not reach this code — WordPress does not re-run the
212 + * activation hook on update — so a genuine pre-2.1.1 site keeps the
213 + * fallback until its first marked write, exactly as before.
214 + *
215 + * `thinkrank_version` is the signal: set_default_options() adds it only
216 + * when absent and never updates it, so it is missing on the very first
217 + * activation and present on every one after.
218 + *
219 + * @since 2.1.1
220 + *
221 + * @return void
222 + */
223 + private function retire_sitemap_legacy_fallback(): void {
224 + if (get_option('thinkrank_version') !== false) {
225 + return;
226 + }
227 +
228 + require_once THINKRANK_PLUGIN_DIR . 'includes/cleanup-webroot.php';
229 +
230 + add_option(THINKRANK_SITEMAP_MARKED_WRITE_OPTION, '1', '', false);
231 + }
232 +
233 + /**
234 + * Give a brand-new install the feed posture the competitors ship with.
235 + *
236 + * The feed controls (#635) default to off in
237 + * {@see Site_Identity_Manager::get_default_settings()}, and they have to:
238 + * two of the three change what a site already publishes. Signing every
239 + * entry adds a line to what existing subscribers receive, and noindexing
240 + * feeds withdraws URLs a site may have had indexed for years — on a podcast
241 + * site, whose feed has to stay indexable, silently at that. Neither belongs
242 + * in a plugin update.
243 + *
244 + * A first install has no subscribers and no indexed feed, so there is
245 + * nothing to change and the protective defaults are simply the right
246 + * starting point — which is what The SEO Framework, Yoast and Rank Math all
247 + * ship. Seeding them here rather than in the defaults is what separates the
248 + * two cases.
249 + *
250 + * Excerpt-only is left off even here: it changes what readers get rather
251 + * than what scrapers can take, and that is the site owner's call.
252 + *
253 + * Same signal and same reasoning as {@see self::retire_sitemap_legacy_fallback()}:
254 + * `thinkrank_version` is absent only on the very first activation, and an
255 + * upgrade does not re-run the activation hook at all.
256 + *
257 + * @since 2.7.0
258 + *
259 + * @return void
260 + */
261 + private function seed_feed_defaults(): void {
262 + if (get_option('thinkrank_version') !== false) {
263 + return;
264 + }
265 +
266 + $manager = new \ThinkRank\SEO\Site_Identity_Manager();
267 +
268 + $manager->save_settings(
269 + 'site',
270 + null,
271 + [
272 + 'feed_source_link' => true,
273 + 'feed_noindex' => true,
274 + ]
275 + );
276 + }
277 +
278 + /**
113 279 * Set default plugin options
114 280 *
115 281 * @return void
116 282 */
@@ -117,9 +283,9 @@
117 283 private function set_default_options(): void {
118 284 $default_options = [
119 285 'thinkrank_version' => THINKRANK_VERSION,
120 286
121 - 'thinkrank_ai_provider' => 'openai',
287 + 'thinkrank_ai_provider' => \ThinkRank\Core\Settings::AI_PROVIDER_NONE,
122 288 'thinkrank_cache_duration' => 3600, // 1 hour
123 289 'thinkrank_max_requests_per_minute' => 10,
124 290 'thinkrank_enable_logging' => true,
125 291 'thinkrank_auto_optimize' => false,
@@ -124,9 +290,9 @@
124 290 'thinkrank_enable_logging' => true,
125 291 'thinkrank_auto_optimize' => false,
126 292 'thinkrank_seo_score_threshold' => 70,
127 293 ];
128 -
294 +
129 295 foreach ($default_options as $option_name => $option_value) {
130 296 if (get_option($option_name) === false) {
131 297 add_option($option_name, $option_value);
132 298 }
@@ -131,13 +297,85 @@
131 297 add_option($option_name, $option_value);
132 298 }
133 299 }
134 300 }
135 -
136 301
302 +
137 303 /**
304 + * Republish the web-root artifacts deactivation took away.
305 + *
306 + * Deactivation removes the published sitemap, robots.txt and llms.txt so an
307 + * inactive ThinkRank stops shadowing whatever the user switched to (#510).
308 + * That is only safe if switching the plugin back on puts them back, which is
309 + * what this does.
310 + *
311 + * Restores strictly what {@see Deactivator::REPUBLISH_OPTION} recorded as
312 + * having been removed — never "everything the settings would allow", which
313 + * on a fresh install would publish files the site never had.
314 + *
315 + * The sitemap goes through schedule_regeneration() rather than being built
316 + * inline: a full rebuild on a large site is far too slow to sit inside an
317 + * activation request, and the debounced hook already respects the master
318 + * `enabled` flag. robots.txt and llms.txt are single small writes, so they
319 + * happen here.
320 + *
321 + * @since 2.1.0
322 + *
323 + * @return void
324 + */
325 + private function restore_webroot_artifacts(): void {
326 + $republish = get_option(Deactivator::REPUBLISH_OPTION, null);
327 +
328 + if ($republish === null) {
329 + // No recorded deactivation — a first install, or an activation that
330 + // already consumed the record.
331 + return;
332 + }
333 +
334 + // Consume it first. A restore that fatals must not re-run on every
335 + // subsequent activation, and each entry below is independently guarded.
336 + delete_option(Deactivator::REPUBLISH_OPTION);
337 +
338 + if (!is_array($republish)) {
339 + return;
340 + }
341 +
342 + try {
343 + if (in_array('sitemap', $republish, true) && class_exists('ThinkRank\\SEO\\Sitemap_Generator')) {
344 + // Read-only instance: the hook-registering one would bind a
345 + // second set of content-change listeners to this request.
346 + (new \ThinkRank\SEO\Sitemap_Generator(false))->schedule_regeneration();
347 + }
348 +
349 + if (in_array('robots', $republish, true) && class_exists('ThinkRank\\SEO\\Site_Identity_Manager')) {
350 + (new \ThinkRank\SEO\Site_Identity_Manager())->sync_robots_txt_file();
351 + }
352 +
353 + if (in_array('llms', $republish, true) && class_exists('ThinkRank\\SEO\\LLMs_Txt_Manager')) {
354 + $llms = new \ThinkRank\SEO\LLMs_Txt_Manager();
355 + $content = $llms->get_published_content();
356 +
357 + // write_llms_txt_to_file() enforces the enabled toggle and the
358 + // delivery mode itself, so an empty document is the only case
359 + // worth short-circuiting here.
360 + if ($content !== '') {
361 + $llms->write_llms_txt_to_file($content);
362 + }
363 + }
364 + } catch (\Throwable $e) {
365 + // A failed republish must not block activation — the user would be
366 + // left unable to switch the plugin on at all. The artifacts rebuild
367 + // on the next content or settings save.
368 + if (defined('WP_DEBUG') && WP_DEBUG) {
369 + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
370 + error_log('ThinkRank: failed to restore web-root artifacts: ' . $e->getMessage());
371 + }
372 + }
373 + }
374 +
375 + /**
138 376 * Schedule cron jobs
139 - *
377 + *
140 378 * @return void
141 379 */
142 380 private function schedule_cron_jobs(): void {
143 381
@@ -144,15 +382,15 @@
144 382 // Schedule cache cleanup
145 383 if (!wp_next_scheduled('thinkrank_cache_cleanup')) {
146 384 wp_schedule_event(time(), 'daily', 'thinkrank_cache_cleanup');
147 385 }
148 -
386 +
149 387 // Schedule usage analytics
150 388 if (!wp_next_scheduled('thinkrank_usage_analytics')) {
151 389 wp_schedule_event(time(), 'weekly', 'thinkrank_usage_analytics');
152 390 }
153 391 }
154 -
392 +
155 393 /**
156 394 * Set activation flag for first-time setup
157 395 *
158 396 * @return void
@@ -162,6 +400,14 @@
162 400 update_option('thinkrank_activation_time', time());
163 401
164 402 // Set flag for showing welcome screen
165 403 update_option('thinkrank_show_welcome', true);
404 +
405 + // Trigger a one-time redirect to the Setup Wizard on the next admin load,
406 + // but only when the wizard has not already been completed. A short-lived
407 + // transient is used so it auto-expires and never fires for bulk/network
408 + // activations that skip the redirect window.
409 + if (!get_option('thinkrank_setup_wizard_completed')) {
410 + set_transient('thinkrank_setup_wizard_redirect', 1, 60);
411 + }
166 412 }
167 413 }