PluginProbe ʕ •ᴥ•ʔ
SiteGuard WP Plugin / 1.8.9
SiteGuard WP Plugin v1.8.9
1.8.9 1.8.8 1.8.7 1.8.6 1.8.6-beta1 1.8.6-beta2 1.8.4 1.8.5 1.8.3 1.8.2 1.8.1 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.2.0 1.2.1 1.2.2 1.2.3 1.4.3 1.5.0 1.5.1 1.5.2 1.6.0 1.6.1 1.7.0 1.7.1 1.7.10 1.7.11 1.7.12 1.7.2 1.7.3 1.7.4 1.7.5 1.7.6 1.7.7 1.7.8 1.7.9 1.8.0 1.8.0-beta1 1.8.0-beta2 1.8.0-beta3 1.8.0-beta4
siteguard / classes / siteguard-rename-login.php
siteguard / classes Last commit date
siteguard-admin-filter.php 3 months ago siteguard-base.php 3 weeks ago siteguard-captcha.php 1 week ago siteguard-config.php 1 year ago siteguard-disable-author-query.php 2 months ago siteguard-disable-pingback.php 3 months ago siteguard-disable-xmlrpc.php 3 months ago siteguard-htaccess.php 3 weeks ago siteguard-login-alert.php 3 months ago siteguard-login-history.php 2 months ago siteguard-login-lock.php 2 months ago siteguard-rename-login.php 1 week ago siteguard-updates-notify.php 1 week ago siteguard-waf-exclude-rule.php 3 months ago
siteguard-rename-login.php
1122 lines
1 <?php
2
3 require_once ABSPATH . '/wp-admin/includes/plugin.php';
4 require_once SITEGUARD_PATH . 'really-simple-captcha/siteguard-really-simple-captcha.php';
5
6 class SiteGuard_RenameLogin extends SiteGuard_Base {
7 private $denied_login = false;
8
9 /**
10 * True while the login URL conflict probe is running. The URL filters below
11 * pass their input through untouched during that window so the probe can
12 * observe what the *other* plugins on the same filter do. See
13 * probe_foreign_login_url().
14 *
15 * Static because feature_off() constructs a second instance of this class,
16 * whose filters are registered as well; a per-instance flag would leave that
17 * second instance rewriting the probe URL and the probe would report the
18 * plugin's own rewrite as a foreign one.
19 */
20 private static $probing = false;
21
22 /** Result of the conflict probe for this request. See get_login_url_conflict(). */
23 private static $conflict = null;
24 private static $conflict_checked = false;
25
26 protected static $incompatible_plugins = array(
27 'WordPress HTTPS (SSL)' => 'wordpress-https/wordpress-https.php',
28 'qTranslate X' => 'qtranslate-x/qtranslate.php',
29 );
30 public static $htaccess_mark = '#==== SITEGUARD_RENAME_LOGIN_SETTINGS';
31
32 const STUB_WRITE_FAIL_TRANSIENT = 'siteguard_rl_stub_fail';
33
34 // Set while feature_on() rebuilds the .htaccess block. clear_settings()
35 // removes the block before update_settings() writes it back, and a
36 // concurrent request landing in that window would see the block missing and
37 // turn the feature off. See SiteGuard::htaccess_check().
38 const HTACCESS_REBUILD_TRANSIENT = 'siteguard_rl_htaccess_rebuild';
39
40 function __construct() {
41 global $siteguard_config;
42
43 add_filter( 'logout_url', array( $this, 'filter_logout_url' ), 10, 2 );
44 add_action( 'admin_bar_menu', array( $this, 'rewrite_adminbar_logout' ), 999 );
45 add_action( 'admin_notices', array( $this, 'maybe_notice_stub_failed' ) );
46
47 // Late priority: plugins that rewrite the login URL register their filters
48 // at various points (plugins_loaded, init, wp_loaded), and admin_init runs
49 // after all of them.
50 add_action( 'admin_init', array( $this, 'check_login_url_conflict' ), 9999 );
51 add_action( 'admin_notices', array( $this, 'maybe_notice_login_url_conflict' ) );
52
53 if ( '1' === $siteguard_config->get( 'renamelogin_enable' ) ) {
54 if ( null !== $this->get_active_incompatible_plugins() ) {
55 $siteguard_config->set( 'renamelogin_enable', '0' );
56 $siteguard_config->update();
57 $this->feature_off();
58 } else {
59 $this->add_filter();
60 }
61 }
62
63 add_action( 'template_redirect', array( $this, 'guard_disabled_entry' ), 0 );
64 add_action( 'template_redirect', array( $this, 'handle_siteguard_rescue' ), 0 );
65 }
66
67 static function get_mark() {
68 return self::$htaccess_mark;
69 }
70
71 function init() {
72 global $siteguard_config;
73
74 $this->denied_login = false;
75
76 if ( '' === $siteguard_config->get( 'renamelogin_path' ) ) {
77 $siteguard_config->set( 'renamelogin_path', 'login_' . sprintf( '%05d', siteguard_rand( 1, 99999 ) ) );
78 }
79 if ( '' === $siteguard_config->get( 'redirect_enable' ) ) {
80 $siteguard_config->set( 'redirect_enable', '0' );
81 }
82 if ( '' === $siteguard_config->get( 'rescue_enable' ) ) {
83 $siteguard_config->set( 'rescue_enable', '1' );
84 }
85 if ( '' === $siteguard_config->get( 'renamelogin_stub' ) ) {
86 $siteguard_config->set( 'renamelogin_stub', SITEGUARD_RENAME_MODE_HTACCESS ); // Apache=0 / Nginx=1
87 }
88 $siteguard_config->update();
89
90 if ( true === siteguard_check_multisite()
91 && null === $this->get_active_incompatible_plugins()
92 ) {
93 $siteguard_config->set( 'renamelogin_enable', '1' );
94 $siteguard_config->update();
95 if ( ! $this->feature_on() ) {
96 $siteguard_config->set( 'renamelogin_enable', '0' );
97 $siteguard_config->update();
98 }
99 } else {
100 $siteguard_config->set( 'renamelogin_enable', '0' );
101 $siteguard_config->update();
102 }
103 }
104
105 function get_active_incompatible_plugins() {
106 $result = array();
107 foreach ( self::$incompatible_plugins as $name => $path ) {
108 if ( is_plugin_active( $path ) ) {
109 $result[] = $name;
110 }
111 }
112 return empty( $result ) ? null : $result;
113 }
114
115 function add_filter() {
116 add_filter( 'plugins_loaded', array( $this, 'handler_plugins_loaded' ), 9999 );
117
118 add_action( 'init', array( $this, 'guard_wp_login_direct_access' ), 0 );
119 add_filter( 'login_init', array( $this, 'handler_login_init' ), 10, 2 );
120 add_filter( 'site_url', array( $this, 'handler_site_url' ), 10, 2 );
121 add_filter( 'network_site_url', array( $this, 'handler_site_url' ), 10, 2 );
122 add_filter( 'wp_redirect', array( $this, 'handler_wp_redirect' ), 10, 2 );
123 add_filter( 'register', array( $this, 'handler_register' ) );
124 add_filter( 'auth_redirect_scheme', array( $this, 'handler_stop_redirect' ), 9999 );
125
126 remove_action( 'template_redirect', 'wp_redirect_admin_locations', 1000 );
127 }
128
129 public function can_use_htaccess() {
130 if ( ! isset( $_SERVER['SERVER_SOFTWARE'] )
131 || ( false === strpos( strtolower( $_SERVER['SERVER_SOFTWARE'] ), 'apache' ) && false === strpos( strtolower( $_SERVER['SERVER_SOFTWARE'] ), 'litespeed' ) )
132 ) {
133 return false;
134 }
135 return SiteGuard_Htaccess::is_writable_htaccess();
136 }
137
138 private function is_stub_mode() {
139 global $siteguard_config;
140 return SITEGUARD_RENAME_MODE_STUB === $siteguard_config->get( 'renamelogin_stub' );
141 }
142
143 private function slug() {
144 global $siteguard_config;
145 $slug = trim( (string) $siteguard_config->get( 'renamelogin_path' ), '/' );
146 return $slug === '' ? 'login' : $slug;
147 }
148
149 private function old_slug() {
150 global $siteguard_config;
151 $slug = trim( (string) $siteguard_config->get( 'oldlogin_path' ), '/' );
152 return $slug === '' ? 'login' : $slug;
153 }
154
155 private function stub_filename() {
156 return $this->slug() . '.php';
157 }
158
159 private function old_stub_filename() {
160 return $this->old_slug() . '.php';
161 }
162
163 public function stub_abspath() {
164 return trailingslashit( ABSPATH ) . $this->stub_filename();
165 }
166
167 private function old_stub_abspath() {
168 return trailingslashit( ABSPATH ) . $this->old_stub_filename();
169 }
170
171 private function stub_url() {
172 return rtrim( site_url(), '/' ) . '/' . $this->stub_filename();
173 }
174
175 private function install_stub() {
176 $file = $this->stub_abspath();
177 $code = "<?php\n/* Generated by SiteGuard WP Plugin */\nrequire_once __DIR__ . '/wp-login.php';\n";
178 $ok = @file_put_contents( $file, $code );
179 if ( false === $ok ) {
180 set_transient( self::STUB_WRITE_FAIL_TRANSIENT, 1, MINUTE_IN_SECONDS * 10 );
181 return false;
182 }
183 @chmod( $file, 0644 );
184 return true;
185 }
186
187 /**
188 * Make sure the stub file the current settings promise is in place, without
189 * rewriting it when it already is. Used to recover an install whose stub
190 * went missing while stub (.php) mode stayed recorded.
191 *
192 * @return bool
193 */
194 public function ensure_stub() {
195 if ( file_exists( $this->stub_abspath() ) ) {
196 return true;
197 }
198 // This runs on every request, so a site whose root is not writable must
199 // not retry (and record the failure) each time. install_stub() keeps the
200 // transient for ten minutes, which paces the retries.
201 if ( get_transient( self::STUB_WRITE_FAIL_TRANSIENT ) ) {
202 return false;
203 }
204 return $this->install_stub();
205 }
206
207 private function remove_stub( $file ) {
208 if ( file_exists( $file ) ) {
209 // To prevent accidental deletion of important files, check if the file was generated by this plugin.
210 $content = file_get_contents( $file, false, null, 0, 100 );
211 if ( false !== $content && false !== strpos( $content, '/* Generated by SiteGuard WP Plugin */' ) ) {
212 @unlink( $file );
213 }
214 }
215 }
216
217 /**
218 * Check if the current request is for the login page (renamed or original).
219 *
220 * @return bool
221 */
222 public function is_login_request() {
223 $req_path = isset( $_SERVER['REQUEST_URI'] ) ? (string) parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH ) : '';
224 $script_name = isset( $_SERVER['SCRIPT_NAME'] ) ? basename( $_SERVER['SCRIPT_NAME'] ) : '';
225 return ( $script_name === 'wp-login.php' || $req_path === '/' . $this->stub_filename() || $req_path === '/' . $this->slug() );
226 }
227
228 /**
229 * Whether REQUEST_URI targets a default login entry point (wp-login /
230 * wp-register) rather than the configured renamed slug.
231 *
232 * Every path segment is reduced to its "stem" (lowercased, any extension
233 * dropped) and compared against wp-login / wp-register. Checking every
234 * segment — not just the basename — is required because Apache MultiViews /
235 * content negotiation serves wp-login.php for an extensionless request, and
236 * with AcceptPathInfo anything after it becomes PATH_INFO:
237 * /wp-login -> wp-login.php
238 * /wp-login/ -> wp-login.php (PATH_INFO "/")
239 * /wp-login/anything -> wp-login.php (PATH_INFO "/anything") <-- basename is "anything"
240 * /wp-login.php/x -> wp-login.php (PATH_INFO "/x")
241 * A basename-only check misses the PATH_INFO variants (the basename is the
242 * trailing segment). Scanning every segment also covers the nested forms that
243 * WordPress core canonicalizes to wp-login.php (e.g. /abc/wp-login.php,
244 * //wp-login.php) and subdirectory installs (/cms/wp-login/x).
245 *
246 * The configured slug is exempt so a site that renamed its login to a reserved
247 * word (e.g. "wp-register", which the settings screen still permits because
248 * wp-register.php does not exist) is not locked out of its own login. Renamed
249 * slugs only contain [a-zA-Z0-9_-] (no dot), so the extension stripping never
250 * collides with a slug.
251 */
252 private function targets_default_login() {
253 $slug = strtolower( $this->slug() );
254 $link = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_url( $_SERVER['REQUEST_URI'] ) : '';
255 // Collapse leading/duplicate slashes before parse_url, otherwise //wp-login.php
256 // is parsed as host=wp-login.php with a NULL path and would slip through.
257 $link = preg_replace( '#^/+#', '/', $link );
258 $path = (string) parse_url( $link, PHP_URL_PATH );
259 $path = preg_replace( '#/+#', '/', $path );
260 $path = urldecode( $path );
261 foreach ( explode( '/', $path ) as $seg ) {
262 if ( '' === $seg ) {
263 continue;
264 }
265 $seg = strtolower( $seg );
266 // Strip trailing whitespace, control characters and non-ASCII bytes so
267 // variants like "wp-login.php " or "wp-login.php%C2%A0" (NBSP) are caught
268 // after WordPress core URL normalization would treat them as wp-login.php.
269 $seg = preg_replace( '/[\s\x00-\x1f\x7f-\xff]+$/', '', $seg );
270 $stem = preg_replace( '/\..*$/', '', $seg );
271 if ( ( 'wp-login' === $stem || 'wp-register' === $stem ) && $stem !== $slug ) {
272 return true;
273 }
274 }
275 return false;
276 }
277
278 /**
279 * Block direct access to the default login entry points at the init hook.
280 *
281 * Runs before login_init so that requests routed through index.php fallback
282 * (e.g. //wp-login.php on subdirectory installs) are stopped before WordPress
283 * core URL canonicalization can leak the renamed slug via wp_redirect.
284 */
285 public function guard_wp_login_direct_access() {
286 if ( ! $this->targets_default_login() ) {
287 return;
288 }
289 // When wp-login.php is the executing script (a direct hit, or MultiViews
290 // content negotiation serving it for /wp-login, /wp-login/x, etc.),
291 // login_init fires and handler_login_init() renders the WordPress theme
292 // 404 via set_404(). Defer to it so the visitor gets the themed 404 page
293 // instead of a bare browser 404.
294 //
295 // Only the index.php fallback path is handled here with a bare 404 + exit:
296 // e.g. //wp-login.php on a subdirectory install, where wp-login.php does
297 // NOT execute (SCRIPT_NAME is index.php) and login_init never fires. That
298 // case must stop now, before WordPress core canonicalizes the URL and
299 // leaks the renamed slug via wp_redirect, and the main query is not yet
300 // set up to render a theme template safely.
301 $script = isset( $_SERVER['SCRIPT_NAME'] ) ? strtolower( basename( $_SERVER['SCRIPT_NAME'] ) ) : '';
302 if ( 'wp-login.php' === $script ) {
303 return;
304 }
305 status_header( 404 );
306 nocache_headers();
307 exit;
308 }
309
310 function handler_login_init() {
311 // See targets_default_login() for rationale, including the renamed slug
312 // exemption that keeps a "wp-login"/"wp-register" slug from 404ing the
313 // site's own login page.
314 if ( $this->targets_default_login() ) {
315 $this->set_404();
316 }
317 }
318
319 function convert_url( $link ) {
320 if ( self::$probing ) {
321 return $link;
322 }
323 $result = $link;
324 $repl = $this->is_stub_mode() ? $this->stub_filename() : $this->slug();
325
326 if ( false !== strpos( $link, 'wp-login.php?action=register' ) && $this->denied_login ) {
327 $this->set_404();
328 } elseif ( false !== strpos( $link, 'wp-login.php' ) ) {
329 $result = str_replace( 'wp-login.php', $repl, $link );
330 }
331 return $result;
332 }
333
334 function handler_site_url( $link ) {
335 return $this->convert_url( $link ); }
336 function handler_register( $link ) {
337 return $this->convert_url( $link ); }
338 function handler_wp_redirect( $link, $status_code ) {
339 if ( ( ( strlen( $link ) <= 5 || 'http:' !== strtolower( substr( $link, 0, 5 ) ) ) && ( strlen( $link ) <= 6 || 'https:' !== strtolower( substr( $link, 0, 6 ) ) ) )
340 || ( isset( $_SERVER['HTTPS'] ) && strtolower( $_SERVER['HTTPS'] ) !== 'off' && 'https' === strtolower( substr( $link, 0, strpos( $link, '://' ) ) ) )
341 || ( ( ! isset( $_SERVER['HTTPS'] ) || strtolower( $_SERVER['HTTPS'] ) === 'off' ) && 'http' === strtolower( substr( $link, 0, strpos( $link, '://' ) ) ) ) ) {
342 return $this->convert_url( $link );
343 }
344 return $link;
345 }
346
347 private function htaccess_body() {
348 $slug = $this->slug();
349
350 $parse_url = parse_url( site_url() );
351 $base = '/';
352 if ( false !== $parse_url && isset( $parse_url['path'] ) && $parse_url['path'] !== '' ) {
353 $base = rtrim( $parse_url['path'], '/' ) . '/';
354 }
355
356 $ht = "<IfModule mod_rewrite.c>\n";
357 $ht .= " RewriteEngine on\n";
358 $ht .= " RewriteBase {$base}\n";
359 $ht .= " RewriteRule ^wp-signup\\.php 404-siteguard [L]\n";
360 $ht .= " RewriteRule ^wp-activate\\.php 404-siteguard [L]\n";
361 $ht .= " RewriteRule ^{$slug}(.*)$ wp-login.php\$1 [L]\n";
362 $ht .= "</IfModule>\n";
363
364 return $ht;
365 }
366
367 function feature_on() {
368 // Announce the rebuild for its whole duration, so that requests arriving
369 // while the .htaccess block is momentarily absent do not act on it.
370 set_transient( self::HTACCESS_REBUILD_TRANSIENT, 1, MINUTE_IN_SECONDS );
371 $result = $this->rebuild_feature();
372 delete_transient( self::HTACCESS_REBUILD_TRANSIENT );
373 return $result;
374 }
375
376 private function rebuild_feature() {
377 global $siteguard_htaccess, $siteguard_config;
378
379 // Remove .htaccess feature
380 SiteGuard_Htaccess::clear_settings( self::$htaccess_mark );
381
382 // Remove old stubs regardless of mode
383 $this->remove_stub( $this->old_stub_abspath() );
384 if ( $this->slug() !== $this->old_slug() ) {
385 $this->remove_stub( $this->stub_abspath() );
386 }
387
388 // Decide between .htaccess mode and stub (.php) mode, recording the reason
389 // when .htaccess cannot be used so the settings screen can explain why.
390 $reason = $this->htaccess_unavailable_reason();
391 if ( array() === $reason ) {
392 $data = $this->htaccess_body();
393 $mark = self::get_mark();
394 $ok = $siteguard_htaccess->update_settings( $mark, $data );
395 if ( $ok ) {
396 $siteguard_config->set( 'renamelogin_stub', SITEGUARD_RENAME_MODE_HTACCESS );
397 $siteguard_config->set( 'renamelogin_stub_reason', array() );
398 $siteguard_config->update();
399 return true;
400 }
401 // Writing the .htaccess block failed unexpectedly; fall back to stub.
402 $reason = array( 'code' => 'not_writable' );
403 }
404
405 if ( $this->install_stub() ) {
406 $siteguard_config->set( 'renamelogin_stub', SITEGUARD_RENAME_MODE_STUB );
407 $siteguard_config->set( 'renamelogin_stub_reason', $reason );
408 $siteguard_config->update();
409 siteguard_error_log( 'Rename Login fell back to stub (.php) mode. Reason: ' . wp_json_encode( $reason ) );
410 return true;
411 }
412
413 return false;
414 }
415
416 /**
417 * Why .htaccess mode cannot be used right now. Returns an empty array when it
418 * can be used; otherwise an array like array( 'code' => ..., 'url' => ... )
419 * describing the cause (server software, write permission, or which stage of
420 * the .htaccess self-test failed). Used to explain stub (.php) fallback.
421 */
422 private function htaccess_unavailable_reason() {
423 if ( ! $this->can_use_htaccess() ) {
424 $software = isset( $_SERVER['SERVER_SOFTWARE'] ) ? $_SERVER['SERVER_SOFTWARE'] : '';
425 if ( '' !== $software && false !== stripos( $software, 'nginx' ) ) {
426 return array( 'code' => 'nginx' );
427 }
428 if ( false === stripos( $software, 'apache' ) && false === stripos( $software, 'litespeed' ) ) {
429 return array( 'code' => 'server_software', 'detail' => $software );
430 }
431 return array( 'code' => 'not_writable' );
432 }
433 if ( ! SiteGuard_Htaccess::test_htaccess() ) {
434 return SiteGuard_Htaccess::$last_reason;
435 }
436 return array();
437 }
438
439 static function feature_off( $old_slug = null ) {
440 // Remove .htaccess feature
441 SiteGuard_Htaccess::clear_settings( self::$htaccess_mark );
442
443 // Remove stubs
444 $that = new self();
445 $that->remove_stub( $that->old_stub_abspath() );
446 if ( $that->slug() !== $that->old_slug() ) {
447 $that->remove_stub( $that->stub_abspath() );
448 }
449
450 // reset mode
451 global $siteguard_config;
452 $siteguard_config->set( 'renamelogin_stub', SITEGUARD_RENAME_MODE_HTACCESS );
453 $siteguard_config->update();
454
455 return true;
456 }
457
458 public function guard_disabled_entry() {
459 global $siteguard_config, $wp;
460
461 if ( '1' === $siteguard_config->get( 'renamelogin_enable' ) ) {
462 return;
463 }
464
465 $slug = $this->slug();
466 $req_path = isset( $_SERVER['REQUEST_URI'] ) ? (string) parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH ) : '';
467
468 $hit_slug = ( $req_path !== '' && rtrim( $req_path, '/' ) === '/' . $slug );
469 $hit_stub = ( $req_path !== '' && rtrim( $req_path, '/' ) === '/' . $this->stub_filename() );
470
471 if ( $hit_slug || $hit_stub ) {
472 if ( isset( $_REQUEST['action'] ) && $_REQUEST['action'] === 'logout' ) {
473 wp_safe_redirect( wp_logout_url() );
474 exit;
475 }
476 $this->set_404();
477 }
478 }
479
480 public function handle_siteguard_rescue() {
481 global $siteguard_config;
482
483 if ( '1' !== $siteguard_config->get( 'renamelogin_enable' ) ) {
484 return;
485 }
486
487 if ( '1' !== $siteguard_config->get( 'rescue_enable' ) ) {
488 return;
489 }
490 if ( ! isset( $_GET['siteguard_rescue'] ) || '1' !== (string) $_GET['siteguard_rescue'] ) {
491 return;
492 }
493
494 if ( $this->current_rescue_count() >= 3 ) {
495 $this->fixed_delay();
496 status_header( 429 );
497 nocache_headers();
498 $this->render_rescue_message( esc_html__( 'Request limit reached. Please try again later.', 'siteguard' ) );
499 exit;
500 }
501
502 if ( 'POST' === $_SERVER['REQUEST_METHOD'] ) {
503 $this->increment_rescue_counter();
504 $this->process_rescue_post();
505 exit;
506 }
507 $this->render_rescue_form();
508 exit;
509 }
510
511 private function rescue_rate_key( $ip ) {
512 return 'sg_rescue_count_' . md5( (string) $ip ); }
513 private function increment_rescue_counter() {
514 $ip = $this->get_ip();
515 $key = $this->rescue_rate_key( $ip );
516 $cnt = (int) get_transient( $key );
517 ++$cnt;
518 set_transient( $key, $cnt, HOUR_IN_SECONDS );
519 return $cnt;
520 }
521 private function current_rescue_count() {
522 $ip = $this->get_ip();
523 $key = $this->rescue_rate_key( $ip );
524 return (int) get_transient( $key );
525 }
526
527 private function render_rescue_form( $errors = array(), $email_value = '' ) {
528 // This form is what an administrator locked out of the login page has
529 // left, and it draws the same CAPTCHA image. On a server that cannot
530 // render one, generating it here would take the rescue path down with the
531 // same 500 as the login page. Show the form without the CAPTCHA instead:
532 // the rate limit of three attempts per hour per IP, the fixed delay and
533 // the identical response whether or not the address exists all still
534 // apply, so this does not turn into a usable oracle.
535 $captcha_available = SiteGuard_CAPTCHA::is_captcha_available();
536 $prefix = '';
537 $imgsrc = '';
538 if ( $captcha_available ) {
539 $captcha = new SiteGuardReallySimpleCaptcha();
540 $language = get_bloginfo( 'language' );
541 ( strpos( $language, 'ja' ) === 0 ) ? $captcha->set_lang_mode( 'jp' ) : $captcha->set_lang_mode( 'en' );
542 $prefix = siteguard_rand();
543 $word = $captcha->generate_random_word();
544 $captcha->generate_image( $prefix, $word );
545 $imgsrc = esc_url( WP_CONTENT_URL . '/siteguard/' . $prefix . '.png' );
546 }
547
548 $action = esc_url( add_query_arg( 'siteguard_rescue', '1', site_url( '/' ) ) );
549
550 nocache_headers();
551 echo '<!DOCTYPE html><html><head><meta charset="' . esc_attr( get_bloginfo( 'charset' ) ) . '">';
552 echo '<meta name="robots" content="noindex,nofollow" />';
553 echo '<title>' . esc_html__( 'Login URL Rescue', 'siteguard' ) . '</title>';
554 echo '</head><body>';
555 echo '<h1>' . esc_html__( 'Login URL Rescue', 'siteguard' ) . '</h1>';
556
557 if ( ! empty( $errors ) ) {
558 echo '<div role="alert" style="color:#b00;">';
559 foreach ( (array) $errors as $e ) {
560 echo '<p>' . esc_html( $e ) . '</p>';
561 }
562 echo '</div>';
563 }
564
565 echo '<form method="post" action="' . $action . '">';
566 wp_nonce_field( 'siteguard_rescue', 'siteguard_rescue_nonce' );
567
568 echo '<p><label>' . esc_html__( 'Administrator email address', 'siteguard' ) . '<br />';
569 echo '<input type="email" name="siteguard_rescue_email" value="' . esc_attr( $email_value ) . '" required style="min-width:280px;" />';
570 echo '</label></p>';
571
572 if ( $captcha_available ) {
573 echo '<p><img src="' . $imgsrc . '" alt="CAPTCHA" /></p>';
574 echo '<p><label>' . esc_html__( 'Enter the characters shown above', 'siteguard' ) . '<br />';
575 echo '<input type="text" name="siteguard_captcha" value="" size="10" required />';
576 echo '</label></p>';
577 echo '<input type="hidden" name="siteguard_captcha_prefix" value="' . esc_attr( $prefix ) . '" />';
578 }
579
580 echo '<p><button type="submit">' . esc_html__( 'Send email', 'siteguard' ) . '</button></p>';
581 echo '</form>';
582
583 echo '</body></html>';
584 }
585
586 private function uniform_delay( $start_ts_ms, $min_ms = 1800, $max_ms = 3200 ) {
587 $target = (int) wp_rand( $min_ms, $max_ms );
588 $elapsed = (int) ( ( microtime( true ) * 1000 ) - $start_ts_ms );
589 $remain = $target - $elapsed;
590 if ( $remain > 0 ) {
591 usleep( $remain * 1000 );
592 }
593 }
594 private function fixed_delay( $min_ms = 1800, $max_ms = 3200 ) {
595 $target = (int) wp_rand( $min_ms, $max_ms );
596 if ( $target > 0 ) {
597 usleep( $target * 1000 );
598 }
599 }
600
601 private function process_rescue_post() {
602 $start = (int) round( microtime( true ) * 1000 );
603
604 if ( ! isset( $_POST['siteguard_rescue_nonce'] ) || ! wp_verify_nonce( $_POST['siteguard_rescue_nonce'], 'siteguard_rescue' ) ) {
605 status_header( 400 );
606 $this->uniform_delay( $start );
607 $this->render_rescue_form( array( esc_html__( 'Invalid request.', 'siteguard' ) ), isset( $_POST['siteguard_rescue_email'] ) ? sanitize_email( $_POST['siteguard_rescue_email'] ) : '' );
608 return;
609 }
610
611 email_exists( 'dummy@example.com' );
612
613 $email = isset( $_POST['siteguard_rescue_email'] ) ? sanitize_email( $_POST['siteguard_rescue_email'] ) : '';
614 $cap = isset( $_POST['siteguard_captcha'] ) ? sanitize_text_field( $_POST['siteguard_captcha'] ) : '';
615 $pref = isset( $_POST['siteguard_captcha_prefix'] ) ? sanitize_text_field( $_POST['siteguard_captcha_prefix'] ) : '';
616
617 $errors = array();
618 if ( empty( $email ) || ! is_email( $email ) ) {
619 $errors[] = esc_html__( 'Please enter a valid email address.', 'siteguard' );
620 }
621
622 // Verify only what the form was able to present. render_rescue_form()
623 // leaves the CAPTCHA out when the server cannot draw one, and demanding it
624 // here would reject every submission and close the rescue path for good.
625 if ( SiteGuard_CAPTCHA::is_captcha_available() ) {
626 $captcha = new SiteGuardReallySimpleCaptcha();
627 $valid_captcha = ( $pref !== '' && $cap !== '' && $captcha->check( $pref, $cap, true ) );
628 if ( ! $valid_captcha ) {
629 $errors[] = esc_html__( 'Invalid CAPTCHA.', 'siteguard' );
630 }
631 }
632
633 if ( ! empty( $errors ) ) {
634 $this->uniform_delay( $start );
635 $this->render_rescue_form( $errors, $email );
636 return;
637 }
638
639 $user = get_user_by( 'email', $email );
640 if ( $user && user_can( $user, 'manage_options' ) ) {
641 $url = $this->get_login_url();
642 $subject = esc_html__( 'WordPress: Login URL Rescue', 'siteguard' );
643 $body = sprintf(
644 esc_html__( "You requested the login URL.\n\nURL: %s\n\nIf you did not request this, you can ignore this email.\n\n--\nSiteGuard WP Plugin", 'siteguard' ),
645 $url
646 );
647 @wp_mail( $email, $subject, $body );
648 }
649
650 nocache_headers();
651 $this->uniform_delay( $start );
652 $this->render_rescue_message( esc_html__( 'An email has been sent if the address exists.', 'siteguard' ) );
653 }
654
655 private function render_rescue_message( $message ) {
656 echo '<!DOCTYPE html><html><head><meta charset="' . esc_attr( get_bloginfo( 'charset' ) ) . '">';
657 echo '<meta name="robots" content="noindex,nofollow" />';
658 echo '<title>' . esc_html__( 'Login URL Rescue', 'siteguard' ) . '</title>';
659 echo '</head><body>';
660 echo '<h1>' . esc_html__( 'Login URL Rescue', 'siteguard' ) . '</h1>';
661 echo '<p>' . esc_html( $message ) . '</p>';
662 echo '</body></html>';
663 }
664
665 public function get_login_url() {
666 global $siteguard_config;
667 if ( '0' === $siteguard_config->get( 'renamelogin_enable' ) ) {
668 return rtrim( site_url(), '/' ) . '/wp-login.php';
669 }
670 if ( $this->is_stub_mode() ) {
671 return $this->stub_url(); }
672 return rtrim( site_url(), '/' ) . '/' . $this->slug();
673 }
674
675 function set_404() {
676 global $wp_query;
677 status_header( 404 );
678 $wp_query->set_404();
679 if ( ( ( $template = get_404_template() ) || ( $template = get_index_template() ) )
680 && ( $template = apply_filters( 'template_include', $template ) ) ) {
681 include $template;
682 }
683 die;
684 }
685
686 function send_notify() {
687 $subject = esc_html__( 'WordPress: Login page URL changed', 'siteguard' );
688 $body = sprintf( esc_html__( "Please bookmark the new login URL.\n\n%s\n\n--\nSiteGuard WP Plugin", 'siteguard' ), $this->get_login_url() );
689
690 $user_query = new WP_User_Query( array( 'role' => 'Administrator' ) );
691 if ( ! empty( $user_query->results ) ) {
692 foreach ( $user_query->results as $user ) {
693 $user_email = $user->get( 'user_email' );
694 if ( true !== @wp_mail( $user_email, $subject, $body ) ) {
695 siteguard_error_log( 'Failed send mail. To:' . $user_email . ' Subject:' . esc_html( $subject ) );
696 }
697 }
698 }
699 }
700
701 function handler_stop_redirect( $scheme ) {
702 global $siteguard_config;
703 $redirect_enable = $siteguard_config->get( 'redirect_enable' );
704 if ( $redirect_enable == 1 ) {
705 if ( $user_id = wp_validate_auth_cookie( '', $scheme ) ) {
706 return $scheme;
707 }
708 wp_safe_redirect( home_url() );
709 exit;
710 }
711 }
712
713 function handler_plugins_loaded() {
714 if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
715 return;
716 }
717 $request = parse_url( $_SERVER['REQUEST_URI'] );
718 $denied_slugs = array( 'wp-register', 'wp-signup', 'wp-activate' );
719 $denied_slugs_to_regex = implode( '|', $denied_slugs );
720
721 $is_denied = false;
722 if ( is_array( $request ) && isset( $request['path'] ) ) {
723 $is_denied = preg_match( '#\/(' . $denied_slugs_to_regex . ')(\.php)?$#i', untrailingslashit( $request['path'] ) );
724 }
725 if ( $is_denied && ! is_admin() ) {
726 $this->denied_login = true;
727 // In stub mode, .htaccess rules are absent; block signup/activate directly.
728 if ( $this->is_stub_mode() ) {
729 status_header( 404 );
730 nocache_headers();
731 exit;
732 }
733 }
734 }
735
736 public function filter_logout_url( $logout_url, $redirect ) {
737 global $siteguard_config;
738 if ( '1' !== $siteguard_config->get( 'renamelogin_enable' ) ) {
739 return $logout_url;
740 }
741 $base = $this->get_login_url();
742
743 // Rebuild the logout URL from scratch to mirror WordPress core's
744 // wp_logout_url(): urlencode redirect_to, then wrap with wp_nonce_url().
745 //
746 // We deliberately do NOT parse_str( $logout_url ) and re-emit its query:
747 // the incoming $logout_url has already been passed through wp_nonce_url(),
748 // which esc_html()-encodes it (so separators are "&amp;"). parse_str() then
749 // splits on "&", mangling keys into "amp;redirect_to"/"amp;_wpnonce" and
750 // url-decoding the value back to its raw form. add_query_arg() re-emits that
751 // raw value, so an attacker-controlled redirect_to would break out of the
752 // href printed (without esc_url) by wp_nonce_ays( 'log-out' ) -> XSS.
753 //
754 // wp_logout_url() applies the same urlencode() contract that consumers rely
755 // on, and wp_nonce_url() re-adds the logout nonce, so building fresh keeps
756 // the URL correctly (and singly) encoded.
757 $args = array( 'action' => 'logout' );
758 if ( ! empty( $redirect ) ) {
759 $args['redirect_to'] = urlencode( $redirect );
760 }
761 return wp_nonce_url( add_query_arg( $args, $base ), 'log-out' );
762 }
763
764 public function rewrite_adminbar_logout( $wp_admin_bar ) {
765 if ( ! is_user_logged_in() ) {
766 return; }
767 if ( ! is_object( $wp_admin_bar ) ) {
768 return; }
769 foreach ( array( 'logout', 'log-out' ) as $id ) {
770 $node = $wp_admin_bar->get_node( $id );
771 if ( ! $node || empty( $node->href ) ) {
772 continue; }
773 $new = $this->filter_logout_url( $node->href, '' );
774 if ( $new && $new !== $node->href ) {
775 $node->href = $new;
776 $wp_admin_bar->add_node( $node );
777 }
778 }
779 }
780
781 /**
782 * Result of the login URL conflict probe for this request, or null when no
783 * other plugin was found to be changing the login page URL.
784 *
785 * Keys: 'url' (the login URL the other plugin produces), 'plugin' (its name,
786 * or '' when it could not be resolved), 'same_url' (bool) and 'fatal_risk'
787 * (bool). Populated by check_login_url_conflict() on admin_init.
788 *
789 * @return array|null
790 */
791 public static function get_login_url_conflict() {
792 return self::$conflict;
793 }
794
795 /**
796 * Run the login URL conflict probe once per admin request.
797 *
798 * Only admin screens are checked: the result is only ever shown to an
799 * administrator, and admin_init is the earliest hook that runs after every
800 * other plugin has registered its URL filters. admin-ajax.php also fires
801 * admin_init, so AJAX (and cron / REST) is excluded to keep the probe off
802 * background requests.
803 */
804 public function check_login_url_conflict() {
805 global $siteguard_config;
806
807 if ( self::$conflict_checked ) {
808 return;
809 }
810 if ( ! is_admin() || wp_doing_ajax() || wp_doing_cron() ) {
811 return;
812 }
813 if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
814 return;
815 }
816 if ( ! current_user_can( 'manage_options' ) ) {
817 return;
818 }
819 if ( '1' !== $siteguard_config->get( 'renamelogin_enable' ) ) {
820 return;
821 }
822
823 self::$conflict_checked = true;
824 self::$conflict = $this->probe_foreign_login_url();
825 }
826
827 /**
828 * Detect whether another plugin is also changing the login page URL.
829 *
830 * Rather than checking for known plugins by name, this observes the symptom
831 * itself: every plugin that renames the login page has to rewrite the URLs
832 * WordPress generates for wp-login.php, and does so through the 'site_url'
833 * or 'login_url' filters. So an unfiltered wp-login.php URL is pushed
834 * through those filters with our own rewrite suspended (self::$probing); if
835 * "wp-login.php" is gone from the result, somebody else is rewriting it too.
836 *
837 * Requiring "wp-login.php" to disappear (rather than any change at all) is
838 * what keeps plugins that merely decorate site_url — multilingual plugins
839 * adding a language prefix, for instance — from being reported.
840 *
841 * @return array|null Conflict details, or null when nothing was detected.
842 */
843 private function probe_foreign_login_url() {
844 // Build the URL without site_url() so the filters under test are applied
845 // exactly once, by us, below.
846 $raw = rtrim( (string) get_option( 'siteurl' ), '/' ) . '/wp-login.php';
847
848 $probes = array(
849 'site_url' => array( 'wp-login.php', null, null ),
850 'login_url' => array( '', false ),
851 );
852
853 // The probe runs other plugins' filter callbacks. A failure in one of
854 // them has to stay inside that one probe: it must not take the admin
855 // screen down, and it must not skip the remaining hook — a plugin that
856 // only filters login_url is exactly the kind this feature looks for.
857 // self::$probing is cleared either way, or the URL rewriting would stay
858 // disabled for the rest of the request.
859 $found = null;
860 self::$probing = true;
861 foreach ( $probes as $hook => $args ) {
862 $params = array_merge( array( $raw ), $args );
863 try {
864 $probed = call_user_func_array( 'apply_filters', array_merge( array( $hook ), $params ) );
865 } catch ( Exception $e ) {
866 siteguard_error_log( 'Login URL conflict probe failed on ' . $hook . ': ' . $e->getMessage() );
867 continue;
868 } catch ( Throwable $e ) {
869 siteguard_error_log( 'Login URL conflict probe failed on ' . $hook . ': ' . $e->getMessage() );
870 continue;
871 }
872 if ( ! is_string( $probed ) || '' === $probed || false !== strpos( $probed, 'wp-login.php' ) ) {
873 continue;
874 }
875 // The conflict itself is established at this point. Failing to name
876 // the plugin responsible must not discard it.
877 $plugin = '';
878 try {
879 $plugin = $this->conflicting_plugin_names( $hook, $raw, $args );
880 } catch ( Exception $e ) {
881 siteguard_error_log( 'Naming the conflicting plugin failed: ' . $e->getMessage() );
882 } catch ( Throwable $e ) {
883 siteguard_error_log( 'Naming the conflicting plugin failed: ' . $e->getMessage() );
884 }
885 $found = array(
886 'url' => $probed,
887 'plugin' => $plugin,
888 );
889 break;
890 }
891 self::$probing = false;
892
893 if ( null === $found ) {
894 return null;
895 }
896
897 $same_url = $this->is_same_login_url( $this->get_login_url(), $found['url'] );
898
899 return array(
900 'url' => $found['url'],
901 'plugin' => $found['plugin'],
902 'same_url' => $same_url,
903 // The fatal error only happens when our .htaccess rewrite makes the
904 // core wp-login.php execute (declaring login_header() and friends)
905 // while the other plugin, matching the very same URL, loads its own
906 // copy of the login page on top of it. In stub (.php) mode our URL
907 // ends in ".php", which no other plugin's slug matches.
908 'fatal_risk' => ( $same_url && ! $this->is_stub_mode() ),
909 );
910 }
911
912 /**
913 * Names of the active plugins whose callbacks on $hook_name remove
914 * wp-login.php from the URL. Each foreign callback is applied on its own so
915 * that a plugin which merely happens to filter the same hook is not blamed
916 * for another plugin's rewrite.
917 *
918 * @param string $hook_name Filter to inspect.
919 * @param string $raw Unfiltered wp-login.php URL.
920 * @param array $args Remaining filter arguments.
921 * @return string Comma separated plugin names, or '' when none could be resolved.
922 */
923 private function conflicting_plugin_names( $hook_name, $raw, $args ) {
924 global $wp_filter;
925
926 if ( empty( $wp_filter[ $hook_name ] ) ) {
927 return '';
928 }
929
930 $self_dir = wp_normalize_path( SITEGUARD_PATH );
931 $names = array();
932
933 foreach ( $wp_filter[ $hook_name ] as $callbacks ) {
934 if ( ! is_array( $callbacks ) ) {
935 continue;
936 }
937 foreach ( $callbacks as $callback ) {
938 if ( ! isset( $callback['function'] ) || ! is_callable( $callback['function'] ) ) {
939 continue;
940 }
941 $file = $this->callback_file( $callback['function'] );
942 if ( '' === $file || 0 === strpos( wp_normalize_path( $file ), $self_dir ) ) {
943 continue;
944 }
945 $name = $this->plugin_name_by_file( $file );
946 if ( '' === $name || in_array( $name, $names, true ) ) {
947 continue;
948 }
949
950 $accepted = isset( $callback['accepted_args'] ) ? (int) $callback['accepted_args'] : 1;
951 $params = array_slice( array_merge( array( $raw ), $args ), 0, max( 1, $accepted ) );
952 try {
953 $result = call_user_func_array( $callback['function'], $params );
954 } catch ( Exception $e ) {
955 continue;
956 } catch ( Throwable $e ) {
957 continue;
958 }
959 if ( is_string( $result ) && false === strpos( $result, 'wp-login.php' ) ) {
960 $names[] = $name;
961 }
962 }
963 }
964
965 return implode( ', ', $names );
966 }
967
968 /**
969 * Source file a filter callback is defined in, or '' when it cannot be
970 * resolved (internal functions, or anything Reflection refuses).
971 *
972 * @param mixed $callback Callback as stored in $wp_filter.
973 * @return string
974 */
975 private function callback_file( $callback ) {
976 try {
977 if ( is_array( $callback ) && 2 === count( $callback ) ) {
978 $class = is_object( $callback[0] ) ? get_class( $callback[0] ) : $callback[0];
979 $ref = new ReflectionMethod( $class, $callback[1] );
980 } elseif ( is_string( $callback ) && false !== strpos( $callback, '::' ) ) {
981 $ref = new ReflectionMethod( $callback );
982 } elseif ( is_object( $callback ) && ! ( $callback instanceof Closure ) ) {
983 $ref = new ReflectionMethod( $callback, '__invoke' );
984 } else {
985 $ref = new ReflectionFunction( $callback );
986 }
987 } catch ( Exception $e ) {
988 return '';
989 }
990
991 $file = $ref->getFileName();
992 return is_string( $file ) ? $file : '';
993 }
994
995 /**
996 * Name of the active plugin that owns $file, or '' when the file does not
997 * belong to one (a theme, a must-use plugin, or WordPress itself).
998 *
999 * @param string $file Absolute path.
1000 * @return string
1001 */
1002 private function plugin_name_by_file( $file ) {
1003 $plugin_dir = trailingslashit( wp_normalize_path( WP_PLUGIN_DIR ) );
1004 $file = wp_normalize_path( $file );
1005 if ( 0 !== strpos( $file, $plugin_dir ) ) {
1006 return '';
1007 }
1008
1009 $relative = substr( $file, strlen( $plugin_dir ) );
1010 $slug = ( false !== strpos( $relative, '/' ) ) ? substr( $relative, 0, strpos( $relative, '/' ) ) : $relative;
1011 if ( '' === $slug ) {
1012 return '';
1013 }
1014
1015 foreach ( get_plugins() as $plugin_file => $data ) {
1016 if ( $plugin_file !== $slug && 0 !== strpos( $plugin_file, $slug . '/' ) ) {
1017 continue;
1018 }
1019 if ( is_plugin_active( $plugin_file ) && ! empty( $data['Name'] ) ) {
1020 return $data['Name'];
1021 }
1022 }
1023 return '';
1024 }
1025
1026 /**
1027 * Whether two login URLs would be requested by the same path. Trailing
1028 * slashes are ignored (some plugins hand out a trailing slashed URL); a
1029 * query string has to match as well, because a plugin that puts its slug in
1030 * the query (e.g. /?secret) is not hit by a request for our path.
1031 *
1032 * @param string $a URL.
1033 * @param string $b URL.
1034 * @return bool
1035 */
1036 private function is_same_login_url( $a, $b ) {
1037 $path_a = untrailingslashit( (string) wp_parse_url( $a, PHP_URL_PATH ) );
1038 $path_b = untrailingslashit( (string) wp_parse_url( $b, PHP_URL_PATH ) );
1039 $query_a = (string) wp_parse_url( $a, PHP_URL_QUERY );
1040 $query_b = (string) wp_parse_url( $b, PHP_URL_QUERY );
1041
1042 return ( $path_a === $path_b && $query_a === $query_b );
1043 }
1044
1045 /**
1046 * Warning text for a detected conflict. Returns pre-escaped HTML.
1047 *
1048 * @param array $conflict As returned by get_login_url_conflict().
1049 * @return string
1050 */
1051 public static function conflict_message( $conflict ) {
1052 $url = '<code>' . esc_html( $conflict['url'] ) . '</code>';
1053 $plugin = '' !== $conflict['plugin'] ? '<strong>' . esc_html( $conflict['plugin'] ) . '</strong>' : '';
1054
1055 if ( ! empty( $conflict['fatal_risk'] ) ) {
1056 if ( '' !== $plugin ) {
1057 return sprintf(
1058 /* translators: 1: plugin name, 2: login URL */
1059 esc_html__( '%1$s is also changing the login page URL, to the same URL as SiteGuard (%2$s). In this state the login page can stop working with a PHP fatal error. Please turn off the login page URL change feature in one of the two plugins.', 'siteguard' ),
1060 $plugin,
1061 $url
1062 );
1063 }
1064 return sprintf(
1065 /* translators: %s: login URL */
1066 esc_html__( 'Another active plugin is also changing the login page URL, to the same URL as SiteGuard (%s). In this state the login page can stop working with a PHP fatal error. Please turn off the login page URL change feature in one of the two plugins.', 'siteguard' ),
1067 $url
1068 );
1069 }
1070
1071 if ( '' !== $plugin ) {
1072 return sprintf(
1073 /* translators: 1: plugin name, 2: login URL */
1074 esc_html__( '%1$s is also changing the login page URL (%2$s). Two different login URLs stay available, which weakens this feature, and the two can conflict later. Please use the login page URL change feature in only one of the two plugins.', 'siteguard' ),
1075 $plugin,
1076 $url
1077 );
1078 }
1079 return sprintf(
1080 /* translators: %s: login URL */
1081 esc_html__( 'Another active plugin is also changing the login page URL (%s). Two different login URLs stay available, which weakens this feature, and the two can conflict later. Please use the login page URL change feature in only one of the two plugins.', 'siteguard' ),
1082 $url
1083 );
1084 }
1085
1086 /**
1087 * Admin notice for the case that actually breaks the site: both plugins
1088 * pointing at the same URL while we serve it through the .htaccess rewrite.
1089 * The milder cases are reported on the Rename Login screen only, so that a
1090 * situation which is not currently breaking anything does not follow the
1091 * administrator around the dashboard.
1092 */
1093 public function maybe_notice_login_url_conflict() {
1094 if ( ! current_user_can( 'manage_options' ) ) {
1095 return;
1096 }
1097 // The Rename Login screen prints its own, always-visible block.
1098 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- reading which admin screen is being rendered; no state is changed.
1099 $screen = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
1100 if ( 'siteguard_rename_login' === $screen ) {
1101 return;
1102 }
1103 $conflict = self::get_login_url_conflict();
1104 if ( null === $conflict || empty( $conflict['fatal_risk'] ) ) {
1105 return;
1106 }
1107
1108 echo '<div class="notice notice-error is-dismissible"><p>' . wp_kses_post( self::conflict_message( $conflict ) ) . '</p></div>';
1109 }
1110
1111 public function maybe_notice_stub_failed() {
1112 if ( ! current_user_can( 'manage_options' ) ) {
1113 return; }
1114 if ( get_transient( self::STUB_WRITE_FAIL_TRANSIENT ) ) {
1115 echo '<div class="notice notice-warning"><p>';
1116 echo esc_html__( 'SiteGuard: Could not create the required login file. Please check file permissions or contact your hosting provider.', 'siteguard' );
1117 echo '</p></div>';
1118 delete_transient( self::STUB_WRITE_FAIL_TRANSIENT );
1119 }
1120 }
1121 }
1122