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.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 1.10.0 All 48 releases
thinkrank / includes / admin / class-search-visibility-notice.php

class-search-visibility-notice.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 2.7.0, at includes/admin/class-search-visibility-notice.php

204 lines 6.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Search Visibility Notice
4 *
5 * Warns site owners when WordPress is set to discourage search engines, which
6 * makes the whole site noindex.
7 *
8 * @package ThinkRank\Admin
9 * @since 1.0.0
10 */
11
12 declare(strict_types=1);
13
14 namespace ThinkRank\Admin;
15
16 // Prevent direct access
17 if (!defined('ABSPATH')) {
18 exit;
19 }
20
21 /**
22 * Search Visibility Notice Class
23 *
24 * Single Responsibility: Surface the "Discourage search engines" setting as a
25 * site-wide admin notice.
26 *
27 * The condition is already reported by the Site SEO Analyzer
28 * (SEO_Analyzer::check_search_visibility()), but nobody opens the Analyzer to
29 * discover their site is invisible — so it also needs to find the user.
30 *
31 * @since 1.0.0
32 */
33 class Search_Visibility_Notice {
34
35 /**
36 * Option flag storing the dismissal.
37 *
38 * Cleared whenever blog_public changes, so re-enabling "Discourage search
39 * engines" later warns again instead of staying silenced forever.
40 *
41 * @var string
42 */
43 public const OPT_DISMISSED = 'thinkrank_search_visibility_dismissed';
44
45 /**
46 * Nonce action for the dismiss request.
47 *
48 * @var string
49 */
50 private const NONCE_ACTION = 'thinkrank_search_visibility_notice';
51
52 /**
53 * Initialize the notice.
54 *
55 * Hooks both `admin_notices` and `thinkrank_admin_notices`: Manager
56 * ::remove_admin_notice() strips every `admin_notices` callback on
57 * ThinkRank's own screens and re-fires `thinkrank_admin_notices` instead,
58 * so a notice registered on only one of the two disappears on half the
59 * admin. The two never both run on the same request.
60 *
61 * @return void
62 */
63 public function init(): void {
64 add_action('admin_notices', [$this, 'render']);
65 add_action('thinkrank_admin_notices', [$this, 'render']);
66 add_action('admin_enqueue_scripts', [$this, 'enqueue_assets']);
67 add_action('wp_ajax_thinkrank_dismiss_search_visibility', [$this, 'ajax_dismiss']);
68
69 // A change either resolves the problem or re-introduces it; both cases
70 // want a clean slate for the dismissal.
71 add_action('update_option_blog_public', [$this, 'reset_dismissal']);
72 }
73
74 /**
75 * Whether the notice should render on this request.
76 *
77 * @return bool
78 */
79 private function should_display(): bool {
80 // blog_public = 0 means "Discourage search engines from indexing this
81 // site" is ticked under Settings → Reading.
82 if (get_option('blog_public')) {
83 return false;
84 }
85
86 // Only users who can actually flip the setting are shown the warning.
87 if (!current_user_can('manage_options')) {
88 return false;
89 }
90
91 return !get_option(self::OPT_DISMISSED);
92 }
93
94 /**
95 * Load the shared notice stylesheet when the notice will render.
96 *
97 * @return void
98 */
99 public function enqueue_assets(): void {
100 if (!$this->should_display()) {
101 return;
102 }
103
104 wp_enqueue_style(
105 'thinkrank-admin-notices',
106 THINKRANK_PLUGIN_URL . 'static/css/admin-notices.css',
107 [],
108 THINKRANK_VERSION
109 );
110 }
111
112 /**
113 * Render the notice.
114 *
115 * @return void
116 */
117 public function render(): void {
118 if (!$this->should_display()) {
119 return;
120 }
121
122 $reading_url = admin_url('options-reading.php');
123 ?>
124 <div class="notice notice-warning is-dismissible thinkrank-notice thinkrank-search-visibility-notice">
125 <div class="thinkrank-notice__inner">
126 <div class="thinkrank-notice__body">
127 <p class="thinkrank-notice__title"><?php esc_html_e('Your site is hidden from search engines', 'thinkrank'); ?></p>
128 <p class="thinkrank-notice__text">
129 <?php esc_html_e('WordPress is set to discourage search engines from indexing this site, so your pages will not appear in search results — no matter how well they are optimized.', 'thinkrank'); ?>
130 </p>
131 <p class="thinkrank-notice__actions">
132 <a href="<?php echo esc_url($reading_url); ?>" class="button button-primary">
133 <?php esc_html_e('Change search engine visibility', 'thinkrank'); ?>
134 </a>
135 <a href="#" class="thinkrank-notice__dismiss thinkrank-dismiss-search-visibility" data-nonce="<?php echo esc_attr(wp_create_nonce(self::NONCE_ACTION)); ?>">
136 <?php esc_html_e('Dismiss', 'thinkrank'); ?>
137 </a>
138 </p>
139 </div>
140 </div>
141 </div>
142 <?php
143 // The notice renders on every admin screen, so the dismiss handler must
144 // ship with it — the thinkrank-admin bundle only loads on ThinkRank
145 // pages. Persist the dismissal for both our "Dismiss" link and core's
146 // × button.
147 wp_print_inline_script_tag(
148 '( function () {
149 document.addEventListener( "click", function ( event ) {
150 var notice = event.target.closest( ".thinkrank-search-visibility-notice" );
151 if ( ! notice ) {
152 return;
153 }
154 var link = event.target.closest( ".thinkrank-dismiss-search-visibility" );
155 if ( ! link && ! event.target.closest( ".notice-dismiss" ) ) {
156 return;
157 }
158 if ( link ) {
159 event.preventDefault();
160 notice.style.display = "none";
161 }
162 window.fetch( window.ajaxurl, {
163 method: "POST",
164 credentials: "same-origin",
165 body: new URLSearchParams( {
166 action: "thinkrank_dismiss_search_visibility",
167 nonce: notice.querySelector( ".thinkrank-dismiss-search-visibility" ).dataset.nonce,
168 } ),
169 } );
170 } );
171 } )();'
172 );
173 }
174
175 /**
176 * AJAX handler persisting the dismissal.
177 *
178 * @return void
179 */
180 public function ajax_dismiss(): void {
181 check_ajax_referer(self::NONCE_ACTION, 'nonce');
182
183 // The nonce proves intent, not authorization — dismissing a site-wide
184 // notice writes an option, so require the same capability that renders
185 // it.
186 if (!current_user_can('manage_options')) {
187 wp_send_json_error('Insufficient permissions', 403);
188 }
189
190 update_option(self::OPT_DISMISSED, 1, true);
191
192 wp_send_json_success();
193 }
194
195 /**
196 * Clear the dismissal whenever the visibility setting changes.
197 *
198 * @return void
199 */
200 public function reset_dismissal(): void {
201 delete_option(self::OPT_DISMISSED);
202 }
203 }
204