PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.29.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.29.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 / admin / importers / class-import-detector.php

class-import-detector.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 1.29.0, at includes/admin/importers/class-import-detector.php

310 lines 9.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Import Detector
5 *
6 * Database-level detection of SEO plugins. Works even when source plugins
7 * are deactivated by querying meta tables and custom tables directly.
8 *
9 * @package ThinkRank\Admin\Importers
10 * @since 2.0.0
11 */
12
13 declare(strict_types=1);
14
15 namespace ThinkRank\Admin\Importers;
16
17 if (!defined('ABSPATH')) {
18 exit;
19 }
20
21 /**
22 * Import Detector Class
23 *
24 * @since 2.0.0
25 */
26 class Import_Detector {
27
28 /**
29 * Transient key for caching detection results
30 */
31 private const CACHE_KEY = 'thinkrank_import_detection';
32
33 /**
34 * Cache TTL in seconds (1 hour)
35 */
36 private const CACHE_TTL = 3600;
37
38 /**
39 * Plugin detection configurations
40 */
41 private const PLUGINS = [
42 'yoast' => [
43 'name' => 'Yoast SEO',
44 'meta_prefix' => '_yoast_wpseo_',
45 'option_keys' => ['wpseo', 'wpseo_titles', 'wpseo_social'],
46 'plugin_files' => ['wordpress-seo/wp-seo.php', 'wordpress-seo-premium/wp-seo-premium.php'],
47 ],
48 'rankmath' => [
49 'name' => 'Rank Math',
50 'meta_prefix' => 'rank_math_',
51 'option_keys' => ['rank-math-options-general', 'rank-math-options-titles'],
52 'plugin_files' => ['seo-by-rank-math/rank-math.php', 'seo-by-rank-math-pro/rank-math-pro.php'],
53 ],
54 'seopress' => [
55 'name' => 'SEOPress',
56 'meta_prefix' => '_seopress_',
57 // SEOPress option names all carry the `_option_name` suffix.
58 'option_keys' => ['seopress_titles_option_name', 'seopress_social_option_name', 'seopress_advanced_option_name'],
59 'plugin_files' => ['wp-seopress/seopress.php', 'wp-seopress-pro/seopress-pro.php'],
60 ],
61 'aioseo' => [
62 'name' => 'All in One SEO',
63 'meta_prefix' => '',
64 'option_keys' => ['aioseo_options'],
65 'plugin_files' => ['all-in-one-seo-pack/all_in_one_seo_pack.php', 'all-in-one-seo-pack-pro/all_in_one_seo_pack.php'],
66 ],
67 ];
68
69 /**
70 * Detect all source plugins present in the database
71 *
72 * @param bool $use_cache Whether to use cached results
73 * @return array Detected plugins with item counts
74 */
75 public function detect(bool $use_cache = true): array {
76 if ($use_cache) {
77 $cached = get_transient(self::CACHE_KEY);
78 if ($cached !== false) {
79 return $cached;
80 }
81 }
82
83 $detected = [];
84
85 foreach (self::PLUGINS as $slug => $config) {
86 $result = $this->detect_plugin($slug, $config);
87 if ($result !== null) {
88 $detected[$slug] = $result;
89 }
90 }
91
92 set_transient(self::CACHE_KEY, $detected, self::CACHE_TTL);
93
94 return $detected;
95 }
96
97 /**
98 * Clear the detection cache
99 *
100 * @return void
101 */
102 public function clear_cache(): void {
103 delete_transient(self::CACHE_KEY);
104 }
105
106 /**
107 * Detect a single plugin's data presence
108 *
109 * @param string $slug Plugin slug
110 * @param array $config Plugin configuration
111 * @return array|null Detection result or null if not found
112 */
113 private function detect_plugin(string $slug, array $config): ?array {
114 global $wpdb;
115
116 // Only surface source plugins that are currently installed AND active.
117 // Migrating from a plugin the user no longer runs isn't actionable, so
118 // leftover data from a deactivated/uninstalled plugin is intentionally
119 // excluded from the migration screen.
120 if (!$this->is_source_active($config['plugin_files'] ?? [])) {
121 return null;
122 }
123
124 $counts = [];
125 $total = 0;
126
127 if ($slug === 'aioseo') {
128 // AIOSEO uses a custom table
129 $counts = $this->detect_aioseo();
130 } else {
131 // Standard postmeta-based plugins
132 $prefix = $config['meta_prefix'];
133
134 // Count posts with this plugin's meta
135 $post_count = (int) $wpdb->get_var(
136 $wpdb->prepare(
137 "SELECT COUNT(DISTINCT post_id) FROM {$wpdb->postmeta} WHERE meta_key LIKE %s",
138 $wpdb->esc_like($prefix) . '%'
139 )
140 );
141
142 if ($post_count > 0) {
143 $counts['postmeta'] = $post_count;
144 }
145
146 // Count terms with this plugin's meta
147 $term_count = (int) $wpdb->get_var(
148 $wpdb->prepare(
149 "SELECT COUNT(DISTINCT term_id) FROM {$wpdb->termmeta} WHERE meta_key LIKE %s",
150 $wpdb->esc_like($prefix) . '%'
151 )
152 );
153
154 if ($term_count > 0) {
155 $counts['termmeta'] = $term_count;
156 }
157 }
158
159 // Redirections / 404 logs. The UI derives its type checkboxes from
160 // these counts, so a type missing here is invisible to the user even
161 // when the exporter supports it (Rank Math Pro's redirects + 404
162 // Monitor and Yoast Premium's redirects were never offered).
163 $counts = array_merge($counts, $this->detect_redirect_data($slug));
164
165 // Check for settings
166 $has_settings = false;
167 foreach ($config['option_keys'] as $option_key) {
168 if (get_option($option_key, null) !== null) {
169 $has_settings = true;
170 break;
171 }
172 }
173
174 if ($has_settings) {
175 $counts['settings'] = 1;
176 }
177
178 // Only return if we found something
179 if (empty($counts)) {
180 return null;
181 }
182
183 foreach ($counts as $count) {
184 $total += $count;
185 }
186
187 return [
188 'plugin' => $slug,
189 'plugin_name' => $config['name'],
190 'counts' => $counts,
191 'total' => $total,
192 ];
193 }
194
195 /**
196 * Whether any of a source plugin's known main files is active.
197 *
198 * Checks both single-site and network activation. A plugin that is merely
199 * installed but not active returns false.
200 *
201 * @param string[] $plugin_files Candidate plugin main files (free + pro).
202 * @return bool
203 */
204 private function is_source_active(array $plugin_files): bool {
205 if (empty($plugin_files)) {
206 return false;
207 }
208
209 if (!function_exists('is_plugin_active')) {
210 require_once ABSPATH . 'wp-admin/includes/plugin.php';
211 }
212
213 foreach ($plugin_files as $plugin_file) {
214 if (is_plugin_active($plugin_file)) {
215 return true;
216 }
217 }
218
219 return false;
220 }
221
222 /**
223 * Count a source plugin's redirect / 404-log stores, matching what its
224 * exporter reads.
225 *
226 * @param string $slug Plugin slug
227 * @return array Partial counts (redirections / 404_logs keys only)
228 */
229 private function detect_redirect_data(string $slug): array {
230 global $wpdb;
231
232 $counts = [];
233
234 $count_table = static function (string $suffix) use ($wpdb): int {
235 $table = $wpdb->prefix . $suffix;
236 if (!$wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table))) {
237 return 0;
238 }
239 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
240 return (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table}");
241 };
242
243 if ($slug === 'rankmath') {
244 $redirects = $count_table('rank_math_redirections');
245 if ($redirects > 0) {
246 $counts['redirections'] = $redirects;
247 }
248 $logs = $count_table('rank_math_404_logs');
249 if ($logs > 0) {
250 $counts['404_logs'] = $logs;
251 }
252 } elseif ($slug === 'yoast') {
253 $redirects = $count_table('yoast_seo_redirects');
254 if ($redirects > 0) {
255 $counts['redirections'] = $redirects;
256 }
257 } elseif ($slug === 'seopress') {
258 $redirects = (int) $wpdb->get_var(
259 "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'seopress_404'"
260 );
261 if ($redirects > 0) {
262 $counts['redirections'] = $redirects;
263 }
264 }
265
266 return $counts;
267 }
268
269 /**
270 * Detect AIOSEO custom table data
271 *
272 * @return array Counts array
273 */
274 private function detect_aioseo(): array {
275 global $wpdb;
276
277 $counts = [];
278 $table_name = $wpdb->prefix . 'aioseo_posts';
279
280 // Check if table exists
281 $table_exists = $wpdb->get_var(
282 $wpdb->prepare("SHOW TABLES LIKE %s", $table_name)
283 );
284
285 if ($table_exists) {
286 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is $wpdb->prefix plus a literal, and every value is passed as a placeholder replacement.
287 $post_count = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table_name}");
288 if ($post_count > 0) {
289 $counts['postmeta'] = $post_count;
290 }
291 }
292
293 // Check for redirections table
294 $redirects_table = $wpdb->prefix . 'aioseo_redirects';
295 $redirects_exists = $wpdb->get_var(
296 $wpdb->prepare("SHOW TABLES LIKE %s", $redirects_table)
297 );
298
299 if ($redirects_exists) {
300 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is $wpdb->prefix plus a literal, and every value is passed as a placeholder replacement.
301 $redirect_count = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$redirects_table}");
302 if ($redirect_count > 0) {
303 $counts['redirections'] = $redirect_count;
304 }
305 }
306
307 return $counts;
308 }
309 }
310