PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.14.1
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.14.1
2.14.1 2.14.0 2.13.0 2.12.0 2.11.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 All 56 releases
thinkrank / includes / core / class-seo-plugin-detector.php

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

303 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 * SEO Plugin Detector
4 *
5 * Simple detection of popular SEO plugins
6 *
7 * @package ThinkRank\Core
8 * @since 1.0.0
9 */
10
11 declare(strict_types=1);
12
13 namespace ThinkRank\Core;
14
15 // Prevent direct access
16 if (!defined('ABSPATH')) {
17 exit;
18 }
19
20 /**
21 * SEO Plugin Detector Class
22 *
23 * Single Responsibility: Detect popular SEO plugins
24 *
25 * @since 1.0.0
26 */
27 class SEO_Plugin_Detector {
28
29 /**
30 * Known SEO plugins
31 *
32 * @var array
33 */
34 private const KNOWN_PLUGINS = [
35 'yoast' => [
36 'name' => 'Yoast SEO',
37 'class' => 'WPSEO',
38 'function' => 'wpseo_init',
39 'file' => 'wordpress-seo/wp-seo.php',
40 'extra_files' => ['wordpress-seo-premium/wp-seo-premium.php'],
41 'importable' => true,
42 ],
43 'rankmath' => [
44 'name' => 'Rank Math',
45 'class' => 'RankMath',
46 'function' => 'rank_math',
47 'file' => 'seo-by-rank-math/rank-math.php',
48 'extra_files' => ['seo-by-rank-math-pro/rank-math-pro.php'],
49 'importable' => true,
50 ],
51 'seopress' => [
52 'name' => 'SEOPress',
53 'class' => 'SEOPress',
54 'function' => 'seopress_init',
55 'file' => 'wp-seopress/seopress.php',
56 'extra_files' => ['wp-seopress-pro/seopress-pro.php'],
57 'importable' => true,
58 ],
59 'aioseo' => [
60 'name' => 'All in One SEO',
61 'class' => 'AIOSEO',
62 'function' => 'aioseo',
63 'file' => 'all-in-one-seo-pack/all_in_one_seo_pack.php',
64 'extra_files' => ['all-in-one-seo-pack-pro/all_in_one_seo_pack.php'],
65 'importable' => true,
66 ],
67 'squirrly' => [
68 'name' => 'Squirrly SEO',
69 'class' => 'SQ_Classes_ObjController',
70 'function' => 'sq_wpcall',
71 'file' => 'squirrly-seo/squirrly.php',
72 'extra_files' => ['squirrly-seo-pack/index.php'],
73 'importable' => true,
74 ],
75 'slimseo' => [
76 'name' => 'Slim SEO',
77 'class' => 'SlimSEO\\Activator',
78 'function' => '',
79 'file' => 'slim-seo/slim-seo.php',
80 // Slim SEO Pro runs with or without the free plugin (#886).
81 'extra_files' => ['slim-seo-pro/slim-seo-pro.php'],
82 'importable' => true,
83 ],
84 'theseoframework' => [
85 'name' => 'The SEO Framework',
86 'class' => 'The_SEO_Framework\\Load',
87 'function' => 'the_seo_framework',
88 'file' => 'autodescription/autodescription.php',
89 'extra_files' => [],
90 'importable' => false,
91 ],
92 // Brainstorm Force's SEO plugin. Missing from this list, a site moving
93 // from SureRank kept both running: every head tag doubled and no
94 // conflict notice or deactivate action appeared (#916). The class is
95 // declared by loader.php, which surerank.php requires directly.
96 'surerank' => [
97 'name' => 'SureRank',
98 'class' => 'SureRank\\Loader',
99 'function' => '',
100 'file' => 'surerank/surerank.php',
101 'extra_files' => [],
102 'importable' => false,
103 ],
104 ];
105
106 /**
107 * Detected plugins cache
108 *
109 * @var array|null
110 */
111 private static ?array $detected_plugins = null;
112
113 /**
114 * Detect active SEO plugins
115 *
116 * @return array Detected plugins
117 */
118 public static function detect_plugins(): array {
119 if (self::$detected_plugins !== null) {
120 return self::$detected_plugins;
121 }
122
123 self::$detected_plugins = [];
124
125 foreach (self::KNOWN_PLUGINS as $slug => $plugin) {
126 if (self::is_plugin_active($plugin)) {
127 self::$detected_plugins[$slug] = $plugin;
128 }
129 }
130
131 return self::$detected_plugins;
132 }
133
134 /**
135 * Check if any SEO plugins are active
136 *
137 * @return bool True if SEO plugins detected
138 */
139 public static function has_seo_plugins(): bool {
140 return !empty(self::detect_plugins());
141 }
142
143 /**
144 * Get detected plugin names
145 *
146 * @return array Plugin names
147 */
148 public static function get_plugin_names(): array {
149 $detected = self::detect_plugins();
150 return array_column($detected, 'name');
151 }
152
153 /**
154 * Check if a specific plugin is active
155 *
156 * @param string $slug Plugin slug
157 * @return bool True if plugin is active
158 */
159 public static function is_plugin_detected(string $slug): bool {
160 $detected = self::detect_plugins();
161 return isset($detected[$slug]);
162 }
163
164 /**
165 * Check if a plugin is active
166 *
167 * @param array $plugin Plugin configuration
168 * @return bool True if plugin is active
169 */
170 private static function is_plugin_active(array $plugin): bool {
171 // Check by class
172 if (!empty($plugin['class']) && class_exists($plugin['class'])) {
173 return true;
174 }
175
176 // Check by function
177 if (!empty($plugin['function']) && function_exists($plugin['function'])) {
178 return true;
179 }
180
181 // Check by plugin file, the add-on's included: Slim SEO Pro runs
182 // without the free plugin, and is no less a second SEO plugin then.
183 if (function_exists('is_plugin_active')) {
184 foreach (array_merge([$plugin['file'] ?? ''], $plugin['extra_files'] ?? []) as $file) {
185 if ($file !== '' && is_plugin_active($file)) {
186 return true;
187 }
188 }
189 }
190
191 return false;
192 }
193
194 /**
195 * Get admin notice message
196 *
197 * @return string Notice message
198 */
199 public static function get_notice_message(): string {
200 if (!self::has_seo_plugins()) {
201 return '';
202 }
203
204 return __('Running more than one SEO plugin can create conflicts: duplicate meta tags, sitemaps, and schema markup that may hurt your search rankings. To avoid any SEO-related conflicts, we recommend deactivating third-party SEO plugins.', 'thinkrank');
205 }
206
207 /**
208 * Whether any detected plugin's SEO data can be imported into ThinkRank.
209 *
210 * @return bool True if at least one detected plugin has an importer
211 */
212 public static function has_importable_plugin(): bool {
213 foreach (self::detect_plugins() as $plugin) {
214 if (!empty($plugin['importable'])) {
215 return true;
216 }
217 }
218
219 return false;
220 }
221
222 /**
223 * Get the human-readable name for a known plugin slug.
224 *
225 * Falls back to the slug itself when unknown, so callers always have a
226 * displayable label.
227 *
228 * @param string $slug Plugin slug from KNOWN_PLUGINS
229 * @return string Plugin name (or the slug if unknown)
230 */
231 public static function get_plugin_name(string $slug): string {
232 return self::KNOWN_PLUGINS[$slug]['name'] ?? $slug;
233 }
234
235 /**
236 * Get the active plugin files to deactivate for a detected plugin.
237 *
238 * Resolves the plugin file(s) — including premium companions such as
239 * Rank Math Pro — server-side from the known-plugins map, so callers
240 * never pass arbitrary plugin paths to deactivate_plugins().
241 *
242 * @param string $slug Plugin slug from KNOWN_PLUGINS
243 * @return array Active plugin file paths (may be empty)
244 */
245 public static function get_deactivatable_files(string $slug): array {
246 if (!isset(self::KNOWN_PLUGINS[$slug]) || !self::is_plugin_detected($slug)) {
247 return [];
248 }
249
250 if (!function_exists('is_plugin_active')) {
251 require_once ABSPATH . 'wp-admin/includes/plugin.php';
252 }
253
254 $plugin = self::KNOWN_PLUGINS[$slug];
255 $files = array_merge([$plugin['file']], $plugin['extra_files'] ?? []);
256
257 return array_values(array_filter($files, 'is_plugin_active'));
258 }
259
260 /**
261 * Should show admin notice
262 *
263 * @return bool True if should show notice
264 */
265 public static function should_show_notice(): bool {
266 // Only show if SEO plugins detected
267 if (!self::has_seo_plugins()) {
268 return false;
269 }
270
271 // Show on admin pages (not just ThinkRank pages)
272 if (!is_admin()) {
273 return false;
274 }
275
276 // Check if user dismissed the notice
277 $dismissed = get_user_meta(get_current_user_id(), 'thinkrank_seo_notice_dismissed', true);
278 if ($dismissed) {
279 return false;
280 }
281
282 return true;
283 }
284
285 /**
286 * Dismiss notice for current user
287 *
288 * @return void
289 */
290 public static function dismiss_notice(): void {
291 update_user_meta(get_current_user_id(), 'thinkrank_seo_notice_dismissed', true);
292 }
293
294 /**
295 * Reset notice dismissal (for testing)
296 *
297 * @return void
298 */
299 public static function reset_notice(): void {
300 delete_user_meta(get_current_user_id(), 'thinkrank_seo_notice_dismissed');
301 }
302 }
303