| @@ -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 | } |