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 "&"). 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 |