PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.7.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.7.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
← All changes | includes/admin/class-seo-notice.php +163 -33 1.0.22.7.0 View file →
@@ -1,10 +1,10 @@
1 1 <?php
2 2 /**
3 3 * SEO Plugin Notice Manager
4 - *
4 + *
5 5 * Handles admin notices for SEO plugin conflicts
6 - *
6 + *
7 7 * @package ThinkRank\Admin
8 8 * @since 1.0.0
9 9 */
10 10
@@ -20,15 +20,15 @@
20 20 }
21 21
22 22 /**
23 23 * SEO Notice Manager Class
24 - *
24 + *
25 25 * Single Responsibility: Display admin notices for SEO plugin conflicts
26 - *
26 + *
27 27 * @since 1.0.0
28 28 */
29 29 class SEO_Notice {
30 -
30 +
31 31 /**
32 32 * Initialize notice manager
33 33 *
34 34 * @return void
@@ -35,11 +35,12 @@
35 35 */
36 36 public function init(): void {
37 37 add_action('admin_notices', [$this, 'display_seo_plugin_notice']);
38 38 add_action('wp_ajax_thinkrank_dismiss_seo_notice', [$this, 'ajax_dismiss_notice']);
39 + add_action('wp_ajax_thinkrank_deactivate_seo_plugin', [$this, 'ajax_deactivate_plugin']);
39 40 add_action('admin_enqueue_scripts', [$this, 'enqueue_notice_scripts']);
40 41 }
41 -
42 +
42 43 /**
43 44 * Display SEO plugin conflict notice
44 45 *
45 46 * @return void
@@ -48,56 +49,151 @@
48 49 if (!SEO_Plugin_Detector::should_show_notice()) {
49 50 return;
50 51 }
51 52
53 + $detected = SEO_Plugin_Detector::detect_plugins();
52 54 $message = SEO_Plugin_Detector::get_notice_message();
53 - if (empty($message)) {
55 + if (empty($detected) || empty($message)) {
54 56 return;
55 57 }
56 -
57 - ?>
58 - <div class="notice notice-warning is-dismissible thinkrank-seo-notice">
59 - <h3><?php esc_html_e('ThinkRank SEO Priority', 'thinkrank'); ?></h3>
60 - <p><?php echo esc_html($message); ?></p>
61 58
62 - <p>
63 - <strong><?php esc_html_e('How it works:', 'thinkrank'); ?></strong><br>
64 - • <?php esc_html_e('Posts with ThinkRank AI metadata → ThinkRank takes priority', 'thinkrank'); ?><br>
65 - • <?php esc_html_e('Posts without ThinkRank metadata → Other SEO plugins work normally', 'thinkrank'); ?><br>
66 - • <?php esc_html_e('You have full control per post', 'thinkrank'); ?>
67 - </p>
59 + $names = array_column($detected, 'name');
60 + $heading = count($names) === 1
61 + /* translators: %s: detected SEO plugin name */
62 + ? sprintf(__('%s is running alongside ThinkRank', 'thinkrank'), $names[0])
63 + : __('Other SEO plugins are running alongside ThinkRank', 'thinkrank');
68 64
69 - <p>
70 - <a href="#" class="button button-secondary thinkrank-dismiss-seo-notice">
71 - <?php esc_html_e('Got it, dismiss this notice', 'thinkrank'); ?>
72 - </a>
73 - </p>
65 + $can_deactivate = current_user_can('deactivate_plugins');
66 + $migrate_url = $this->get_migration_url();
67 + ?>
68 + <div class="notice notice-warning is-dismissible thinkrank-notice thinkrank-seo-notice">
69 + <div class="thinkrank-notice__inner">
70 + <div class="thinkrank-notice__body">
71 + <p class="thinkrank-notice__title"><?php echo esc_html($heading); ?></p>
72 + <?php if (count($names) > 1) : ?>
73 + <p class="thinkrank-notice__badges">
74 + <?php foreach ($names as $name) : ?>
75 + <span class="thinkrank-notice__badge"><?php echo esc_html($name); ?></span>
76 + <?php endforeach; ?>
77 + </p>
78 + <?php endif; ?>
79 + <p class="thinkrank-notice__text"><?php echo esc_html($message); ?></p>
80 + <?php if ($migrate_url) : ?>
81 + <p class="thinkrank-notice__hint">
82 + <?php echo self::get_shield_icon(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static, trusted SVG markup ?>
83 + <span><?php esc_html_e('Your existing SEO data is safe — migrate titles, descriptions, and other metadata into ThinkRank before deactivating.', 'thinkrank'); ?></span>
84 + </p>
85 + <?php endif; ?>
86 + <p class="thinkrank-notice__actions">
87 + <?php if ($can_deactivate) : ?>
88 + <?php foreach ($detected as $slug => $plugin) : ?>
89 + <button type="button" class="button button-primary thinkrank-deactivate-seo-plugin" data-plugin="<?php echo esc_attr($slug); ?>">
90 + <?php
91 + /* translators: %s: detected SEO plugin name */
92 + echo esc_html(sprintf(__('Deactivate %s', 'thinkrank'), $plugin['name']));
93 + ?>
94 + </button>
95 + <?php endforeach; ?>
96 + <?php endif; ?>
97 + <?php if ($migrate_url) : ?>
98 + <a href="<?php echo esc_url($migrate_url); ?>" class="button button-secondary">
99 + <?php esc_html_e('Migrate SEO data first', 'thinkrank'); ?>
100 + </a>
101 + <?php endif; ?>
102 + <a href="#" class="thinkrank-notice__dismiss thinkrank-dismiss-seo-notice"><?php esc_html_e('Dismiss', 'thinkrank'); ?></a>
103 + </p>
104 + </div>
105 + </div>
74 106 </div>
75 107 <?php
76 108 }
77 -
109 +
78 110 /**
111 + * Small shield glyph for the data-safety reassurance chip.
112 + *
113 + * @return string Inline SVG markup
114 + */
115 + private static function get_shield_icon(): string {
116 + return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">'
117 + . '<path d="M12 2 4 5v6c0 5 3.4 8.4 8 10 4.6-1.6 8-5 8-10V5l-8-3zm-1 13-3-3 1.4-1.4 1.6 1.6 3.6-3.6L16 10l-5 5z" fill="currentColor"/>'
118 + . '</svg>';
119 + }
120 +
121 + /**
122 + * URL of the Setup Wizard migration step, when migration is still possible.
123 + *
124 + * The wizard redirects to the dashboard once completed, so the link is
125 + * only offered while the wizard is still accessible and at least one
126 + * detected plugin has a ThinkRank importer.
127 + *
128 + * @return string Migration URL or '' when unavailable
129 + */
130 + private function get_migration_url(): string {
131 + if (!SEO_Plugin_Detector::has_importable_plugin()) {
132 + return '';
133 + }
134 +
135 + if (get_option(Setup_Wizard::OPT_COMPLETED, false)) {
136 + return '';
137 + }
138 +
139 + return admin_url('admin.php?page=' . Setup_Wizard::PAGE_SLUG);
140 + }
141 +
142 + /**
79 143 * Enqueue notice scripts
80 - *
144 + *
81 145 * @param string $hook Current admin page hook
82 146 * @return void
83 147 */
84 148 public function enqueue_notice_scripts(string $hook): void {
85 - if (!SEO_Plugin_Detector::has_seo_plugins()) {
149 + if (!SEO_Plugin_Detector::should_show_notice()) {
86 150 return;
87 151 }
88 -
152 +
153 + wp_enqueue_style(
154 + 'thinkrank-admin-notices',
155 + THINKRANK_PLUGIN_URL . 'static/css/admin-notices.css',
156 + [],
157 + THINKRANK_VERSION
158 + );
159 +
89 160 wp_add_inline_script('jquery', '
90 161 jQuery(document).ready(function($) {
162 + function thinkrankDismissSeoNotice() {
163 + $.post(ajaxurl, {
164 + action: "thinkrank_dismiss_seo_notice",
165 + nonce: "' . wp_create_nonce('thinkrank_seo_notice_nonce') . '"
166 + }, function(response) {
167 + if (response.success) {
168 + $(".thinkrank-seo-notice").fadeOut();
169 + }
170 + });
171 + }
172 +
91 173 $(".thinkrank-dismiss-seo-notice").on("click", function(e) {
92 174 e.preventDefault();
93 -
175 + thinkrankDismissSeoNotice();
176 + });
177 +
178 + // Persist dismissal from the core notice X button too
179 + // (delegated — core injects the button after DOM ready)
180 + $(document).on("click", ".thinkrank-seo-notice .notice-dismiss", thinkrankDismissSeoNotice);
181 +
182 + $(".thinkrank-deactivate-seo-plugin").on("click", function(e) {
183 + e.preventDefault();
184 + var $button = $(this);
185 + $button.prop("disabled", true);
186 +
94 187 $.post(ajaxurl, {
95 - action: "thinkrank_dismiss_seo_notice",
188 + action: "thinkrank_deactivate_seo_plugin",
189 + plugin: $button.data("plugin"),
96 190 nonce: "' . wp_create_nonce('thinkrank_seo_notice_nonce') . '"
97 191 }, function(response) {
98 192 if (response.success) {
99 - $(".thinkrank-seo-notice").fadeOut();
193 + window.location.reload();
194 + } else {
195 + $button.prop("disabled", false);
100 196 }
101 197 });
102 198 });
103 199 });
@@ -102,12 +198,12 @@
102 198 });
103 199 });
104 200 ');
105 201 }
106 -
202 +
107 203 /**
108 204 * AJAX handler to dismiss SEO notice
109 - *
205 + *
110 206 * @return void
111 207 */
112 208 public function ajax_dismiss_notice(): void {
113 209 $nonce = sanitize_text_field(wp_unslash($_POST['nonce'] ?? ''));
@@ -113,9 +209,43 @@
113 209 $nonce = sanitize_text_field(wp_unslash($_POST['nonce'] ?? ''));
114 210 if (!wp_verify_nonce($nonce, 'thinkrank_seo_notice_nonce')) {
115 211 wp_send_json_error('Security check failed');
116 212 }
117 -
213 +
214 + // The notice is an admin-facing plugin-conflict warning — only users who
215 + // can manage the plugin should be able to dismiss it.
216 + if (!current_user_can('edit_posts')) {
217 + wp_send_json_error('Insufficient permissions');
218 + }
219 +
118 220 SEO_Plugin_Detector::dismiss_notice();
221 + wp_send_json_success();
222 + }
223 +
224 + /**
225 + * AJAX handler to deactivate a conflicting SEO plugin.
226 + *
227 + * Only accepts a known-plugin slug; the plugin file(s) — including
228 + * premium companions — are resolved server-side by the detector.
229 + *
230 + * @return void
231 + */
232 + public function ajax_deactivate_plugin(): void {
233 + $nonce = sanitize_text_field(wp_unslash($_POST['nonce'] ?? ''));
234 + if (!wp_verify_nonce($nonce, 'thinkrank_seo_notice_nonce')) {
235 + wp_send_json_error('Security check failed');
236 + }
237 +
238 + if (!current_user_can('deactivate_plugins')) {
239 + wp_send_json_error('Insufficient permissions');
240 + }
241 +
242 + $slug = sanitize_key(wp_unslash($_POST['plugin'] ?? ''));
243 + $files = SEO_Plugin_Detector::get_deactivatable_files($slug);
244 + if (empty($files)) {
245 + wp_send_json_error('Unknown plugin');
246 + }
247 +
248 + deactivate_plugins($files);
119 249 wp_send_json_success();
120 250 }
121 251 }