PluginProbe
NETSENSAI Shield / trunk
NETSENSAI Shield vtrunk
1.6.1 trunk 1.1 1.2 1.3 1.4 1.4.1 1.4.2 1.4.3 1.4.5 1.4.6 1.4.7 1.4.8 1.4.9 1.6.0
netsensai-shield / includes / login_url_functions.php

login_url_functions.php in NETSENSAI Shield trunk, at includes/login_url_functions.php

562 lines 18.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // Exit if accessed directly.
3 if ( ! defined( 'ABSPATH' ) ) {
4 exit;
5 }
6
7 /**
8 * Zwraca bezpieczny, znormalizowany slug niestandardowej strony logowania (bez ukośników, lowercase).
9 * Fallbackuje do 'mysecurelogin', jeśli slug jest pusty lub zabroniony.
10 *
11 * @return string
12 */
13 function ns_shield_get_custom_slug() {
14 $raw = get_option( 'ns_shield_login_url', '/mysecurelogin/' );
15
16 // Normalizacja
17 $slug = is_string( $raw ) ? strtolower( trim( $raw ) ) : '';
18 $slug = untrailingslashit( $slug );
19 $slug = ltrim( $slug, '/' );
20
21 // Redukcja do bezpiecznych znaków (alfanum + myślniki)
22 // Pozostawiamy polskie litery do dalszej transliteracji WordPressa:
23 $slug = sanitize_title_with_dashes( $slug );
24
25 // Zabronione wartości i puste
26 $forbidden = array(
27 '', 'login', 'wp-login', 'login.php', 'wp-login.php', 'wp-admin'
28 );
29 if ( in_array( $slug, $forbidden, true ) ) {
30 $slug = 'mysecurelogin';
31 }
32
33 return $slug;
34 }
35
36 /**
37 * Check whether the custom-login feature is explicitly enabled.
38 *
39 * @return bool
40 */
41 function ns_shield_login_url_is_enabled() {
42 $value = get_option( 'ns_shield_login_url_enabled', false );
43
44 return true === $value || 1 === $value || '1' === $value;
45 }
46
47 /**
48 * Krótki helper do odpowiedzi 404 (spójny komunikat/headers).
49 */
50 function ns_shield_die_404() {
51 status_header( 404 );
52 nocache_headers();
53 wp_die(
54 esc_html__( '404 Not Found', 'netsensai-shield' ),
55 esc_html__( 'Not Found', 'netsensai-shield' ),
56 array( 'response' => 404 )
57 );
58 exit;
59 }
60
61 /**
62 * Failsafe: true, jeśli ż�
63 danie dotyczy naszej strony logowania
64 * (po query var LUB po samej ścieżce URL).
65 *
66 * @return bool
67 */
68 function ns_shield_is_custom_login_request() {
69 if ( ! ns_shield_login_url_is_enabled() ) {
70 return false;
71 }
72
73 if ( intval( get_query_var( 'ns_shield_custom_login' ) ) === 1 ) {
74 return true;
75 }
76
77 $request_uri = isset( $_SERVER['REQUEST_URI'] )
78 ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) )
79 : '';
80 $path = (string) wp_parse_url( $request_uri, PHP_URL_PATH );
81 $path = rtrim( $path, '/' );
82
83 $custom_path = rtrim( '/' . ns_shield_get_custom_slug(), '/' );
84
85 return ( $path === $custom_path );
86 }
87
88 /**
89 * Dodaje regułę rewrite dla niestandardowego adresu logowania.
90 */
91 function ns_shield_add_rewrite_rule() {
92 if ( ns_shield_login_url_is_enabled() ) {
93 $custom_slug = ns_shield_get_custom_slug();
94 if ( ! empty( $custom_slug ) ) {
95 add_rewrite_rule(
96 '^' . preg_quote( $custom_slug, '/' ) . '/?$',
97 'index.php?ns_shield_custom_login=1',
98 'top'
99 );
100 }
101 }
102 }
103 add_action( 'init', 'ns_shield_add_rewrite_rule', 1 );
104
105 /**
106 * Mark the custom-login rewrite rules for a single deferred flush.
107 *
108 * @return void
109 */
110 function ns_shield_mark_login_url_rewrite_flush_required() {
111 update_option( 'ns_shield_rewrite_flush_required', true, false );
112 }
113
114 /**
115 * Mark a first-time custom-login option for a deferred rewrite flush.
116 *
117 * @param string $option Option name.
118 * @param mixed $value New option value.
119 * @return void
120 */
121 function ns_shield_login_url_option_added( $option, $value ) {
122 ns_shield_mark_login_url_rewrite_flush_required();
123 }
124
125 /**
126 * Mark a changed custom-login option for a deferred rewrite flush.
127 *
128 * @param mixed $old_value Previous option value.
129 * @param mixed $value New option value.
130 * @param string $option Option name.
131 * @return void
132 */
133 function ns_shield_login_url_option_updated( $old_value, $value, $option ) {
134 if ( $old_value === $value ) {
135 return;
136 }
137
138 ns_shield_mark_login_url_rewrite_flush_required();
139 }
140
141 add_action( 'add_option_ns_shield_login_url', 'ns_shield_login_url_option_added', 10, 2 );
142 add_action( 'update_option_ns_shield_login_url', 'ns_shield_login_url_option_updated', 10, 3 );
143 add_action( 'add_option_ns_shield_login_url_enabled', 'ns_shield_login_url_option_added', 10, 2 );
144 add_action( 'update_option_ns_shield_login_url_enabled', 'ns_shield_login_url_option_updated', 10, 3 );
145
146 /**
147 * Flush one pending custom-login rewrite update after its rule is registered.
148 *
149 * @return void
150 */
151 function ns_shield_flush_pending_login_url_rewrite_rules() {
152 $flush_required = get_option( 'ns_shield_rewrite_flush_required', false );
153
154 if ( true !== $flush_required && 1 !== $flush_required && '1' !== $flush_required ) {
155 return;
156 }
157
158 // The current rule is registered on init:1. wp_loaded ensures WordPress can
159 // persist it instead of deferring the flush until after this flag is removed.
160 flush_rewrite_rules( false );
161
162 $rules = get_option( 'rewrite_rules', array() );
163 $target = 'index.php?ns_shield_custom_login=1';
164 $is_verified = false;
165
166 if ( ! is_array( $rules ) ) {
167 return;
168 }
169
170 if ( ns_shield_login_url_is_enabled() ) {
171 $expected_rule = '^' . preg_quote( ns_shield_get_custom_slug(), '/' ) . '/?$';
172 $is_verified = isset( $rules[ $expected_rule ] ) && $target === $rules[ $expected_rule ];
173 } else {
174 $is_verified = ! in_array( $target, $rules, true );
175 }
176
177 if ( $is_verified ) {
178 delete_option( 'ns_shield_rewrite_flush_required' );
179 }
180 }
181 add_action( 'wp_loaded', 'ns_shield_flush_pending_login_url_rewrite_rules', 0 );
182
183 /**
184 * Rejestruje zmienn�
185 query do rozpoznawania custom login.
186 */
187 function ns_shield_query_vars( $vars ) {
188 $vars[] = 'ns_shield_custom_login';
189 return $vars;
190 }
191 add_filter( 'query_vars', 'ns_shield_query_vars' );
192
193 /**
194 * Blokuje domyślne URL-e logowania i /wp-admin dla niezalogowanych,
195 * z uniknięciem fałszywych 404 na podstronach (Woo/motywy) oraz whitelist�
196 admin-ajax/admin-post.
197 */
198 function ns_shield_block_default_urls() {
199 // Jeśli to nasz custom login – nic nie blokuj tutaj.
200 if ( ns_shield_is_custom_login_request() ) {
201 return;
202 }
203
204 if ( ! ns_shield_login_url_is_enabled() ) {
205 return;
206 }
207
208 // Bież�
209 ca ścieżka ż�
210 dania (bez trailing slash)
211 $request_uri_full = isset( $_SERVER['REQUEST_URI'] )
212 ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) )
213 : '';
214 $parsed_url = wp_parse_url( $request_uri_full );
215 $request_path = isset( $parsed_url['path'] ) ? rtrim( $parsed_url['path'], '/' ) : '';
216
217 // Zestaw znanych domyślnych ścieżek logowania WordPress
218 $default_login_paths = array( '/wp-login.php','/wp_login.php','/login.php','/wp-login','/wp_login','/login' );
219 $is_default_login = in_array( $request_path, $default_login_paths, true );
220
221 // action=lostpassword/register – blokuj TYLKO na prawdziwym wp-login, nie globalnie (unikamy 404 na front-endzie)
222 $action_param = isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public login routing parameter; it does not change state.
223 if ( $action_param && in_array( $action_param, array( 'register', 'lostpassword' ), true ) ) {
224 if ( $is_default_login ) {
225 ns_shield_die_404();
226 }
227 // jeżeli to nie wp-login – przepuszczamy (np. WooCommerce/motyw).
228 }
229
230 // Obsługa prób wejścia na domyślne loginy
231 if ( $is_default_login ) {
232 // 1) wylogowanie → redirect na nasz slug z loggedout=true
233 $loggedout = isset( $_GET['loggedout'] ) ? sanitize_text_field( wp_unslash( $_GET['loggedout'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public login routing parameter; it does not change state.
234 if ( $loggedout === 'true' ) {
235 ns_shield_die_404_brand( 'loggedout_on_default_wp_login_strict', array( 'path_norm' => $path ) );
236 }
237
238 // 2) bezpieczne stany przekieruj na nasz slug z zachowaniem parametrów
239 $pass_qs_keys = array( 'checkemail', 'action', 'key', 'login', 'redirect_to', 'reauth', 'wp_lang' );
240 $qs = array();
241 foreach ( $pass_qs_keys as $k ) {
242 if ( isset( $_GET[ $k ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public login routing parameter; it does not change state.
243 $qs[ $k ] = sanitize_text_field( wp_unslash( $_GET[ $k ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public login routing parameter; it does not change state.
244 }
245 }
246 if ( ! empty( $qs ) ) {
247 wp_safe_redirect( add_query_arg( $qs, home_url( '/' . ns_shield_get_custom_slug() ) ) );
248 exit;
249 }
250
251 // 3) reszta przypadków → 404
252 ns_shield_die_404();
253 }
254
255 // /wp-admin dla niezalogowanych – pozwól na publiczne admin-ajax.php i admin-post.php (często używane przez front)
256 if ( ! is_user_logged_in() && 0 === strpos( $request_path, '/wp-admin' ) ) {
257 if ( preg_match( '#^/wp-admin/(admin-ajax\.php|admin-post\.php)$#', $request_path ) ) {
258 return; // whitelista
259 }
260 ns_shield_die_404();
261 }
262 }
263 add_action( 'wp_loaded', 'ns_shield_block_default_urls' );
264
265 /**
266 * Obsługuje wyświetlanie wp-login.php na naszym custom slugu.
267 */
268 function ns_shield_handle_custom_login_page() {
269 if ( ns_shield_is_custom_login_request() ) {
270 $custom_slug = ns_shield_get_custom_slug();
271 $forbidden_slugs = array( 'login', 'wp-login', 'login.php', 'wp-login.php' );
272
273 if ( in_array( $custom_slug, $forbidden_slugs, true ) ) {
274 ns_shield_die_404();
275 }
276
277 // Przekazujemy WP właściwy login i czyścimy ewentualne błędy.
278 global $user_login, $error;
279 $user_login = isset( $_GET['login'] ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public login routing parameter; it does not change state.
280 ? sanitize_user( wp_unslash( $_GET['login'] ) ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public login routing parameter; it does not change state.
281 : '';
282 $error = '';
283
284 require_once ABSPATH . 'wp-login.php';
285 exit;
286 }
287 }
288 add_action( 'template_redirect', 'ns_shield_handle_custom_login_page' );
289
290 /**
291 * Wył�
292 cz canonical redirect na naszym custom slugu,
293 * żeby WP nie zjadał parametrów (login/key itp.).
294 */
295 function ns_shield_disable_canonical_redirect( $redirect_url, $requested_url ) {
296 if ( ns_shield_is_custom_login_request() ) {
297 return false;
298 }
299 return $redirect_url;
300 }
301 add_filter( 'redirect_canonical', 'ns_shield_disable_canonical_redirect', 10, 2 );
302
303 /**
304 * Zwraca URL logowania przepięty na nasz custom slug (ZACHOWUJE istniej�
305 ce query – np. checkemail=confirm).
306 *
307 * @param string $login_url
308 * @param string $redirect
309 * @return string
310 */
311 function ns_shield_custom_login_url( $login_url, $redirect ) {
312 if ( ! ns_shield_login_url_is_enabled() ) {
313 return $login_url;
314 }
315
316 // Wyci�
317 gnij istniej�
318 ce parametry z podanego $login_url (np. ?checkemail=confirm)
319 $parts = wp_parse_url( $login_url );
320 $args = array();
321 if ( ! empty( $parts['query'] ) ) {
322 wp_parse_str( $parts['query'], $args );
323 }
324
325 // Jeśli WP podał redirect_to w argumencie filtra, nadpisz/uzupełnij
326 if ( ! empty( $redirect ) ) {
327 $args['redirect_to'] = $redirect;
328 }
329
330 // Złóż URL na customowym slugu z oryginalnymi parametrami
331 $base = home_url( '/' . ns_shield_get_custom_slug() );
332 return ! empty( $args ) ? add_query_arg( $args, $base ) : $base;
333 }
334 add_filter( 'login_url', 'ns_shield_custom_login_url', 10, 2 );
335
336 /**
337 * Przepina link "Nie pamiętam hasła" na nasz custom slug.
338 *
339 * @param string $lost_url
340 * @param string $redirect
341 * @return string
342 */
343 function ns_shield_custom_lostpassword_url( $lost_url, $redirect ) {
344 if ( ! ns_shield_login_url_is_enabled() ) {
345 return $lost_url;
346 }
347 $url = home_url( '/' . ns_shield_get_custom_slug() );
348 $url = add_query_arg( 'action', 'lostpassword', $url );
349 if ( ! empty( $redirect ) ) {
350 $url = add_query_arg( 'redirect_to', rawurlencode( $redirect ), $url );
351 }
352 return $url;
353 }
354 add_filter( 'lostpassword_url', 'ns_shield_custom_lostpassword_url', 10, 2 );
355
356 /**
357 * (Opcjonalnie) przepina URL rejestracji, jeśli gdzieś jest używany.
358 *
359 * @param string $register_url
360 * @return string
361 */
362 function ns_shield_custom_register_url( $register_url ) {
363 if ( ! ns_shield_login_url_is_enabled() ) {
364 return $register_url;
365 }
366 $url = home_url( '/' . ns_shield_get_custom_slug() );
367 return add_query_arg( 'action', 'register', $url );
368 }
369 add_filter( 'register_url', 'ns_shield_custom_register_url', 10 );
370
371 /**
372 * Modyfikuje URL-e wp-login.php generowane przez site_url() i network_site_url().
373 * Uwaga: podpis funkcji ma parametry opcjonalne, by działał z oboma filtrami.
374 *
375 * @param string $url
376 * @param string $path
377 * @param string|null $orig_scheme
378 * @param int|null $blog_id
379 * @return string
380 */
381 function ns_shield_override_wp_login_url( $url, $path = '', $orig_scheme = null, $blog_id = null ) {
382 if (
383 ns_shield_login_url_is_enabled()
384 && is_string( $path )
385 && preg_match( '#^wp-login(\.php)?#', $path )
386 ) {
387 $query = '';
388 if ( false !== ( $pos = strpos( $path, '?' ) ) ) {
389 $query = substr( $path, $pos ); // zaczyna się od "?"
390 }
391 return home_url( '/' . ns_shield_get_custom_slug() . $query );
392 }
393 return $url;
394 }
395 add_filter( 'site_url', 'ns_shield_override_wp_login_url', 10, 4 );
396 add_filter( 'network_site_url', 'ns_shield_override_wp_login_url', 10, 3 ); // Multisite/Network
397
398 /**
399 * Podmienia w mailu każdy wp-login.php?... na /<custom-slug>?...
400 */
401 function ns_shield_custom_retrieve_password_message( $message, $key, $user_login, $user_data ) {
402 if ( ! ns_shield_login_url_is_enabled() ) {
403 return $message;
404 }
405
406 $slug = ns_shield_get_custom_slug();
407
408 $message = preg_replace_callback(
409 '#https?://[^/]+/wp-login\.php(\?[^\\s]+)#',
410 function( $m ) use ( $slug ) {
411 return home_url( '/' . $slug . $m[1] );
412 },
413 $message
414 );
415
416 return $message;
417 }
418 add_filter( 'retrieve_password_message', 'ns_shield_custom_retrieve_password_message', 10, 4 );
419
420 /**
421 * Po udanym logowaniu kieruje do /wp-admin.
422 */
423 function ns_shield_custom_login_redirect( $redirect_to, $requested_redirect_to, $user ) {
424 if ( ns_shield_login_url_is_enabled() && ! is_wp_error( $user ) ) {
425 return admin_url();
426 }
427 return $redirect_to;
428 }
429 add_filter( 'login_redirect', 'ns_shield_custom_login_redirect', 10, 3 );
430
431 /**
432 * Po wylogowaniu przekierowuje na nasz custom slug z loggedout=true.
433 */
434 function ns_shield_custom_logout_url( $redirect_to, $requested_redirect_to, $user ) {
435 if ( ns_shield_login_url_is_enabled() ) {
436 return home_url( '/' . ns_shield_get_custom_slug() . '?loggedout=true' );
437 }
438 return home_url( '/wp-login.php?loggedout=true' );
439 }
440 add_filter( 'logout_redirect', 'ns_shield_custom_logout_url', 10, 3 );
441
442 /**
443 * NOWE: przekierowanie wp-login.php?checkemail=... na custom slug.
444 * Uruchamiane tylko na stronie logowania (login_init), gdy WP i pluggable s�
445 już gotowe.
446 */
447 function ns_shield_redirect_checkemail_on_login() {
448 if ( ! ns_shield_login_url_is_enabled() ) {
449 return;
450 }
451 // nie przekierowuj, jeśli już jesteśmy na customowym slugu
452 if ( ns_shield_is_custom_login_request() ) {
453 return;
454 }
455 // tylko gdy przychodzi query checkemail=...
456 if ( isset( $_GET['checkemail'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
457 $val = sanitize_text_field( wp_unslash( $_GET['checkemail'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
458 $slug = ns_shield_get_custom_slug();
459 $url = add_query_arg( 'checkemail', $val, home_url( '/' . $slug ) );
460 wp_safe_redirect( $url );
461 exit;
462 }
463 }
464 add_action( 'login_init', 'ns_shield_redirect_checkemail_on_login', 0 );
465
466 /**
467 * Wył�
468 cza autocomplete w formularzu logowania.
469 */
470 function ns_shield_disable_autocomplete() {
471 echo '<script>
472 document.addEventListener("DOMContentLoaded", function() {
473 var loginForm = document.getElementById("loginform");
474 if (loginForm) {
475 loginForm.setAttribute("autocomplete", "off");
476 }
477 });
478 </script>';
479 }
480 add_action( 'login_form', 'ns_shield_disable_autocomplete' );
481
482 /**
483 * Dodaje nonce do formularza logowania.
484 */
485 function ns_shield_add_login_nonce() {
486 wp_nonce_field( 'login_nonce' );
487 }
488 add_action( 'login_form', 'ns_shield_add_login_nonce' );
489
490 /**
491 * Wymusza action dla formularzy login/reset na nasz slug
492 * z pełnym zachowaniem query string (action, key, login, wp_lang itd.).
493 */
494 function ns_shield_login_form_action( $action ) {
495 if ( ! ns_shield_login_url_is_enabled() ) {
496 return $action;
497 }
498 $slug = ns_shield_get_custom_slug();
499 $query = '';
500 if ( ! empty( $_SERVER['QUERY_STRING'] ) ) {
501 $query = '?' . sanitize_text_field( wp_unslash( $_SERVER['QUERY_STRING'] ) );
502 }
503 return home_url( '/' . $slug . $query );
504 }
505 add_filter( 'login_form_action', 'ns_shield_login_form_action', 10, 1 );
506
507 /**
508 * Ustaw nagłówki anty-cache wył�
509 cznie na naszym slugu (stabilność przy CDN/WAF).
510 */
511 add_action( 'send_headers', function () {
512 if ( ns_shield_is_custom_login_request() ) {
513 header( 'Cache-Control: no-store, no-cache, must-revalidate, max-age=0' );
514 header( 'Pragma: no-cache' );
515 }
516 });
517
518 /** Załadowanie strażnika URL */
519 require_once __DIR__ . '/integrations/login-url-guard.php';
520
521 /**
522 * Render przeł�
523 cznika i pola slug w ustawieniach wtyczki.
524 */
525 function ns_shield_change_login_url() {
526 $status = get_option( 'ns_shield_login_url_enabled', false );
527 $login_url = get_option( 'ns_shield_login_url', '/mysecurelogin/' );
528 ?>
529 <div class="change-url-container">
530 <label class="switch">
531 <input type="checkbox"
532 name="ns_shield_login_url_enabled"
533 id="ns_shield_login_url_enabled"
534 value="1" <?php checked( 1, $status, true ); ?>>
535 <span class="slider round"></span>
536 </label>
537 <div class="tooltip" id="tooltip-change-login-url">
538 <?php echo esc_html( ns_shield_get_login_url_tooltip() ); ?>
539 </div>
540 <input type="text"
541 name="ns_shield_login_url"
542 id="ns_shield_login_url"
543 value="<?php echo esc_attr( $login_url ); ?>"
544 placeholder="<?php echo esc_attr__( 'Enter new login URL', 'netsensai-shield' ); ?>"
545 class="login-url-input"
546 style="display:<?php echo $status ? 'block' : 'none'; ?>;" />
547 </div>
548 <script>
549 document.addEventListener("DOMContentLoaded", function() {
550 var checkbox = document.getElementById("ns_shield_login_url_enabled"),
551 field = document.getElementById("ns_shield_login_url");
552 if (checkbox && field) {
553 field.style.display = checkbox.checked ? "block" : "none";
554 checkbox.addEventListener("change", function() {
555 field.style.display = this.checked ? "block" : "none";
556 });
557 }
558 });
559 </script>
560 <?php
561 }
562