PluginProbe ʕ •ᴥ•ʔ
SiteGuard WP Plugin / 1.8.7
SiteGuard WP Plugin v1.8.7
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 1 month ago siteguard-base.php 1 month ago siteguard-captcha.php 1 month ago siteguard-config.php 11 months ago siteguard-disable-author-query.php 2 weeks ago siteguard-disable-pingback.php 1 month ago siteguard-disable-xmlrpc.php 1 month ago siteguard-htaccess.php 2 weeks ago siteguard-login-alert.php 1 month ago siteguard-login-history.php 1 month ago siteguard-login-lock.php 1 month ago siteguard-rename-login.php 1 week ago siteguard-updates-notify.php 1 month ago siteguard-waf-exclude-rule.php 1 month ago
siteguard-rename-login.php
712 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 protected static $incompatible_plugins = array(
10 'WordPress HTTPS (SSL)' => 'wordpress-https/wordpress-https.php',
11 'qTranslate X' => 'qtranslate-x/qtranslate.php',
12 );
13 public static $htaccess_mark = '#==== SITEGUARD_RENAME_LOGIN_SETTINGS';
14
15 const STUB_WRITE_FAIL_TRANSIENT = 'siteguard_rl_stub_fail';
16
17 function __construct() {
18 global $siteguard_config;
19
20 add_filter( 'logout_url', array( $this, 'filter_logout_url' ), 10, 2 );
21 add_action( 'admin_bar_menu', array( $this, 'rewrite_adminbar_logout' ), 999 );
22 add_action( 'admin_notices', array( $this, 'maybe_notice_stub_failed' ) );
23
24 if ( '1' === $siteguard_config->get( 'renamelogin_enable' ) ) {
25 if ( null !== $this->get_active_incompatible_plugins() ) {
26 $siteguard_config->set( 'renamelogin_enable', '0' );
27 $siteguard_config->update();
28 $this->feature_off();
29 } else {
30 $this->add_filter();
31 }
32 }
33
34 add_action( 'template_redirect', array( $this, 'guard_disabled_entry' ), 0 );
35 add_action( 'template_redirect', array( $this, 'handle_siteguard_rescue' ), 0 );
36 }
37
38 static function get_mark() {
39 return self::$htaccess_mark;
40 }
41
42 function init() {
43 global $siteguard_config;
44
45 $this->denied_login = false;
46
47 if ( '' === $siteguard_config->get( 'renamelogin_path' ) ) {
48 $siteguard_config->set( 'renamelogin_path', 'login_' . sprintf( '%05d', siteguard_rand( 1, 99999 ) ) );
49 }
50 if ( '' === $siteguard_config->get( 'redirect_enable' ) ) {
51 $siteguard_config->set( 'redirect_enable', '0' );
52 }
53 if ( '' === $siteguard_config->get( 'rescue_enable' ) ) {
54 $siteguard_config->set( 'rescue_enable', '1' );
55 }
56 if ( '' === $siteguard_config->get( 'renamelogin_stub' ) ) {
57 $siteguard_config->set( 'renamelogin_stub', SITEGUARD_RENAME_MODE_HTACCESS ); // Apache=0 / Nginx=1
58 }
59 $siteguard_config->update();
60
61 if ( true === siteguard_check_multisite()
62 && null === $this->get_active_incompatible_plugins()
63 ) {
64 $siteguard_config->set( 'renamelogin_enable', '1' );
65 $siteguard_config->update();
66 if ( ! $this->feature_on() ) {
67 $siteguard_config->set( 'renamelogin_enable', '0' );
68 $siteguard_config->update();
69 }
70 } else {
71 $siteguard_config->set( 'renamelogin_enable', '0' );
72 $siteguard_config->update();
73 }
74 }
75
76 function get_active_incompatible_plugins() {
77 $result = array();
78 foreach ( self::$incompatible_plugins as $name => $path ) {
79 if ( is_plugin_active( $path ) ) {
80 $result[] = $name;
81 }
82 }
83 return empty( $result ) ? null : $result;
84 }
85
86 function add_filter() {
87 add_filter( 'plugins_loaded', array( $this, 'handler_plugins_loaded' ), 9999 );
88
89 add_action( 'init', array( $this, 'guard_wp_login_direct_access' ), 0 );
90 add_filter( 'login_init', array( $this, 'handler_login_init' ), 10, 2 );
91 add_filter( 'site_url', array( $this, 'handler_site_url' ), 10, 2 );
92 add_filter( 'network_site_url', array( $this, 'handler_site_url' ), 10, 2 );
93 add_filter( 'wp_redirect', array( $this, 'handler_wp_redirect' ), 10, 2 );
94 add_filter( 'register', array( $this, 'handler_register' ) );
95 add_filter( 'auth_redirect_scheme', array( $this, 'handler_stop_redirect' ), 9999 );
96
97 remove_action( 'template_redirect', 'wp_redirect_admin_locations', 1000 );
98 }
99
100 public function can_use_htaccess() {
101 if ( ! isset( $_SERVER['SERVER_SOFTWARE'] )
102 || ( false === strpos( strtolower( $_SERVER['SERVER_SOFTWARE'] ), 'apache' ) && false === strpos( strtolower( $_SERVER['SERVER_SOFTWARE'] ), 'litespeed' ) )
103 ) {
104 return false;
105 }
106 return SiteGuard_Htaccess::is_writable_htaccess();
107 }
108
109 private function is_stub_mode() {
110 global $siteguard_config;
111 return SITEGUARD_RENAME_MODE_STUB === $siteguard_config->get( 'renamelogin_stub' );
112 }
113
114 private function slug() {
115 global $siteguard_config;
116 $slug = trim( (string) $siteguard_config->get( 'renamelogin_path' ), '/' );
117 return $slug === '' ? 'login' : $slug;
118 }
119
120 private function old_slug() {
121 global $siteguard_config;
122 $slug = trim( (string) $siteguard_config->get( 'oldlogin_path' ), '/' );
123 return $slug === '' ? 'login' : $slug;
124 }
125
126 private function stub_filename() {
127 return $this->slug() . '.php';
128 }
129
130 private function old_stub_filename() {
131 return $this->old_slug() . '.php';
132 }
133
134 private function stub_abspath() {
135 return trailingslashit( ABSPATH ) . $this->stub_filename();
136 }
137
138 private function old_stub_abspath() {
139 return trailingslashit( ABSPATH ) . $this->old_stub_filename();
140 }
141
142 private function stub_url() {
143 return rtrim( site_url(), '/' ) . '/' . $this->stub_filename();
144 }
145
146 private function install_stub() {
147 $file = $this->stub_abspath();
148 $code = "<?php\n/* Generated by SiteGuard WP Plugin */\nrequire_once __DIR__ . '/wp-login.php';\n";
149 $ok = @file_put_contents( $file, $code );
150 if ( false === $ok ) {
151 set_transient( self::STUB_WRITE_FAIL_TRANSIENT, 1, MINUTE_IN_SECONDS * 10 );
152 return false;
153 }
154 @chmod( $file, 0644 );
155 return true;
156 }
157
158 private function remove_stub( $file ) {
159 if ( file_exists( $file ) ) {
160 // To prevent accidental deletion of important files, check if the file was generated by this plugin.
161 $content = file_get_contents( $file, false, null, 0, 100 );
162 if ( false !== $content && false !== strpos( $content, '/* Generated by SiteGuard WP Plugin */' ) ) {
163 @unlink( $file );
164 }
165 }
166 }
167
168 /**
169 * Check if the current request is for the login page (renamed or original).
170 *
171 * @return bool
172 */
173 public function is_login_request() {
174 $req_path = isset( $_SERVER['REQUEST_URI'] ) ? (string) parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH ) : '';
175 $script_name = isset( $_SERVER['SCRIPT_NAME'] ) ? basename( $_SERVER['SCRIPT_NAME'] ) : '';
176 return ( $script_name === 'wp-login.php' || $req_path === '/' . $this->stub_filename() || $req_path === '/' . $this->slug() );
177 }
178
179 /**
180 * Whether REQUEST_URI targets a default login entry point (wp-login /
181 * wp-register) rather than the configured renamed slug.
182 *
183 * Every path segment is reduced to its "stem" (lowercased, any extension
184 * dropped) and compared against wp-login / wp-register. Checking every
185 * segment — not just the basename — is required because Apache MultiViews /
186 * content negotiation serves wp-login.php for an extensionless request, and
187 * with AcceptPathInfo anything after it becomes PATH_INFO:
188 * /wp-login -> wp-login.php
189 * /wp-login/ -> wp-login.php (PATH_INFO "/")
190 * /wp-login/anything -> wp-login.php (PATH_INFO "/anything") <-- basename is "anything"
191 * /wp-login.php/x -> wp-login.php (PATH_INFO "/x")
192 * A basename-only check misses the PATH_INFO variants (the basename is the
193 * trailing segment). Scanning every segment also covers the nested forms that
194 * WordPress core canonicalizes to wp-login.php (e.g. /abc/wp-login.php,
195 * //wp-login.php) and subdirectory installs (/cms/wp-login/x).
196 *
197 * The configured slug is exempt so a site that renamed its login to a reserved
198 * word (e.g. "wp-register", which the settings screen still permits because
199 * wp-register.php does not exist) is not locked out of its own login. Renamed
200 * slugs only contain [a-zA-Z0-9_-] (no dot), so the extension stripping never
201 * collides with a slug.
202 */
203 private function targets_default_login() {
204 $slug = strtolower( $this->slug() );
205 $link = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_url( $_SERVER['REQUEST_URI'] ) : '';
206 // Collapse leading/duplicate slashes before parse_url, otherwise //wp-login.php
207 // is parsed as host=wp-login.php with a NULL path and would slip through.
208 $link = preg_replace( '#^/+#', '/', $link );
209 $path = (string) parse_url( $link, PHP_URL_PATH );
210 $path = preg_replace( '#/+#', '/', $path );
211 $path = urldecode( $path );
212 foreach ( explode( '/', $path ) as $seg ) {
213 if ( '' === $seg ) {
214 continue;
215 }
216 $seg = strtolower( $seg );
217 // Strip trailing whitespace, control characters and non-ASCII bytes so
218 // variants like "wp-login.php " or "wp-login.php%C2%A0" (NBSP) are caught
219 // after WordPress core URL normalization would treat them as wp-login.php.
220 $seg = preg_replace( '/[\s\x00-\x1f\x7f-\xff]+$/', '', $seg );
221 $stem = preg_replace( '/\..*$/', '', $seg );
222 if ( ( 'wp-login' === $stem || 'wp-register' === $stem ) && $stem !== $slug ) {
223 return true;
224 }
225 }
226 return false;
227 }
228
229 /**
230 * Block direct access to the default login entry points at the init hook.
231 *
232 * Runs before login_init so that requests routed through index.php fallback
233 * (e.g. //wp-login.php on subdirectory installs) are stopped before WordPress
234 * core URL canonicalization can leak the renamed slug via wp_redirect.
235 */
236 public function guard_wp_login_direct_access() {
237 if ( ! $this->targets_default_login() ) {
238 return;
239 }
240 // When wp-login.php is the executing script (a direct hit, or MultiViews
241 // content negotiation serving it for /wp-login, /wp-login/x, etc.),
242 // login_init fires and handler_login_init() renders the WordPress theme
243 // 404 via set_404(). Defer to it so the visitor gets the themed 404 page
244 // instead of a bare browser 404.
245 //
246 // Only the index.php fallback path is handled here with a bare 404 + exit:
247 // e.g. //wp-login.php on a subdirectory install, where wp-login.php does
248 // NOT execute (SCRIPT_NAME is index.php) and login_init never fires. That
249 // case must stop now, before WordPress core canonicalizes the URL and
250 // leaks the renamed slug via wp_redirect, and the main query is not yet
251 // set up to render a theme template safely.
252 $script = isset( $_SERVER['SCRIPT_NAME'] ) ? strtolower( basename( $_SERVER['SCRIPT_NAME'] ) ) : '';
253 if ( 'wp-login.php' === $script ) {
254 return;
255 }
256 status_header( 404 );
257 nocache_headers();
258 exit;
259 }
260
261 function handler_login_init() {
262 // See targets_default_login() for rationale, including the renamed slug
263 // exemption that keeps a "wp-login"/"wp-register" slug from 404ing the
264 // site's own login page.
265 if ( $this->targets_default_login() ) {
266 $this->set_404();
267 }
268 }
269
270 function convert_url( $link ) {
271 $result = $link;
272 $repl = $this->is_stub_mode() ? $this->stub_filename() : $this->slug();
273
274 if ( false !== strpos( $link, 'wp-login.php?action=register' ) && $this->denied_login ) {
275 $this->set_404();
276 } elseif ( false !== strpos( $link, 'wp-login.php' ) ) {
277 $result = str_replace( 'wp-login.php', $repl, $link );
278 }
279 return $result;
280 }
281
282 function handler_site_url( $link ) {
283 return $this->convert_url( $link ); }
284 function handler_register( $link ) {
285 return $this->convert_url( $link ); }
286 function handler_wp_redirect( $link, $status_code ) {
287 if ( ( ( strlen( $link ) <= 5 || 'http:' !== strtolower( substr( $link, 0, 5 ) ) ) && ( strlen( $link ) <= 6 || 'https:' !== strtolower( substr( $link, 0, 6 ) ) ) )
288 || ( isset( $_SERVER['HTTPS'] ) && strtolower( $_SERVER['HTTPS'] ) !== 'off' && 'https' === strtolower( substr( $link, 0, strpos( $link, '://' ) ) ) )
289 || ( ( ! isset( $_SERVER['HTTPS'] ) || strtolower( $_SERVER['HTTPS'] ) === 'off' ) && 'http' === strtolower( substr( $link, 0, strpos( $link, '://' ) ) ) ) ) {
290 return $this->convert_url( $link );
291 }
292 return $link;
293 }
294
295 private function htaccess_body() {
296 $slug = $this->slug();
297
298 $parse_url = parse_url( site_url() );
299 $base = '/';
300 if ( false !== $parse_url && isset( $parse_url['path'] ) && $parse_url['path'] !== '' ) {
301 $base = rtrim( $parse_url['path'], '/' ) . '/';
302 }
303
304 $ht = "<IfModule mod_rewrite.c>\n";
305 $ht .= " RewriteEngine on\n";
306 $ht .= " RewriteBase {$base}\n";
307 $ht .= " RewriteRule ^wp-signup\\.php 404-siteguard [L]\n";
308 $ht .= " RewriteRule ^wp-activate\\.php 404-siteguard [L]\n";
309 $ht .= " RewriteRule ^{$slug}(.*)$ wp-login.php\$1 [L]\n";
310 $ht .= "</IfModule>\n";
311
312 return $ht;
313 }
314
315 function feature_on() {
316 global $siteguard_htaccess, $siteguard_config;
317
318 // Remove .htaccess feature
319 SiteGuard_Htaccess::clear_settings( self::$htaccess_mark );
320
321 // Remove old stubs regardless of mode
322 $this->remove_stub( $this->old_stub_abspath() );
323 if ( $this->slug() !== $this->old_slug() ) {
324 $this->remove_stub( $this->stub_abspath() );
325 }
326
327 // Decide between .htaccess mode and stub (.php) mode, recording the reason
328 // when .htaccess cannot be used so the settings screen can explain why.
329 $reason = $this->htaccess_unavailable_reason();
330 if ( array() === $reason ) {
331 $data = $this->htaccess_body();
332 $mark = self::get_mark();
333 $ok = $siteguard_htaccess->update_settings( $mark, $data );
334 if ( $ok ) {
335 $siteguard_config->set( 'renamelogin_stub', SITEGUARD_RENAME_MODE_HTACCESS );
336 $siteguard_config->set( 'renamelogin_stub_reason', array() );
337 $siteguard_config->update();
338 return true;
339 }
340 // Writing the .htaccess block failed unexpectedly; fall back to stub.
341 $reason = array( 'code' => 'not_writable' );
342 }
343
344 if ( $this->install_stub() ) {
345 $siteguard_config->set( 'renamelogin_stub', SITEGUARD_RENAME_MODE_STUB );
346 $siteguard_config->set( 'renamelogin_stub_reason', $reason );
347 $siteguard_config->update();
348 siteguard_error_log( 'Rename Login fell back to stub (.php) mode. Reason: ' . wp_json_encode( $reason ) );
349 return true;
350 }
351
352 return false;
353 }
354
355 /**
356 * Why .htaccess mode cannot be used right now. Returns an empty array when it
357 * can be used; otherwise an array like array( 'code' => ..., 'url' => ... )
358 * describing the cause (server software, write permission, or which stage of
359 * the .htaccess self-test failed). Used to explain stub (.php) fallback.
360 */
361 private function htaccess_unavailable_reason() {
362 if ( ! $this->can_use_htaccess() ) {
363 $software = isset( $_SERVER['SERVER_SOFTWARE'] ) ? $_SERVER['SERVER_SOFTWARE'] : '';
364 if ( '' !== $software && false !== stripos( $software, 'nginx' ) ) {
365 return array( 'code' => 'nginx' );
366 }
367 if ( false === stripos( $software, 'apache' ) && false === stripos( $software, 'litespeed' ) ) {
368 return array( 'code' => 'server_software', 'detail' => $software );
369 }
370 return array( 'code' => 'not_writable' );
371 }
372 if ( ! SiteGuard_Htaccess::test_htaccess() ) {
373 return SiteGuard_Htaccess::$last_reason;
374 }
375 return array();
376 }
377
378 static function feature_off( $old_slug = null ) {
379 // Remove .htaccess feature
380 SiteGuard_Htaccess::clear_settings( self::$htaccess_mark );
381
382 // Remove stubs
383 $that = new self();
384 $that->remove_stub( $that->old_stub_abspath() );
385 if ( $that->slug() !== $that->old_slug() ) {
386 $that->remove_stub( $that->stub_abspath() );
387 }
388
389 // reset mode
390 global $siteguard_config;
391 $siteguard_config->set( 'renamelogin_stub', SITEGUARD_RENAME_MODE_HTACCESS );
392 $siteguard_config->update();
393
394 return true;
395 }
396
397 public function guard_disabled_entry() {
398 global $siteguard_config, $wp;
399
400 if ( '1' === $siteguard_config->get( 'renamelogin_enable' ) ) {
401 return;
402 }
403
404 $slug = $this->slug();
405 $req_path = isset( $_SERVER['REQUEST_URI'] ) ? (string) parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH ) : '';
406
407 $hit_slug = ( $req_path !== '' && rtrim( $req_path, '/' ) === '/' . $slug );
408 $hit_stub = ( $req_path !== '' && rtrim( $req_path, '/' ) === '/' . $this->stub_filename() );
409
410 if ( $hit_slug || $hit_stub ) {
411 if ( isset( $_REQUEST['action'] ) && $_REQUEST['action'] === 'logout' ) {
412 wp_safe_redirect( wp_logout_url() );
413 exit;
414 }
415 $this->set_404();
416 }
417 }
418
419 public function handle_siteguard_rescue() {
420 global $siteguard_config;
421
422 if ( '1' !== $siteguard_config->get( 'renamelogin_enable' ) ) {
423 return;
424 }
425
426 if ( '1' !== $siteguard_config->get( 'rescue_enable' ) ) {
427 return;
428 }
429 if ( ! isset( $_GET['siteguard_rescue'] ) || '1' !== (string) $_GET['siteguard_rescue'] ) {
430 return;
431 }
432
433 if ( $this->current_rescue_count() >= 3 ) {
434 $this->fixed_delay();
435 status_header( 429 );
436 nocache_headers();
437 $this->render_rescue_message( esc_html__( 'Request limit reached. Please try again later.', 'siteguard' ) );
438 exit;
439 }
440
441 if ( 'POST' === $_SERVER['REQUEST_METHOD'] ) {
442 $this->increment_rescue_counter();
443 $this->process_rescue_post();
444 exit;
445 }
446 $this->render_rescue_form();
447 exit;
448 }
449
450 private function rescue_rate_key( $ip ) {
451 return 'sg_rescue_count_' . md5( (string) $ip ); }
452 private function increment_rescue_counter() {
453 $ip = $this->get_ip();
454 $key = $this->rescue_rate_key( $ip );
455 $cnt = (int) get_transient( $key );
456 ++$cnt;
457 set_transient( $key, $cnt, HOUR_IN_SECONDS );
458 return $cnt;
459 }
460 private function current_rescue_count() {
461 $ip = $this->get_ip();
462 $key = $this->rescue_rate_key( $ip );
463 return (int) get_transient( $key );
464 }
465
466 private function render_rescue_form( $errors = array(), $email_value = '' ) {
467 $captcha = new SiteGuardReallySimpleCaptcha();
468 $language = get_bloginfo( 'language' );
469 ( strpos( $language, 'ja' ) === 0 ) ? $captcha->set_lang_mode( 'jp' ) : $captcha->set_lang_mode( 'en' );
470 $prefix = siteguard_rand();
471 $word = $captcha->generate_random_word();
472 $captcha->generate_image( $prefix, $word );
473
474 $action = esc_url( add_query_arg( 'siteguard_rescue', '1', site_url( '/' ) ) );
475 $imgsrc = esc_url( WP_CONTENT_URL . '/siteguard/' . $prefix . '.png' );
476
477 nocache_headers();
478 echo '<!DOCTYPE html><html><head><meta charset="' . esc_attr( get_bloginfo( 'charset' ) ) . '">';
479 echo '<meta name="robots" content="noindex,nofollow" />';
480 echo '<title>' . esc_html__( 'Login URL Rescue', 'siteguard' ) . '</title>';
481 echo '</head><body>';
482 echo '<h1>' . esc_html__( 'Login URL Rescue', 'siteguard' ) . '</h1>';
483
484 if ( ! empty( $errors ) ) {
485 echo '<div role="alert" style="color:#b00;">';
486 foreach ( (array) $errors as $e ) {
487 echo '<p>' . esc_html( $e ) . '</p>';
488 }
489 echo '</div>';
490 }
491
492 echo '<form method="post" action="' . $action . '">';
493 wp_nonce_field( 'siteguard_rescue', 'siteguard_rescue_nonce' );
494
495 echo '<p><label>' . esc_html__( 'Administrator email address', 'siteguard' ) . '<br />';
496 echo '<input type="email" name="siteguard_rescue_email" value="' . esc_attr( $email_value ) . '" required style="min-width:280px;" />';
497 echo '</label></p>';
498
499 echo '<p><img src="' . $imgsrc . '" alt="CAPTCHA" /></p>';
500 echo '<p><label>' . esc_html__( 'Enter the characters shown above', 'siteguard' ) . '<br />';
501 echo '<input type="text" name="siteguard_captcha" value="" size="10" required />';
502 echo '</label></p>';
503 echo '<input type="hidden" name="siteguard_captcha_prefix" value="' . esc_attr( $prefix ) . '" />';
504
505 echo '<p><button type="submit">' . esc_html__( 'Send email', 'siteguard' ) . '</button></p>';
506 echo '</form>';
507
508 echo '</body></html>';
509 }
510
511 private function uniform_delay( $start_ts_ms, $min_ms = 1800, $max_ms = 3200 ) {
512 $target = (int) wp_rand( $min_ms, $max_ms );
513 $elapsed = (int) ( ( microtime( true ) * 1000 ) - $start_ts_ms );
514 $remain = $target - $elapsed;
515 if ( $remain > 0 ) {
516 usleep( $remain * 1000 );
517 }
518 }
519 private function fixed_delay( $min_ms = 1800, $max_ms = 3200 ) {
520 $target = (int) wp_rand( $min_ms, $max_ms );
521 if ( $target > 0 ) {
522 usleep( $target * 1000 );
523 }
524 }
525
526 private function process_rescue_post() {
527 $start = (int) round( microtime( true ) * 1000 );
528
529 if ( ! isset( $_POST['siteguard_rescue_nonce'] ) || ! wp_verify_nonce( $_POST['siteguard_rescue_nonce'], 'siteguard_rescue' ) ) {
530 status_header( 400 );
531 $this->uniform_delay( $start );
532 $this->render_rescue_form( array( esc_html__( 'Invalid request.', 'siteguard' ) ), isset( $_POST['siteguard_rescue_email'] ) ? sanitize_email( $_POST['siteguard_rescue_email'] ) : '' );
533 return;
534 }
535
536 email_exists( 'dummy@example.com' );
537
538 $email = isset( $_POST['siteguard_rescue_email'] ) ? sanitize_email( $_POST['siteguard_rescue_email'] ) : '';
539 $cap = isset( $_POST['siteguard_captcha'] ) ? sanitize_text_field( $_POST['siteguard_captcha'] ) : '';
540 $pref = isset( $_POST['siteguard_captcha_prefix'] ) ? sanitize_text_field( $_POST['siteguard_captcha_prefix'] ) : '';
541
542 $errors = array();
543 if ( empty( $email ) || ! is_email( $email ) ) {
544 $errors[] = esc_html__( 'Please enter a valid email address.', 'siteguard' );
545 }
546
547 $captcha = new SiteGuardReallySimpleCaptcha();
548 $valid_captcha = ( $pref !== '' && $cap !== '' && $captcha->check( $pref, $cap, true ) );
549 if ( ! $valid_captcha ) {
550 $errors[] = esc_html__( 'Invalid CAPTCHA.', 'siteguard' );
551 }
552
553 if ( ! empty( $errors ) ) {
554 $this->uniform_delay( $start );
555 $this->render_rescue_form( $errors, $email );
556 return;
557 }
558
559 $user = get_user_by( 'email', $email );
560 if ( $user && user_can( $user, 'manage_options' ) ) {
561 $url = $this->get_login_url();
562 $subject = esc_html__( 'WordPress: Login URL Rescue', 'siteguard' );
563 $body = sprintf(
564 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' ),
565 $url
566 );
567 @wp_mail( $email, $subject, $body );
568 }
569
570 nocache_headers();
571 $this->uniform_delay( $start );
572 $this->render_rescue_message( esc_html__( 'An email has been sent if the address exists.', 'siteguard' ) );
573 }
574
575 private function render_rescue_message( $message ) {
576 echo '<!DOCTYPE html><html><head><meta charset="' . esc_attr( get_bloginfo( 'charset' ) ) . '">';
577 echo '<meta name="robots" content="noindex,nofollow" />';
578 echo '<title>' . esc_html__( 'Login URL Rescue', 'siteguard' ) . '</title>';
579 echo '</head><body>';
580 echo '<h1>' . esc_html__( 'Login URL Rescue', 'siteguard' ) . '</h1>';
581 echo '<p>' . esc_html( $message ) . '</p>';
582 echo '</body></html>';
583 }
584
585 private function get_login_url() {
586 global $siteguard_config;
587 if ( '0' === $siteguard_config->get( 'renamelogin_enable' ) ) {
588 return rtrim( site_url(), '/' ) . '/wp-login.php';
589 }
590 if ( $this->is_stub_mode() ) {
591 return $this->stub_url(); }
592 return rtrim( site_url(), '/' ) . '/' . $this->slug();
593 }
594
595 function set_404() {
596 global $wp_query;
597 status_header( 404 );
598 $wp_query->set_404();
599 if ( ( ( $template = get_404_template() ) || ( $template = get_index_template() ) )
600 && ( $template = apply_filters( 'template_include', $template ) ) ) {
601 include $template;
602 }
603 die;
604 }
605
606 function send_notify() {
607 $subject = esc_html__( 'WordPress: Login page URL changed', 'siteguard' );
608 $body = sprintf( esc_html__( "Please bookmark the new login URL.\n\n%s\n\n--\nSiteGuard WP Plugin", 'siteguard' ), $this->get_login_url() );
609
610 $user_query = new WP_User_Query( array( 'role' => 'Administrator' ) );
611 if ( ! empty( $user_query->results ) ) {
612 foreach ( $user_query->results as $user ) {
613 $user_email = $user->get( 'user_email' );
614 if ( true !== @wp_mail( $user_email, $subject, $body ) ) {
615 siteguard_error_log( 'Failed send mail. To:' . $user_email . ' Subject:' . esc_html( $subject ) );
616 }
617 }
618 }
619 }
620
621 function handler_stop_redirect( $scheme ) {
622 global $siteguard_config;
623 $redirect_enable = $siteguard_config->get( 'redirect_enable' );
624 if ( $redirect_enable == 1 ) {
625 if ( $user_id = wp_validate_auth_cookie( '', $scheme ) ) {
626 return $scheme;
627 }
628 wp_safe_redirect( home_url() );
629 exit;
630 }
631 }
632
633 function handler_plugins_loaded() {
634 if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
635 return;
636 }
637 $request = parse_url( $_SERVER['REQUEST_URI'] );
638 $denied_slugs = array( 'wp-register', 'wp-signup', 'wp-activate' );
639 $denied_slugs_to_regex = implode( '|', $denied_slugs );
640
641 $is_denied = false;
642 if ( is_array( $request ) && isset( $request['path'] ) ) {
643 $is_denied = preg_match( '#\/(' . $denied_slugs_to_regex . ')(\.php)?$#i', untrailingslashit( $request['path'] ) );
644 }
645 if ( $is_denied && ! is_admin() ) {
646 $this->denied_login = true;
647 // In stub mode, .htaccess rules are absent; block signup/activate directly.
648 if ( $this->is_stub_mode() ) {
649 status_header( 404 );
650 nocache_headers();
651 exit;
652 }
653 }
654 }
655
656 public function filter_logout_url( $logout_url, $redirect ) {
657 global $siteguard_config;
658 if ( '1' !== $siteguard_config->get( 'renamelogin_enable' ) ) {
659 return $logout_url;
660 }
661 $base = $this->get_login_url();
662
663 // Rebuild the logout URL from scratch to mirror WordPress core's
664 // wp_logout_url(): urlencode redirect_to, then wrap with wp_nonce_url().
665 //
666 // We deliberately do NOT parse_str( $logout_url ) and re-emit its query:
667 // the incoming $logout_url has already been passed through wp_nonce_url(),
668 // which esc_html()-encodes it (so separators are "&amp;"). parse_str() then
669 // splits on "&", mangling keys into "amp;redirect_to"/"amp;_wpnonce" and
670 // url-decoding the value back to its raw form. add_query_arg() re-emits that
671 // raw value, so an attacker-controlled redirect_to would break out of the
672 // href printed (without esc_url) by wp_nonce_ays( 'log-out' ) -> XSS.
673 //
674 // wp_logout_url() applies the same urlencode() contract that consumers rely
675 // on, and wp_nonce_url() re-adds the logout nonce, so building fresh keeps
676 // the URL correctly (and singly) encoded.
677 $args = array( 'action' => 'logout' );
678 if ( ! empty( $redirect ) ) {
679 $args['redirect_to'] = urlencode( $redirect );
680 }
681 return wp_nonce_url( add_query_arg( $args, $base ), 'log-out' );
682 }
683
684 public function rewrite_adminbar_logout( $wp_admin_bar ) {
685 if ( ! is_user_logged_in() ) {
686 return; }
687 if ( ! is_object( $wp_admin_bar ) ) {
688 return; }
689 foreach ( array( 'logout', 'log-out' ) as $id ) {
690 $node = $wp_admin_bar->get_node( $id );
691 if ( ! $node || empty( $node->href ) ) {
692 continue; }
693 $new = $this->filter_logout_url( $node->href, '' );
694 if ( $new && $new !== $node->href ) {
695 $node->href = $new;
696 $wp_admin_bar->add_node( $node );
697 }
698 }
699 }
700
701 public function maybe_notice_stub_failed() {
702 if ( ! current_user_can( 'manage_options' ) ) {
703 return; }
704 if ( get_transient( self::STUB_WRITE_FAIL_TRANSIENT ) ) {
705 echo '<div class="notice notice-warning"><p>';
706 echo esc_html__( 'SiteGuard: Could not create the required login file. Please check file permissions or contact your hosting provider.', 'siteguard' );
707 echo '</p></div>';
708 delete_transient( self::STUB_WRITE_FAIL_TRANSIENT );
709 }
710 }
711 }
712