PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.1.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.1.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 / core / class-activator.php

class-activator.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 2.1.0, at includes/core/class-activator.php

318 lines 11.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Plugin Activator Class
5 *
6 * Handles plugin activation tasks
7 *
8 * @package ThinkRank\Core
9 * @since 1.0.0
10 */
11
12 declare(strict_types=1);
13
14 namespace ThinkRank\Core;
15
16 use ThinkRank\Database\Database_Schema;
17
18 // Prevent direct access
19 if (!defined('ABSPATH')) {
20 exit;
21 }
22
23 /**
24 * Activator Class
25 *
26 * Single Responsibility: Handle plugin activation tasks only
27 *
28 * @since 1.0.0
29 */
30 class Activator {
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 /**
42 * Plugin activation tasks
43 *
44 * @return void
45 * @throws \Exception If activation fails
46 */
47 public function activate(): void {
48 delete_option(self::UNINSTALLED_OPTION);
49
50 $this->check_requirements();
51 $this->create_database_tables();
52 $this->set_default_options();
53 $this->setup_indexnow_key();
54 $this->schedule_cron_jobs();
55 $this->restore_webroot_artifacts();
56 $this->set_activation_flag();
57
58 // Grant the admin capabilities here rather than waiting for the `init`
59 // hook Role_Manager registers, so the menu is reachable on the very
60 // first admin request after activation.
61 Capability_Manager::ensure();
62 }
63
64 /**
65 * Setup IndexNow API Key
66 *
67 * Generates a unique 128-bit key and creates the key file in the root directory.
68 *
69 * @return void
70 */
71 private function setup_indexnow_key(): void {
72 $option_name = 'thinkrank_instant_indexing_settings';
73 $settings = get_option($option_name, []);
74
75 // Check if key exists
76 if (empty($settings['api_key'])) {
77 try {
78 // Generate 128-bit key (32 hex characters)
79 // Using bin2hex(random_bytes(16)) as requested
80 $key = bin2hex(random_bytes(16));
81
82 // Save to options
83 $settings['api_key'] = $key;
84
85 // Initialize default post types if not set
86 if (!isset($settings['auto_submit_post_types'])) {
87 $settings['auto_submit_post_types'] = ['post', 'page'];
88 }
89
90 update_option($option_name, $settings);
91
92 // Create the key file in WordPress root using WP_Filesystem
93 $file_path = ABSPATH . $key . '.txt';
94 global $wp_filesystem;
95 if (!function_exists('WP_Filesystem')) {
96 require_once ABSPATH . 'wp-admin/includes/file.php';
97 }
98 WP_Filesystem();
99 if ($wp_filesystem && $wp_filesystem->is_writable(ABSPATH)) {
100 $wp_filesystem->put_contents($file_path, $key, FS_CHMOD_FILE);
101 }
102 } catch (\Exception $e) {
103 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
104 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
105 error_log('ThinkRank: Failed to create IndexNow key file: ' . $e->getMessage());
106 }
107 }
108 }
109 }
110
111 /**
112 * Check system requirements
113 *
114 * @return void
115 * @throws \Exception If requirements not met
116 */
117 private function check_requirements(): void {
118 // PHP version check
119 if (version_compare(PHP_VERSION, '7.4', '<')) {
120 throw new \Exception('ThinkRank requires PHP 7.4 or higher');
121 }
122
123 // WordPress version check
124 if (version_compare(get_bloginfo('version'), '6.0', '<')) {
125 throw new \Exception('ThinkRank requires WordPress 6.0 or higher');
126 }
127
128 // Required PHP extensions
129 $required_extensions = ['curl', 'json', 'mbstring'];
130 foreach ($required_extensions as $extension) {
131 if (!extension_loaded($extension)) {
132 throw new \Exception(sprintf("Required PHP extension '%s' is not loaded", esc_html($extension)));
133 }
134 }
135
136 // Check if we can write to WordPress root directory (for robots.txt, llms.txt, sitemaps)
137 if (!wp_is_writable(ABSPATH)) {
138 // Log warning but don't block activation — some hosts restrict ABSPATH writes
139 // and file-writing features will gracefully degrade via WP_Filesystem checks
140 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
141 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
142 error_log('ThinkRank: WordPress root directory is not writable. Some features (robots.txt, llms.txt, sitemaps) may not work.');
143 }
144 }
145 }
146
147 /**
148 * Create database tables
149 *
150 * Uses the consolidated Database_Schema class to create all 11 ThinkRank tables:
151 * - SEO Tables (7): Settings, Analysis, Keywords, Schema, Social, Performance, Local
152 * - AI/Core Tables (4): AI Cache, AI Usage, Content Briefs, SEO Scores
153 *
154 * @return void
155 * @throws \Exception If table creation fails
156 */
157 private function create_database_tables(): void {
158 try {
159 // Use the Database_Schema class to create all 11 ThinkRank tables
160 $schema = new Database_Schema();
161 $results = $schema->create_tables();
162
163 if (!$results['success']) {
164 $error_message = 'Failed to create database tables: ' . implode(', ', $results['errors']);
165 throw new \Exception($error_message);
166 }
167
168 // Add performance indexes for Phase 2 optimization
169 // Best effort: activation continues if the performance indexes
170 // cannot be created.
171 $schema->add_performance_indexes();
172
173 // Database tables created successfully
174
175 } catch (\Exception $e) {
176 throw new \Exception('Database table creation failed: ' . esc_html($e->getMessage()));
177 }
178 }
179
180
181
182 /**
183 * Set default plugin options
184 *
185 * @return void
186 */
187 private function set_default_options(): void {
188 $default_options = [
189 'thinkrank_version' => THINKRANK_VERSION,
190
191 'thinkrank_ai_provider' => 'openai',
192 'thinkrank_cache_duration' => 3600, // 1 hour
193 'thinkrank_max_requests_per_minute' => 10,
194 'thinkrank_enable_logging' => true,
195 'thinkrank_auto_optimize' => false,
196 'thinkrank_seo_score_threshold' => 70,
197 ];
198
199 foreach ($default_options as $option_name => $option_value) {
200 if (get_option($option_name) === false) {
201 add_option($option_name, $option_value);
202 }
203 }
204 }
205
206
207 /**
208 * Republish the web-root artifacts deactivation took away.
209 *
210 * Deactivation removes the published sitemap, robots.txt and llms.txt so an
211 * inactive ThinkRank stops shadowing whatever the user switched to (#510).
212 * That is only safe if switching the plugin back on puts them back, which is
213 * what this does.
214 *
215 * Restores strictly what {@see Deactivator::REPUBLISH_OPTION} recorded as
216 * having been removed — never "everything the settings would allow", which
217 * on a fresh install would publish files the site never had.
218 *
219 * The sitemap goes through schedule_regeneration() rather than being built
220 * inline: a full rebuild on a large site is far too slow to sit inside an
221 * activation request, and the debounced hook already respects the master
222 * `enabled` flag. robots.txt and llms.txt are single small writes, so they
223 * happen here.
224 *
225 * @since 2.1.0
226 *
227 * @return void
228 */
229 private function restore_webroot_artifacts(): void {
230 $republish = get_option(Deactivator::REPUBLISH_OPTION, null);
231
232 if ($republish === null) {
233 // No recorded deactivation — a first install, or an activation that
234 // already consumed the record.
235 return;
236 }
237
238 // Consume it first. A restore that fatals must not re-run on every
239 // subsequent activation, and each entry below is independently guarded.
240 delete_option(Deactivator::REPUBLISH_OPTION);
241
242 if (!is_array($republish)) {
243 return;
244 }
245
246 try {
247 if (in_array('sitemap', $republish, true) && class_exists('ThinkRank\\SEO\\Sitemap_Generator')) {
248 // Read-only instance: the hook-registering one would bind a
249 // second set of content-change listeners to this request.
250 (new \ThinkRank\SEO\Sitemap_Generator(false))->schedule_regeneration();
251 }
252
253 if (in_array('robots', $republish, true) && class_exists('ThinkRank\\SEO\\Site_Identity_Manager')) {
254 (new \ThinkRank\SEO\Site_Identity_Manager())->sync_robots_txt_file();
255 }
256
257 if (in_array('llms', $republish, true) && class_exists('ThinkRank\\SEO\\LLMs_Txt_Manager')) {
258 $llms = new \ThinkRank\SEO\LLMs_Txt_Manager();
259 $content = $llms->get_published_content();
260
261 // write_llms_txt_to_file() enforces the enabled toggle and the
262 // delivery mode itself, so an empty document is the only case
263 // worth short-circuiting here.
264 if ($content !== '') {
265 $llms->write_llms_txt_to_file($content);
266 }
267 }
268 } catch (\Throwable $e) {
269 // A failed republish must not block activation — the user would be
270 // left unable to switch the plugin on at all. The artifacts rebuild
271 // on the next content or settings save.
272 if (defined('WP_DEBUG') && WP_DEBUG) {
273 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
274 error_log('ThinkRank: failed to restore web-root artifacts: ' . $e->getMessage());
275 }
276 }
277 }
278
279 /**
280 * Schedule cron jobs
281 *
282 * @return void
283 */
284 private function schedule_cron_jobs(): void {
285
286 // Schedule cache cleanup
287 if (!wp_next_scheduled('thinkrank_cache_cleanup')) {
288 wp_schedule_event(time(), 'daily', 'thinkrank_cache_cleanup');
289 }
290
291 // Schedule usage analytics
292 if (!wp_next_scheduled('thinkrank_usage_analytics')) {
293 wp_schedule_event(time(), 'weekly', 'thinkrank_usage_analytics');
294 }
295 }
296
297 /**
298 * Set activation flag for first-time setup
299 *
300 * @return void
301 */
302 private function set_activation_flag(): void {
303 update_option('thinkrank_activated', true);
304 update_option('thinkrank_activation_time', time());
305
306 // Set flag for showing welcome screen
307 update_option('thinkrank_show_welcome', true);
308
309 // Trigger a one-time redirect to the Setup Wizard on the next admin load,
310 // but only when the wizard has not already been completed. A short-lived
311 // transient is used so it auto-expires and never fires for bulk/network
312 // activations that skip the redirect window.
313 if (!get_option('thinkrank_setup_wizard_completed')) {
314 set_transient('thinkrank_setup_wizard_redirect', 1, 60);
315 }
316 }
317 }
318