PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.0.1
Defender Security – Malware Scanner, Login Security & Firewall v6.0.1
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
defender-security / src / controller / class-mask-login.php

class-mask-login.php in Defender Security – Malware Scanner, Login Security & Firewall 6.0.1, at src/controller/class-mask-login.php

1,206 lines 34.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles mask login.
4 *
5 * @package WP_Defender\Controller
6 */
7
8 namespace WP_Defender\Controller;
9
10 use WP_User;
11 use WP_Query;
12 use WP_Admin_Bar;
13 use WP_Recovery_Mode;
14 use WP_Defender\Event;
15 use Calotes\Helper\HTTP;
16 use Calotes\Helper\Route;
17 use WP_Defender\Traits\IO;
18 use Calotes\Component\Request;
19 use Calotes\Component\Response;
20 use WP_Defender\Component\Two_Fa;
21 use WP_Defender\Traits\Permission;
22 use WP_Defender\Component\Blacklist_Lockout;
23 use WP_Defender\Component\Config\Config_Hub_Helper;
24 use WP_Defender\Component\Security_Tweaks\Servers\Server;
25 use WP_Defender\Component\Mask_Login as Component_Mask_Login;
26 use WP_Defender\Model\Setting\Mask_Login as Model_Mask_Login;
27
28 /**
29 * Handles mask login.
30 */
31 class Mask_Login extends Event {
32
33 use IO;
34 use Permission;
35
36 /**
37 * The model for handling the data.
38 *
39 * @var Model_Mask_Login
40 */
41 protected $model;
42
43 /**
44 * Service for handling logic.
45 *
46 * @var Component_Mask_Login
47 */
48 protected $service;
49
50 /**
51 * The compatibility notices.
52 *
53 * @var array
54 */
55 protected $compatibility_notices = array();
56
57 /**
58 * Initializes the model and service, registers routes, and sets up scheduled events if the model is active.
59 */
60 public function __construct() {
61 // Internal cache, so we don't need to query many times.
62 $this->model = wd_di()->get( Model_Mask_Login::class );
63 $this->service = wd_di()->get( Component_Mask_Login::class );
64 $this->register_routes();
65
66 if ( $this->get_model()->is_active() ) {
67 $auth_component = wd_di()->get( Two_Fa::class );
68 require_once ABSPATH . 'wp-admin/includes/plugin.php';
69 $is_jetpack_sso = $auth_component->is_jetpack_sso();
70 $is_tml = $auth_component->is_tml();
71 if ( $is_jetpack_sso || $is_tml ) {
72 if ( $is_jetpack_sso ) {
73 $this->compatibility_notices[] = esc_html__(
74 'We`ve detected a conflict with Jetpack`s Wordpress.com Log In feature. Please disable it and return to this page to continue setup.',
75 'defender-security'
76 );
77 }
78 if ( $is_tml ) {
79 $this->compatibility_notices[] = esc_html__(
80 'We`ve detected a conflict with Theme my login. Please disable it and return to this page to continue setup.',
81 'defender-security'
82 );
83 }
84
85 return;
86 }
87 // Monitor wp-admin, wp-login.php.
88 add_filter( 'wp_redirect', array( $this, 'filter_wp_redirect' ), 10 );
89 // Filter site_url & network_site_url so people won't get block screen.
90 add_filter( 'site_url', array( $this, 'filter_site_url' ), 100 );
91 add_filter( 'network_site_url', array( $this, 'filter_site_url' ), 100 );
92 // For prevent admin redirect.
93 remove_action( 'template_redirect', 'wp_redirect_admin_locations' );
94 // If Pro site is activated and user email is not defined, we need to update the email to match the new login URL.
95 add_filter( 'update_welcome_email', array( $this, 'update_welcome_email_prosite_case' ), 10, 6 );
96 add_filter( 'lostpassword_redirect', array( $this, 'change_lostpassword_redirect' ), 10 );
97 // Log links in email.
98 add_filter( 'report_email_logs_link', array( $this, 'update_report_logs_link' ), 10, 2 );
99 if ( class_exists( 'bbPress' ) ) {
100 add_filter( 'bbp_redirect_login', array( $this, 'make_sure_wpadmin_after_login' ), 10, 3 );
101 }
102
103 if ( 'flywheel' === Server::get_current_server() ) {
104 if ( ! is_user_logged_in() ) {
105 add_action( 'login_form_rp', array( $this, 'handle_password_reset' ) );
106 add_action( 'login_form_resetpass', array( $this, 'handle_password_reset' ) );
107 }
108 add_filter( 'retrieve_password_message', array( $this, 'flywheel_change_password_message' ), 10, 4 );
109 }
110
111 global $pagenow;
112 if ( is_network_admin() && 'sites.php' === $pagenow ) {
113 // Add 4th parameter $scheme when the plugin will support WP at least v5.8.
114 add_filter( 'admin_url', array( $this, 'change_subsites_admin_url' ), 10, 3 );
115 }
116
117 if ( is_admin() && 'my-sites.php' === $pagenow ) {
118 add_filter( 'myblogs_blog_actions', array( $this, 'update_myblogs_blog_actions' ), 10, 2 );
119 }
120
121 if ( is_multisite() ) {
122 add_action( 'admin_bar_menu', array( $this, 'update_admin_bar_menu' ), 100 );
123 }
124
125 if ( $this->service->is_set_locale( $this->model->mask_url ) ) {
126 add_action( 'init', array( $this, 'set_locale' ) );
127 }
128 // Never catch if from cli.
129 if ( ! defender_is_wp_cli() ) {
130 $this->before_mask_login_handle();
131 }
132 add_action( 'init', array( $this, 'handle_login_request' ), 99 );
133 }
134 }
135
136 /**
137 * Adjusts the redirect URL after login to ensure it redirects to the admin dashboard if the current URL is the
138 * home URL.
139 *
140 * @param string $url The current redirect URL.
141 * @param string $raw_url The raw redirect URL.
142 * @param object $user The user object.
143 *
144 * @return string The adjusted redirect URL after applying filters.
145 */
146 public function make_sure_wpadmin_after_login( string $url, string $raw_url, object $user ): string {
147 if ( home_url() === $url ) {
148 $url = admin_url();
149 }
150
151 return apply_filters( 'defender_redirect_login', $url, $raw_url, $user );
152 }
153
154 /**
155 * Show login page.
156 *
157 * @return void
158 */
159 public function show_login_page(): void {
160 global $error, $interim_login, $action, $user_login, $user, $redirect_to;
161 // Simulate the environment as a "login page".
162 $GLOBALS['pagenow'] = 'wp-login.php'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
163 if ( $this->service->is_recovery_mode() ) {
164 ( new WP_Recovery_Mode() )->initialize();
165 }
166 require_once ABSPATH . 'wp-login.php';
167 die;
168 }
169
170 /**
171 * Before Mask Login handling.
172 *
173 * @return void
174 * @since 2.8.0
175 */
176 public function before_mask_login_handle(): void {
177 // Some plugins for Cron actions clear HTTP_HOST-param.
178 $host = defender_get_data_from_request( 'HTTP_HOST', 's' );
179 if ( ! isset( $host['HTTP_HOST'] ) ) {
180 $host = '';
181 }
182 $current_url = set_url_scheme( 'http://' . $host . defender_get_data_from_request( 'REQUEST_URI', 's' ) );
183 $login_url = $this->get_model()->get_new_login_url( $this->get_site_url() );
184
185 if (
186 ! is_user_logged_in() &&
187 '' !== $login_url &&
188 ! $this->service->is_land_on_masked_url( $this->model->mask_url ) &&
189 /**
190 * Filter to redirect current URL to Mask Login URL.
191 *
192 * @param bool $allowed Should we redirect to Mask Login URL?.
193 * @param string $current_url Current URL to check.
194 *
195 * @since 2.8.0
196 */
197 true === apply_filters( 'wpdef_maybe_redirect_to_mask_login_url', false, $current_url )
198 ) {
199 $modified_url = add_query_arg( 'redirect_to', rawurlencode( $current_url ), $login_url );
200
201 wp_safe_redirect( $modified_url );
202 die();
203 }
204 }
205
206 /**
207 * Protect unauthorized login redirect.
208 *
209 * @param string $location The redirect URL.
210 *
211 * @return string
212 */
213 public function protect_unauthorized_login_redirect( string $location ) {
214 // Make sure that wp-login.php is never used in site URLs or redirects.
215 if ( ! $this->service->is_login_url() ) {
216 $redirect_path = trim( (string) wp_parse_url( $location, PHP_URL_PATH ), '/' );
217 if ( $redirect_path === $this->get_model()->mask_url ) {
218 $this->maybe_lock();
219 }
220 }
221
222 return $location;
223 }
224
225 /**
226 * If it is request to wp-admin, wp-login.php and similar slugs, we block for sure. If no, then follow the wp flow.
227 *
228 * @return void
229 */
230 public function handle_login_request() {
231 // If the IP is BLC whitelisted, then skip processing URLs other than the Masked Login URL.
232 if ( wd_di()->get( Blacklist_Lockout::class )->is_blc_ip_whitelisted() ) {
233 if ( $this->service->is_land_on_masked_url( $this->model->mask_url ) ) {
234 $this->show_login_page();
235 }
236
237 return;
238 }
239
240 // Need to check if the current request is for signup, login.
241 // If it is not the slug, then we redirect to the 404 redirect, or 403 wp die.
242 $requested_path = $this->service->get_request_path();
243 $requested_path_without_slash = ltrim( $requested_path, '/' );
244 if ( ! $requested_path_without_slash && '' !== get_option( 'permalink_structure', '' ) ) {
245 return;
246 } else {
247 $params = wp_parse_args( defender_get_data_from_request( 'QUERY_STRING', 's' ), array() );
248 if ( isset( $params[ $this->model->mask_url ] ) ) {
249 $this->show_login_page();
250 }
251 }
252
253 if ( '/' . ltrim( $this->get_model()->mask_url, '/' ) === $requested_path ) {
254 // We need to redirect this one to wp-login and open it.
255 $this->show_login_page();
256 return;
257 }
258 /**
259 * Allowed if:
260 * it's AJAX,
261 * the user is logged in,
262 * it's an admin post request.
263 */
264 if (
265 defined( 'DOING_AJAX' )
266 || is_user_logged_in()
267 || $this->is_allowed_path( $requested_path_without_slash )
268 ) {
269 // Do nothing.
270 return;
271 }
272
273 // If user is not logged in but login cookie is set.
274 $cookie = defender_get_data_from_request( null, 'c' );
275 if ( isset( $cookie[ LOGGED_IN_COOKIE ] ) && ! is_user_logged_in() ) {
276 $user_id = wp_validate_auth_cookie( $cookie[ LOGGED_IN_COOKIE ], 'logged_in' );
277
278 if ( $user_id ) {
279 // Cookie is valid so login the user.
280 wp_set_current_user( $user_id );
281
282 // Return from here because of valid user found.
283 return;
284 }
285 }
286
287 $ticket = HTTP::get( 'ticket', false );
288 // Todo: need if express_tickets are not saved?
289 if ( false !== $ticket && $this->service->redeem_ticket( $ticket ) ) {
290 // Allow to pass.
291 return;
292 }
293
294 // If current is same then we show the login screen.
295 if ( $this->service->is_land_on_masked_url( $this->model->mask_url ) ) {
296 $this->show_login_page();
297 return;
298 }
299
300 // If it's the verification link to change Network Admin Email.
301 $is_multisite = is_multisite();
302 $haystack = wp_parse_url( $requested_path, PHP_URL_QUERY );
303 if (
304 $is_multisite && is_string( $haystack )
305 && false !== strpos( $haystack, 'network_admin_hash' )
306 ) {
307 $logs_url = add_query_arg(
308 'redirect_to',
309 rawurlencode( $requested_path ),
310 $this->get_model()->get_new_login_url()
311 );
312 wp_safe_redirect( $logs_url );
313 die;
314 }
315 /**
316 * Block if it's:
317 * 1) no MU but there is an attempt to load the 'wp-signup.php' page,
318 * 2) from the list of forbidden slugs.
319 */
320 if (
321 ( ! $is_multisite && 'wp-signup.php' === $requested_path_without_slash )
322 || $this->service->is_on_login_page( $requested_path_without_slash )
323 ) {
324 // If they are here and the flow getting here, then just lock.
325 $this->maybe_lock();
326 return;
327 }
328 }
329
330 /**
331 * Save settings.
332 *
333 * @param Request $request The request object containing new settings data.
334 *
335 * @return Response
336 * @defender_route
337 */
338 public function save_settings( Request $request ): Response {
339 $data = $request->get_data_by_model( $this->model );
340 $this->model->import( $data );
341 if ( $this->model->validate() ) {
342 $this->model->save();
343 Config_Hub_Helper::set_clear_active_flag();
344
345 return new Response(
346 true,
347 array_merge(
348 array(
349 'message' => esc_html__( 'Your settings have been updated.', 'defender-security' ),
350 'auto_close' => true,
351 ),
352 $this->data_frontend()
353 )
354 );
355 }
356 $data_frontend = $this->data_frontend();
357 $result = array( 'message' => $this->model->get_formatted_errors() );
358 // Don't hide the error notice if the module is not activated.
359 if ( ! $data_frontend['is_active'] ) {
360 $result['auto_close'] = false;
361 }
362
363 return new Response(
364 false,
365 // Merge stored data to avoid errors.
366 array_merge( $result, $data_frontend )
367 );
368 }
369
370 /**
371 * Filter every admin/login URL to return the masked one.
372 *
373 * @param string $site_url The complete URL.
374 *
375 * @return string
376 */
377 public function filter_site_url( string $site_url ) {
378 return $this->alter_url( $site_url, 'site_url' );
379 }
380
381 /**
382 * Filters the WordPress redirect URL to return the masked one.
383 *
384 * @param string $location The complete URL.
385 *
386 * @return string The masked URL.
387 */
388 public function filter_wp_redirect( string $location ) {
389 return $this->alter_url( $location, 'wp_safe_redirect' );
390 }
391
392 /**
393 * Alters the URL based on the source and whether the user is logged in or not.
394 *
395 * @param string $current_url The current URL.
396 * @param string $source The source of the URL.
397 *
398 * @return string The altered URL.
399 */
400 public function alter_url( string $current_url, string $source ): string {
401 if ( is_user_logged_in() && false === stripos( $current_url, 'wp-login.php' ) ) {
402 // Do nothing.
403 return $current_url;
404 }
405
406 if ( 'wp_safe_redirect' === $source && ! is_user_logged_in() ) {
407 $parsed_url = wp_parse_url( $current_url );
408
409 $parsed_query = array();
410 if ( isset( $parsed_url['query'] ) ) {
411 wp_parse_str( $parsed_url['query'], $parsed_query );
412 }
413
414 if (
415 isset( $parsed_url['path'] ) && 'wp-login.php' === trim( $parsed_url['path'], '/' ) &&
416 isset( $parsed_query['checkemail'] ) && 'registered' === $parsed_query['checkemail']
417 ) {
418 return $this->model->get_mask_url() . $this->get_permalink_separator() . build_query( $parsed_query );
419 }
420 }
421
422 if (
423 'wp_safe_redirect' === $source &&
424 ! is_user_logged_in() &&
425 ! wd_di()->get( Blacklist_Lockout::class )->is_blc_ip_whitelisted()
426 ) {
427 return $this->protect_unauthorized_login_redirect( $current_url );
428 }
429
430 if ( false !== stripos( $current_url, 'wp-login.php' ) ) {
431 // This is URL go to old wp-login.php.
432 $query = wp_parse_url( $current_url, PHP_URL_QUERY );
433 $query = $query ?? '';
434 parse_str( $query, $params );
435
436 if ( isset( $params['login'] ) ) {
437 $params['login'] = rawurlencode( $params['login'] );
438 }
439
440 return add_query_arg( $params, $this->get_model()->get_new_login_url( $this->get_site_url() ) );
441 } else {
442 // This case when admin maps a domain into subsite, we need to update the new domain/masked-login into the list.
443 if ( ! function_exists( 'get_current_screen' ) ) {
444 require_once ABSPATH . 'wp-admin/includes/screen.php';
445 }
446 $screen = get_current_screen();
447
448 if ( ! is_object( $screen ) ) {
449 return $current_url;
450 }
451 if ( 'sites-network' === $screen->id ) {
452 // Case URLs inside sites list, need to check those with custom domain cause when it's redirect, it will require re-login.
453 $requested_path = $this->service->get_request_path( $current_url );
454 if ( '/wp-admin' === $requested_path ) {
455 $current_domain = defender_get_data_from_request( 'HTTP_HOST', 's' );
456 $sub_domain = wp_parse_url( $current_url, PHP_URL_HOST );
457 if ( is_string( $sub_domain ) && '' !== trim( $sub_domain ) && false === stripos( $sub_domain, $current_domain ) ) {
458 return $this->get_model()->get_new_login_url( $sub_domain );
459 }
460 }
461 }
462 /**
463 * Todo:
464 * add other condition ('my-sites' === $screen->id),
465 * create OTP key and link with the 'otp' arg inside
466 */
467 }
468
469 return $current_url;
470 }
471
472 /**
473 * Show the wp die screen for lockout, or redirect to defined URL.
474 *
475 * @return void
476 */
477 public function maybe_lock(): void {
478 if ( 'custom_url' === $this->get_model()->redirect_traffic && strlen( $this->get_model()->redirect_traffic_url ) ) {
479 if ( 'url' === $this->get_model()->is_url_or_slug() ) {
480 $redirect_url = wp_sanitize_redirect( $this->get_model()->redirect_traffic_url );
481 $lp = wp_parse_url( $redirect_url );
482
483 // Give up if malformed URL.
484 if ( false === $lp ) {
485 $this->show_forbidden_screen();
486 }
487 // If the URL is without scheme, e.g. example.com, then add 'http' protocol at the beginning of the URL.
488 if ( ! isset( $lp['scheme'] ) && isset( $lp['path'] ) ) {
489 $redirect_url = 'http://' . untrailingslashit( $redirect_url );
490 }
491 $parsed_url = wp_parse_url( $redirect_url, PHP_URL_HOST );
492 if ( is_string( $parsed_url ) ) {
493 /**
494 * Filters the list of allowed hosts to redirect to.
495 *
496 * @param string[] $hosts An array of allowed host names.
497 *
498 * @return string[] An array of allowed host names.
499 */
500 add_filter(
501 'allowed_redirect_hosts',
502 function ( array $hosts ) use ( $parsed_url ) {
503 $hosts[] = $parsed_url;
504
505 return $hosts;
506 },
507 );
508 }
509
510 wp_safe_redirect( $redirect_url );
511 } else {
512 wp_safe_redirect( home_url( $this->get_model()->redirect_traffic_url ) );
513 }
514 die;
515 }
516
517 if ( 'wp_page' === $this->get_model()->redirect_traffic ) {
518 $id = $this->get_model()->redirect_traffic_page_id;
519 $post = get_post( $id );
520 if ( is_object( $post ) ) {
521 wp_safe_redirect( get_permalink( $post ) );
522 exit;
523 }
524 }
525
526 // Handle user profile email change request.
527 $this->handle_email_change_request();
528
529 $this->show_forbidden_screen();
530 }
531
532 /**
533 * Show the forbidden screen.
534 */
535 public function show_forbidden_screen(): void {
536 wp_die(
537 esc_html__( 'This feature is temporarily forbidden for security reasons. Try logging in again.', 'defender-security' ),
538 esc_html__( 'Forbidden', 'defender-security' ),
539 array(
540 'response' => 403,
541 )
542 );
543 }
544
545 /**
546 * Safe way to get cached model.
547 *
548 * @return Model_Mask_Login
549 */
550 private function get_model() {
551 if ( is_object( $this->model ) ) {
552 return $this->model;
553 }
554
555 return new Model_Mask_Login();
556 }
557
558 /**
559 * Redirects the user to the admin URL if the given URL is the home URL.
560 *
561 * @param string $url The URL to be redirected.
562 * @param string $raw_url The raw URL.
563 * @param object $user The user object.
564 *
565 * @return string The filtered URL after applying the 'defender_redirect_login' filter.
566 */
567 public function redirect_login( $url, $raw_url, $user ) {
568 if ( home_url() === $url ) {
569 $url = admin_url();
570 }
571
572 return apply_filters( 'defender_redirect_login', $url, $raw_url, $user );
573 }
574
575 /**
576 * Retrieves the site URL based on the provided parameters.
577 * This function retrieves the site URL for the current or specified blog. If the blog ID is empty or not in a
578 * multisite environment, it retrieves the site URL from the 'siteurl' option. Otherwise, it switches to the
579 * specified blog, retrieves the site URL, and then restores the current blog.
580 *
581 * @param int|null $blog_id The blog ID in a multisite environment. Default is null.
582 * @param string $path Additional path to append to the site URL. Default is an empty string.
583 * @param string|null $scheme The scheme to use. Default is null.
584 *
585 * @return string The site URL with the specified parameters.
586 */
587 private function get_site_url( $blog_id = null, $path = '', $scheme = null ) {
588 if ( ! is_int( $blog_id ) || 0 === $blog_id || ! is_multisite() ) {
589 $url = get_option( 'siteurl' );
590 } else {
591 switch_to_blog( $blog_id );
592 $url = get_option( 'siteurl' );
593 restore_current_blog();
594 }
595
596 $url = set_url_scheme( $url, $scheme );
597
598 if ( $path && is_string( $path ) ) {
599 $url .= '/' . ltrim( $path, '/' );
600 }
601
602 /**
603 * Filters the list of plugins for which 'site_url' filter should be skipped.
604 *
605 * @param array $plugins A list of plugin file paths relative to the plugin's directory.
606 *
607 * @since 4.1.0
608 */
609 $plugins = apply_filters(
610 'wd_mask_login_skip_site_url_filter',
611 array( 'wp-ultimo/wp-ultimo.php' )
612 );
613 $skip_site_url_filter = false;
614 if ( is_array( $plugins ) ) {
615 foreach ( $plugins as $plugin ) {
616 if ( is_plugin_active( $plugin ) || is_plugin_active_for_network( $plugin ) ) {
617 $skip_site_url_filter = true;
618 break;
619 }
620 }
621 }
622
623 if ( $skip_site_url_filter ) {
624 return apply_filters( 'site_url', $url, $path, $scheme, $blog_id );
625 } else {
626 return $url;
627 }
628 }
629
630 /**
631 * Removes settings for all submodules.
632 */
633 public function remove_settings() {
634 }
635
636 /**
637 * Delete all the data & the cache.
638 */
639 public function remove_data() {
640 }
641
642 /**
643 * Converts the current object state to an array.
644 *
645 * @return array The array representation of the object.
646 */
647 public function to_array(): array {
648 $model = new Model_Mask_Login();
649 [ $routes, $nonces ] = Route::export_routes( 'mask_login' );
650
651 return array(
652 'enabled' => $model->enabled,
653 'useable' => $model->is_active(),
654 'login_url' => $model->get_new_login_url(),
655 'endpoints' => $routes,
656 'nonces' => $nonces,
657 );
658 }
659
660 /**
661 * Provides data for the dashboard widget.
662 *
663 * @return array An array of dashboard widget data.
664 */
665 public function dashboard_widget(): array {
666 $model = new Model_Mask_Login();
667
668 return array(
669 'model' => $model->export(),
670 'is_active' => $model->is_active(),
671 'is_mask_url_page_post_exists' => $model->is_mask_url_page_post_exists(),
672 );
673 }
674
675 /**
676 * Provides data for the frontend.
677 *
678 * @return array An array of data for the frontend.
679 */
680 public function data_frontend(): array {
681 // Don't use cache because wrong url is displayed for forbidden slugs.
682 $model = new Model_Mask_Login();
683
684 $data = array_merge(
685 array(
686 'model' => $model->export(),
687 'is_active' => $model->is_active(),
688 'new_login_url' => $model->get_new_login_url(),
689 'notices' => $this->compatibility_notices,
690 'is_mask_url_empty' => $model->is_mask_url_empty(),
691 'is_mask_url_page_post_exists' => $model->is_mask_url_page_post_exists(),
692 ),
693 $this->dump_routes_and_nonces()
694 );
695
696 if ( isset( $data['model']['redirect_traffic_page_id'] ) ) {
697 $id = $data['model']['redirect_traffic_page_id'];
698
699 $data['redirect_traffic_page_title'] = $id > 0 ? get_the_title( $id ) : '';
700 $data['redirect_traffic_page_url'] = $id > 0 ? get_the_permalink( $id ) : '#';
701 }
702
703 return $data;
704 }
705
706 /**
707 * Imports data into the model.
708 *
709 * @param array $data Data to be imported into the model.
710 */
711 public function import_data( array $data ) {
712 $model = $this->get_model();
713
714 $model->import( $data );
715 if ( $model->validate() ) {
716 $model->save();
717 }
718 }
719
720 /**
721 * Updates the welcome email for a specific site case.
722 *
723 * @param string $welcome_email The original welcome email content.
724 * @param int $blog_id The ID of the site.
725 * @param int $user_id The ID of the user.
726 * @param string $password The user's password.
727 * @param string $title The title of the welcome email.
728 * @param array $meta Additional metadata for the welcome email.
729 *
730 * @return string The updated welcome email content.
731 */
732 public function update_welcome_email_prosite_case(
733 string $welcome_email,
734 int $blog_id,
735 int $user_id,
736 string $password,
737 string $title,
738 array $meta
739 ) {
740 $url = get_blogaddress_by_id( $blog_id );
741 $welcome_email = str_replace(
742 $url . 'wp-login.php',
743 $this->get_model()->get_new_login_url( rtrim( $url, '/' ) ),
744 $welcome_email
745 );
746
747 return $welcome_email;
748 }
749
750 /**
751 * Updates the report logs link by adding a 'redirect_to' query parameter to the new login URL.
752 *
753 * @param string $logs_url The original logs URL.
754 * @param string $email The email address.
755 *
756 * @return string The updated logs URL with the 'redirect_to' query parameter.
757 */
758 public function update_report_logs_link( string $logs_url, string $email ): string {
759 return add_query_arg( 'redirect_to', $logs_url, $this->get_model()->get_new_login_url() );
760 }
761
762 /**
763 * Replaces the password reset link in the given message with a new login URL that includes a token.
764 *
765 * @param string $message The original message containing the password reset link.
766 * @param string $key The key used for the password reset link.
767 * @param string $user_login The username of the user.
768 * @param WP_User $user_data The user data.
769 *
770 * @return string The updated message with the new login URL.
771 * @since 2.5.5
772 */
773 public function flywheel_change_password_message(
774 string $message,
775 string $key,
776 string $user_login,
777 WP_User $user_data
778 ) {
779 $message = str_replace(
780 network_site_url( "wp-login.php?action=rp&key=$key&login=" . rawurlencode( $user_login ), 'login' ),
781 $this->get_model()->get_new_login_url( $this->get_site_url() )
782 . "?action=rp&key=$key&login=" . rawurlencode( $user_login ) . '&wd-ml-token=' . rawurlencode( $user_login ),
783 $message
784 );
785
786 return $message;
787 }
788
789 /**
790 * Change redirect param of the link 'Lost your password?'.
791 *
792 * @return string
793 */
794 public function change_lostpassword_redirect() {
795 return $this->get_model()->get_new_login_url( $this->get_site_url() ) . $this->get_permalink_separator() . 'checkemail=confirm';
796 }
797
798 /**
799 * Handle user profile email change request.
800 *
801 * @return void
802 */
803 private function handle_email_change_request() {
804 // If it is not for admin request.
805 if ( ! is_admin() ) {
806 return;
807 }
808
809 // If `IS_PROFILE_PAGE` constant is defined.
810 if ( ! defined( 'IS_PROFILE_PAGE' ) ) {
811 return;
812 }
813
814 // If request is not for profile page.
815 if ( ! IS_PROFILE_PAGE ) {
816 return;
817 }
818
819 // If query data is not set.
820 $hash = defender_get_data_from_request( 'newuseremail', 'g' );
821 if ( ! isset( $hash ) ) {
822 return;
823 }
824
825 global $wpdb;
826 $like = '%' . $wpdb->esc_like( $hash ) . '%';
827 $meta_key = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
828 $wpdb->prepare( "SELECT meta_key FROM {$wpdb->usermeta} WHERE meta_value LIKE %s", $like )
829 );
830
831 // Hash not found.
832 if ( '_new_email' !== $meta_key ) {
833 return;
834 }
835
836 // Everything good, now redirect user to login page.
837 $current_url = add_query_arg( defender_get_data_from_request( null, 'g' ), admin_url( 'profile.php' ) );
838 $redirect_url = esc_url( wp_login_url( $current_url ) );
839 wp_safe_redirect( $redirect_url );
840 die();
841 }
842
843 /**
844 * Exports strings.
845 *
846 * @return array An array of strings.
847 */
848 public function export_strings(): array {
849 return array(
850 \WP_Defender\Model\Setting\Mask_Login::get_module_name() . ' '
851 . ( $this->get_model()->enabled ? esc_html__( 'active', 'defender-security' ) : esc_html__( 'inactive', 'defender-security' ) ),
852 );
853 }
854
855 /**
856 * Generates configuration strings based on the provided configuration.
857 *
858 * @param array $config Configuration data.
859 *
860 * @return array Returns an array of configuration strings.
861 */
862 public function config_strings( array $config ): array {
863 return array(
864 \WP_Defender\Model\Setting\Mask_Login::get_module_name() . ' '
865 . ( $config['enabled'] ? esc_html__( 'active', 'defender-security' ) : esc_html__( 'inactive', 'defender-security' ) ),
866 );
867 }
868
869 /**
870 * Support for the password reset page on various hosting.
871 *
872 * @return void
873 */
874 public function handle_password_reset(): void {
875 // Get the email link.
876 $action = defender_get_data_from_request( 'action', 'g' );
877 $key = wp_unslash( defender_get_data_from_request( 'key', 'r' ) );
878 $login = wp_unslash( defender_get_data_from_request( 'login', 'r' ) );
879 $wd_ml_token = defender_get_data_from_request( 'wd-ml-token', 'g' );
880 if (
881 isset( $action, $key, $login, $wd_ml_token )
882 && 'rp' === $action
883 && $login === $wd_ml_token
884 ) {
885
886 $user = check_password_reset_key( $key, $login );
887 if ( ! is_wp_error( $user ) ) {
888 $value = sprintf( '%s:%s', $login, $key );
889 set_site_transient( 'wd-rp-' . COOKIEHASH, $value, 2 * MINUTE_IN_SECONDS );
890 wp_safe_redirect( remove_query_arg( array( 'key', 'login', 'wd-ml-token' ) ) );
891 exit;
892 }
893 }
894 $value = get_site_transient( 'wd-rp-' . COOKIEHASH );
895 // Process the data and display the result.
896 if (
897 isset( $action )
898 && in_array( $action, array( 'rp', 'resetpass' ), true )
899 && isset( $value ) && 0 < strpos( $value, ':' )
900 ) {
901 [ $login, $key ] = explode( ':', wp_unslash( $value ), 2 );
902 $user = check_password_reset_key( $key, $login );
903 if ( 'resetpass' === $action ) {
904 delete_site_transient( 'wd-rp-' . COOKIEHASH );
905 }
906 if ( ! is_wp_error( $user ) ) {
907 $this->render_partial(
908 'mask-login/reset',
909 array(
910 'user' => $user,
911 )
912 );
913 exit;
914 }
915 }
916 }
917
918 /**
919 * Check if a path is allowed without login masking.
920 *
921 * @param string $path Path to check.
922 *
923 * @return bool
924 * @since 2.6.4
925 */
926 private function is_allowed_path( string $path ): bool {
927 // Admin post requests to admin-post.php should be allowed.
928 $action = defender_get_data_from_request( 'action', 'r' );
929 $allowed = 'wp-admin/admin-post.php' === $path && is_string( $action ) && '' !== trim( $action );
930
931 /**
932 * Filter to allow whitelisting paths from login masking.
933 *
934 * @param bool $allowed Is current path allowed?.
935 * @param string $path Path to check.
936 *
937 * @since 2.6.4
938 */
939 return apply_filters( 'wd_mask_login_is_allowed_path', $allowed, $path );
940 }
941
942 /**
943 * An endpoint for fetching Post/Page.
944 *
945 * @param Request $request Request data.
946 *
947 * @return void
948 * @since 2.7.1
949 * @defender_route
950 */
951 public function get_posts( Request $request ): void {
952 $data = $request->get_data(
953 array(
954 'per_page' => array(
955 'type' => 'int',
956 'sanitize' => 'sanitize_text_field',
957 ),
958 'search' => array(
959 'type' => 'string',
960 'sanitize' => 'sanitize_text_field',
961 ),
962 )
963 );
964
965 $per_page = $data['per_page'] ?? 50;
966 $search = $data['search'] ?? '';
967
968 add_filter( 'posts_where', array( $this, 'posts_where_title' ), 10, 2 );
969 $post_query = new WP_Query(
970 array(
971 'post_type' => array( 'page', 'post' ),
972 'posts_per_page' => $per_page,
973 'search_by_post_title' => $search,
974 'post_status' => 'publish',
975 'orderby' => 'title',
976 'order' => 'ASC',
977 )
978 );
979 remove_filter( 'posts_where', array( $this, 'posts_where_title' ), 10 );
980
981 $posts_array = $post_query->posts;
982 $data = array();
983 foreach ( $posts_array as $post ) {
984 $data[] = array(
985 'id' => $post->ID,
986 'name' => $post->post_title,
987 'url' => get_the_permalink( $post->ID ),
988 );
989 }
990
991 wp_send_json_success( $data );
992 }
993
994 /**
995 * Filter the WHERE clause of the query.
996 *
997 * @param string $where The WHERE clause of the query.
998 * @param WP_Query $wp_query The query object.
999 *
1000 * @return string $where
1001 * @since 2.7.1
1002 */
1003 public function posts_where_title( string $where, WP_Query $wp_query ) {
1004 global $wpdb;
1005
1006 $search_term = $wp_query->get( 'search_by_post_title' );
1007 if ( is_string( $search_term ) && '' !== trim( $search_term ) ) {
1008 $where .= ' AND ' . $wpdb->posts . '.post_title LIKE \'%' . esc_sql( $wpdb->esc_like( $search_term ) ) . '%\'';
1009 }
1010
1011 return $where;
1012 }
1013
1014 /**
1015 * Change the admin URL for sub sites.
1016 *
1017 * @param string $url The original URL.
1018 * @param string $path The path of the URL.
1019 * @param mixed $blog_id The ID of the blog.
1020 *
1021 * @return string The modified URL.
1022 */
1023 public function change_subsites_admin_url( string $url, string $path, $blog_id ) {
1024 if ( '' === trim( $path ) && is_int( $blog_id ) && 0 < $blog_id ) {
1025 $mask_url = trim( $this->model->mask_url );
1026
1027 if ( '' !== $mask_url && $this->check_if_domain_is_mapped( $url ) ) {
1028 $url = str_replace( 'wp-admin', $mask_url, untrailingslashit( $url ) );
1029 }
1030 }
1031
1032 return $url;
1033 }
1034
1035 /**
1036 * Check if domain is mapped.
1037 *
1038 * @param string $url The URL.
1039 *
1040 * @return bool
1041 */
1042 public function check_if_domain_is_mapped( string $url ): bool {
1043 $is_mapped = false;
1044
1045 if ( '' !== trim( $url ) ) {
1046 $url_arr = wp_parse_url( $url );
1047 $net_url_arr = wp_parse_url( network_site_url() );
1048
1049 if (
1050 isset( $url_arr['host'] ) && is_string( $url_arr['host'] ) && '' !== trim( $url_arr['host'] ) &&
1051 isset( $net_url_arr['host'] ) && is_string( $net_url_arr['host'] ) && '' !== trim( $net_url_arr['host'] ) &&
1052 $this->get_domain_from_host( $url_arr['host'] ) !== $this->get_domain_from_host( $net_url_arr['host'] )
1053 ) {
1054 $is_mapped = true;
1055 }
1056 }
1057
1058 return $is_mapped;
1059 }
1060
1061 /**
1062 * Extract domain from host.
1063 *
1064 * @param string $host The host.
1065 *
1066 * @return string
1067 */
1068 public function get_domain_from_host( string $host ): string {
1069 $host = strtolower( trim( $host ) );
1070
1071 $count = substr_count( $host, '.' );
1072 if ( 2 === $count ) {
1073 if ( strlen( explode( '.', $host )[1] ) > 3 ) {
1074 $host = explode( '.', $host, 2 )[1];
1075 }
1076 } elseif ( $count > 2 ) {
1077 $host = $this->get_domain_from_host( explode( '.', $host, 2 )[1] );
1078 }
1079
1080 return $host;
1081 }
1082
1083 /**
1084 * Update admin bar menu url to masked login url if domain is mapped.
1085 *
1086 * @param WP_Admin_Bar $admin_bar Admin bar object.
1087 *
1088 * @return void
1089 * @since 3.4.0
1090 */
1091 public function update_admin_bar_menu( WP_Admin_Bar $admin_bar ) {
1092 $mask_url = trim( $this->model->mask_url );
1093 if ( '' === $mask_url ) {
1094 return;
1095 }
1096
1097 $admin_bar_nodes = $admin_bar->get_nodes();
1098 $needle = '/wp-admin/';
1099 $length = strlen( $needle );
1100 foreach ( $admin_bar_nodes as $nodes ) {
1101 if ( substr( $nodes->href, - $length ) === $needle && $this->check_if_domain_is_mapped( $nodes->href ) ) {
1102 $href = str_replace( 'wp-admin', $mask_url, untrailingslashit( $nodes->href ) );
1103
1104 $admin_bar->add_menu(
1105 array(
1106 'id' => $nodes->id,
1107 'href' => $href,
1108 )
1109 );
1110 }
1111 }
1112 }
1113
1114 /**
1115 * Update my sites action url to masked login url if domain is mapped.
1116 *
1117 * @param string $actions The current action links.
1118 * @param object $user_blog The user blog object.
1119 *
1120 * @return string
1121 * @since 3.4.0
1122 */
1123 public function update_myblogs_blog_actions( string $actions, object $user_blog ) {
1124 $mask_url = trim( $this->model->mask_url );
1125
1126 if ( '' === $mask_url ) {
1127 return $actions;
1128 }
1129
1130 $admin_url = get_admin_url( $user_blog->userblog_id );
1131
1132 if ( $this->check_if_domain_is_mapped( $admin_url ) ) {
1133 $updated_admin_url = str_replace( 'wp-admin', $mask_url, untrailingslashit( $admin_url ) );
1134 $actions = str_replace( $admin_url, $updated_admin_url, $actions );
1135 }
1136
1137 return $actions;
1138 }
1139
1140 /**
1141 * Set locale on Mask Login page.
1142 *
1143 * @return void
1144 * @since 3.12.0
1145 */
1146 public function set_locale(): void {
1147 $this->service->set_locale();
1148 }
1149
1150 /**
1151 * Enable/disable module.
1152 *
1153 * @param Request $request Request object.
1154 *
1155 * @return Response
1156 * @defender_route
1157 * @since 3.12.0
1158 */
1159 public function toggle_module( Request $request ): Response {
1160 $data = $request->get_data(
1161 array(
1162 'enabled' => array(
1163 'type' => 'boolean',
1164 ),
1165 )
1166 );
1167
1168 $this->model->enabled = $data['enabled'];
1169 $this->model->save();
1170
1171 Config_Hub_Helper::set_clear_active_flag();
1172
1173 if ( ! $this->model->enabled || ! $this->model->is_mask_url_page_post_exists() ) {
1174 return new Response(
1175 true,
1176 array_merge(
1177 array(
1178 'message' => esc_html__( 'Your settings have been updated.', 'defender-security' ),
1179 'auto_close' => true,
1180 ),
1181 $this->data_frontend()
1182 )
1183 );
1184 }
1185
1186 return new Response(
1187 false,
1188 array(
1189 'error' => esc_html__(
1190 'A page already exists at this URL. Please enter a unique URL for your login area.',
1191 'defender-security'
1192 ),
1193 )
1194 );
1195 }
1196
1197 /**
1198 * Get permalink separator.
1199 *
1200 * @return string
1201 */
1202 public function get_permalink_separator(): string {
1203 return $this->model->is_permalink_structure_empty() ? '&' : '?';
1204 }
1205 }
1206