PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.8.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.8.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 trunk 1.0.0 1.0.1 1.0.2 1.1.0 All 49 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.8.0, at includes/core/class-seo-plugin-detector.php

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