PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.13.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.13.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 1.26.0 1.25.0 All 54 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.13.0, at includes/core/class-seo-plugin-detector.php

291 lines 8.4 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 ];
93
94 /**
95 * Detected plugins cache
96 *
97 * @var array|null
98 */
99 private static ?array $detected_plugins = null;
100
101 /**
102 * Detect active SEO plugins
103 *
104 * @return array Detected plugins
105 */
106 public static function detect_plugins(): array {
107 if (self::$detected_plugins !== null) {
108 return self::$detected_plugins;
109 }
110
111 self::$detected_plugins = [];
112
113 foreach (self::KNOWN_PLUGINS as $slug => $plugin) {
114 if (self::is_plugin_active($plugin)) {
115 self::$detected_plugins[$slug] = $plugin;
116 }
117 }
118
119 return self::$detected_plugins;
120 }
121
122 /**
123 * Check if any SEO plugins are active
124 *
125 * @return bool True if SEO plugins detected
126 */
127 public static function has_seo_plugins(): bool {
128 return !empty(self::detect_plugins());
129 }
130
131 /**
132 * Get detected plugin names
133 *
134 * @return array Plugin names
135 */
136 public static function get_plugin_names(): array {
137 $detected = self::detect_plugins();
138 return array_column($detected, 'name');
139 }
140
141 /**
142 * Check if a specific plugin is active
143 *
144 * @param string $slug Plugin slug
145 * @return bool True if plugin is active
146 */
147 public static function is_plugin_detected(string $slug): bool {
148 $detected = self::detect_plugins();
149 return isset($detected[$slug]);
150 }
151
152 /**
153 * Check if a plugin is active
154 *
155 * @param array $plugin Plugin configuration
156 * @return bool True if plugin is active
157 */
158 private static function is_plugin_active(array $plugin): bool {
159 // Check by class
160 if (!empty($plugin['class']) && class_exists($plugin['class'])) {
161 return true;
162 }
163
164 // Check by function
165 if (!empty($plugin['function']) && function_exists($plugin['function'])) {
166 return true;
167 }
168
169 // Check by plugin file, the add-on's included: Slim SEO Pro runs
170 // without the free plugin, and is no less a second SEO plugin then.
171 if (function_exists('is_plugin_active')) {
172 foreach (array_merge([$plugin['file'] ?? ''], $plugin['extra_files'] ?? []) as $file) {
173 if ($file !== '' && is_plugin_active($file)) {
174 return true;
175 }
176 }
177 }
178
179 return false;
180 }
181
182 /**
183 * Get admin notice message
184 *
185 * @return string Notice message
186 */
187 public static function get_notice_message(): string {
188 if (!self::has_seo_plugins()) {
189 return '';
190 }
191
192 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');
193 }
194
195 /**
196 * Whether any detected plugin's SEO data can be imported into ThinkRank.
197 *
198 * @return bool True if at least one detected plugin has an importer
199 */
200 public static function has_importable_plugin(): bool {
201 foreach (self::detect_plugins() as $plugin) {
202 if (!empty($plugin['importable'])) {
203 return true;
204 }
205 }
206
207 return false;
208 }
209
210 /**
211 * Get the human-readable name for a known plugin slug.
212 *
213 * Falls back to the slug itself when unknown, so callers always have a
214 * displayable label.
215 *
216 * @param string $slug Plugin slug from KNOWN_PLUGINS
217 * @return string Plugin name (or the slug if unknown)
218 */
219 public static function get_plugin_name(string $slug): string {
220 return self::KNOWN_PLUGINS[$slug]['name'] ?? $slug;
221 }
222
223 /**
224 * Get the active plugin files to deactivate for a detected plugin.
225 *
226 * Resolves the plugin file(s) — including premium companions such as
227 * Rank Math Pro — server-side from the known-plugins map, so callers
228 * never pass arbitrary plugin paths to deactivate_plugins().
229 *
230 * @param string $slug Plugin slug from KNOWN_PLUGINS
231 * @return array Active plugin file paths (may be empty)
232 */
233 public static function get_deactivatable_files(string $slug): array {
234 if (!isset(self::KNOWN_PLUGINS[$slug]) || !self::is_plugin_detected($slug)) {
235 return [];
236 }
237
238 if (!function_exists('is_plugin_active')) {
239 require_once ABSPATH . 'wp-admin/includes/plugin.php';
240 }
241
242 $plugin = self::KNOWN_PLUGINS[$slug];
243 $files = array_merge([$plugin['file']], $plugin['extra_files'] ?? []);
244
245 return array_values(array_filter($files, 'is_plugin_active'));
246 }
247
248 /**
249 * Should show admin notice
250 *
251 * @return bool True if should show notice
252 */
253 public static function should_show_notice(): bool {
254 // Only show if SEO plugins detected
255 if (!self::has_seo_plugins()) {
256 return false;
257 }
258
259 // Show on admin pages (not just ThinkRank pages)
260 if (!is_admin()) {
261 return false;
262 }
263
264 // Check if user dismissed the notice
265 $dismissed = get_user_meta(get_current_user_id(), 'thinkrank_seo_notice_dismissed', true);
266 if ($dismissed) {
267 return false;
268 }
269
270 return true;
271 }
272
273 /**
274 * Dismiss notice for current user
275 *
276 * @return void
277 */
278 public static function dismiss_notice(): void {
279 update_user_meta(get_current_user_id(), 'thinkrank_seo_notice_dismissed', true);
280 }
281
282 /**
283 * Reset notice dismissal (for testing)
284 *
285 * @return void
286 */
287 public static function reset_notice(): void {
288 delete_user_meta(get_current_user_id(), 'thinkrank_seo_notice_dismissed');
289 }
290 }
291