PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.3.0
Defender Security – Malware Scanner, Login Security & Firewall v6.3.0
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
← All changes | src/component/class-hub-connector.php +589 -27 5.6.2 → 6.3.0 View file →
@@ -8,9 +8,11 @@
8 8 namespace WP_Defender\Component;
9 9
10 10 use WPMUDEV\Hub\Connector;
11 11 use Calotes\Base\Component;
12 +use WP_Defender\Behavior\WPMUDEV;
12 13 use WP_Defender\Traits\Defender_Dashboard_Client;
14 +use WPMUDEV\Hub\Connector\API;
13 15
14 16 /**
15 17 * Handles the functionality related to the Hub Connector module.
16 18 *
@@ -33,8 +35,15 @@
33 35 */
34 36 public const CONNECTION_ACTION = 'hub_connection';
35 37
36 38 /**
39 + * Temporary flag used while the first Hub sync is being checked.
40 + *
41 + * @const string
42 + */
43 + public const SYNCING_TRANSIENT_KEY = 'wpdef_hc_site_syncing';
44 +
45 + /**
37 46 * Checks if the Hub connector should render its UI.
38 47 *
39 48 * Verifies the nonce from the global-ip page to determine if the Hub connector should render its UI.
40 49 *
@@ -40,9 +49,21 @@
40 49 *
41 50 * @return bool The nonce value on success, false on failure.
42 51 */
43 52 public static function should_render() {
44 - return ! self::get_status() && wp_verify_nonce( defender_get_data_from_request( '_def_nonce', 'g' ), self::CONNECTION_ACTION );
53 + // Team selection callbacks need the library's own UI — let them through.
54 + if ( self::is_team_selection_callback() ) {
55 + return ! self::get_hcm_status() && wp_verify_nonce( defender_get_data_from_request( '_def_nonce', 'g' ), self::CONNECTION_ACTION );
56 + }
57 +
58 + $page_action = sanitize_text_field( wp_unslash( $_GET['page_action'] ?? '' ) );
59 + $is_hub_callback = isset( $_GET['hub_connector_callback'] ) && '1' === $_GET['hub_connector_callback'];
60 +
61 + if ( self::CONNECTION_ACTION === $page_action || $is_hub_callback ) {
62 + return false;
63 + }
64 +
65 + return ! self::get_hcm_status() && wp_verify_nonce( defender_get_data_from_request( '_def_nonce', 'g' ), self::CONNECTION_ACTION );
45 66 }
46 67
47 68 /**
48 69 * Initialize the Hub Connector module and set its options.
@@ -76,12 +97,12 @@
76 97
77 98 $result = $this->maybe_summary_box_trigger( $view );
78 99 // Get advanced params.
79 100 $res = $this->get_utm_tags( $page, $result['view'], $result['is_summary'] );
80 - if ( ! empty( $res['utm_campaign'] ) ) {
101 + if ( '' !== $res['utm_campaign'] ) {
81 102 $options['extra_args']['register']['utm_campaign'] = $res['utm_campaign'];
82 103 }
83 - if ( ! empty( $res['utm_content'] ) ) {
104 + if ( '' !== $res['utm_content'] ) {
84 105 $options['extra_args']['register']['utm_content'] = $res['utm_content'];
85 106 }
86 107
87 108 Connector::get()->set_options( self::PLUGIN_IDENTIFIER, $options );
@@ -87,8 +108,22 @@
87 108 Connector::get()->set_options( self::PLUGIN_IDENTIFIER, $options );
88 109 }
89 110
90 111 /**
112 + * Checks if the current request is a Hub team-selection callback.
113 + *
114 + * @return bool
115 + */
116 + public static function is_team_selection_callback(): bool {
117 + // Follow the approach implemented in HC module. Skip a sanitization step to avoid ruining the input.
118 + $is_multi_auth = isset( $_REQUEST['is_multi_auth'] ) ? (int) $_REQUEST['is_multi_auth'] : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
119 +
120 + return isset( $_REQUEST['hub_connector_callback'] ) && '1' === $_REQUEST['hub_connector_callback'] // phpcs:ignore WordPress.Security.NonceVerification.Recommended
121 + && isset( $_REQUEST['user_apikey'] ) && '' !== $_REQUEST['user_apikey'] // phpcs:ignore WordPress.Security.NonceVerification.Recommended
122 + && 1 === $is_multi_auth;
123 + }
124 +
125 + /**
91 126 * Gets the label for the button to connect the site to the Hub.
92 127 *
93 128 * @return string The label for the button.
94 129 */
@@ -136,9 +171,9 @@
136 171 * @return string
137 172 */
138 173 public static function get_feature_name(): string {
139 174 $feature_name = defender_get_data_from_request( 'module_name', 'g' );
140 - if ( ! empty( $feature_name ) ) {
175 + if ( ! is_null( $feature_name ) && '' !== $feature_name ) {
141 176 return $feature_name;
142 177 }
143 178
144 179 $view = defender_get_data_from_request( 'view', 'g' );
@@ -161,9 +196,9 @@
161 196 */
162 197 private function get_utm_tags( string $page = '', string $view = '', bool $is_summary = false ): array {
163 198 $utm_campaign = '';
164 199 $utm_content = '';
165 - if ( ! empty( $page ) ) {
200 + if ( '' !== $page ) {
166 201 switch ( $page ) {
167 202 // There are buttons on notice or widget on the Dashboard page.
168 203 case 'wp-defender':
169 204 $utm_content = 'hub-connector';
@@ -210,41 +245,568 @@
210 245 * Retrieve the Hub connector URL.
211 246 *
212 247 * @param string $page Optional. The page to load. Default is empty string.
213 248 * @param string $view Optional. The view to load. Default is empty string.
249 + * @param string $utm_campaign Optional. The UTM campaign parameter. Default is empty string.
250 + * @param string $utm_content Optional. The UTM content parameter. Default is empty string.
251 + * @param string $module_slug Optional. Module slug for post-connection auto-enable. Default is empty string.
214 252 *
215 253 * @return string
216 254 */
217 - public function get_url( string $page = '', string $view = '' ): string {
218 - if ( $this->is_dash_activated() ) {
219 - $args = array(
220 - 'page' => 'wpmudev',
221 - 'utm_source' => self::PLUGIN_IDENTIFIER,
222 - 'utm_medium' => 'plugin',
223 - );
255 + public function get_url( string $page = '', string $view = '', string $utm_campaign = '', string $utm_content = '', string $module_slug = '' ): string {
256 + $args = self::get_connection_args( '' !== $page ? $page : 'wp-defender', $module_slug );
224 257
225 - $result = $this->maybe_summary_box_trigger( $view );
226 - // Get advanced params.
227 - $res = $this->get_utm_tags( $page, $result['view'], $result['is_summary'] );
228 - if ( ! empty( $res['utm_campaign'] ) ) {
229 - $args['utm_campaign'] = $res['utm_campaign'];
258 + if ( '' !== $view ) {
259 + $args['view'] = $view;
260 + }
261 +
262 + $result = $this->maybe_summary_box_trigger( $view );
263 + // Get advanced params.
264 + $res = $this->get_utm_tags( $page, $result['view'], $result['is_summary'] );
265 +
266 + if ( '' !== $utm_campaign ) {
267 + $args['utm_campaign'] = $utm_campaign;
268 + } elseif ( '' !== $res['utm_campaign'] ) {
269 + $args['utm_campaign'] = $res['utm_campaign'];
270 + }
271 +
272 + if ( '' !== $utm_content ) {
273 + $args['utm_content'] = $utm_content;
274 + } elseif ( '' !== $res['utm_content'] ) {
275 + $args['utm_content'] = $res['utm_content'];
276 + }
277 +
278 + return add_query_arg( $args, self::get_admin_url() );
279 + }
280 +
281 + /**
282 + * Checks if Hub Connector is logged in.
283 + *
284 + * @return bool
285 + */
286 + public static function is_logged_in(): bool {
287 + if ( ! class_exists( '\WPMUDEV\Hub\Connector\API' ) ) {
288 + return false;
289 + }
290 +
291 + $api = API::get();
292 +
293 + return $api && method_exists( $api, 'is_logged_in' ) && $api->is_logged_in();
294 + }
295 +
296 + /**
297 + * Sync site data with Hub.
298 + *
299 + * @return bool|\WP_Error
300 + */
301 + public function sync() {
302 + if ( ! class_exists( '\WPMUDEV\Hub\Connector\API' ) ) {
303 + return false;
304 + }
305 +
306 + $api = API::get();
307 +
308 + if ( $api && method_exists( $api, 'sync_site' ) ) {
309 + $sync = $api->sync_site();
310 + if ( is_wp_error( $sync ) ) {
311 + return $sync;
230 312 }
231 - if ( ! empty( $res['utm_content'] ) ) {
232 - $args['utm_content'] = $res['utm_content'];
313 + }
314 +
315 + return true;
316 + }
317 +
318 + /**
319 + * Verify the auth nonce from request.
320 + *
321 + * @return bool
322 + */
323 + public static function verify_auth_nonce(): bool {
324 + return (bool) wp_verify_nonce(
325 + sanitize_text_field( wp_unslash( $_REQUEST['auth_nonce'] ?? '' ) ),
326 + 'auth_nonce'
327 + );
328 + }
329 +
330 + /**
331 + * Check if the site is in the temporary post-auth Hub syncing state.
332 + *
333 + * @return bool
334 + */
335 + public static function is_syncing(): bool {
336 + // If already logged in or a login error occurred, the auth flow is settled —
337 + // clear any stale syncing transient and report not syncing.
338 + if ( self::is_logged_in() || self::has_error_in_login() ) {
339 + delete_site_transient( self::SYNCING_TRANSIENT_KEY );
340 +
341 + return false;
342 + }
343 +
344 + $syncing = false;
345 + if ( current_user_can( 'manage_options' ) && self::verify_auth_nonce() ) {
346 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified above by self::verify_auth_nonce().
347 + $page_action = sanitize_text_field( wp_unslash( $_REQUEST['page_action'] ?? '' ) );
348 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified above by self::verify_auth_nonce().
349 + $set_apikey = sanitize_text_field( wp_unslash( $_REQUEST['set_apikey'] ?? '' ) );
350 + $syncing = self::CONNECTION_ACTION === $page_action && '' !== $set_apikey;
351 + }
352 +
353 + if ( $syncing ) {
354 + set_site_transient( self::SYNCING_TRANSIENT_KEY, true, MINUTE_IN_SECONDS );
355 + }
356 +
357 + return (bool) get_site_transient( self::SYNCING_TRANSIENT_KEY );
358 + }
359 +
360 + /**
361 + * Check if there is an error in login response from HUB.
362 + *
363 + * @return bool
364 + */
365 + public static function has_error_in_login() {
366 + // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only URL parameter check / validated via verify_auth_nonce below.
367 + $page_action = sanitize_text_field( wp_unslash( $_GET['page_action'] ?? '' ) );
368 + $api_error = sanitize_text_field( wp_unslash( $_GET['api_error'] ?? '' ) );
369 + $is_hub_callback = isset( $_GET['hub_connector_callback'] ) && '1' === $_GET['hub_connector_callback'];
370 + $has_api_error = ! in_array( trim( $api_error ), array( '', '0' ), true );
371 +
372 + if ( self::CONNECTION_ACTION === $page_action ) {
373 + return $has_api_error;
374 + }
375 +
376 + if ( ! $is_hub_callback || ! self::verify_auth_nonce() ) {
377 + return false;
378 + }
379 +
380 + return $has_api_error;
381 + }
382 +
383 + /**
384 + * Get authentication error messages. Copied from HC.
385 + *
386 + * Based on the error code, prepare different error messages.
387 + *
388 + * @return string
389 + */
390 + public static function get_auth_error(): string {
391 + /**
392 + * Nonce is verified in `process_auth_callback` before this method called.
393 + *
394 + * @see self::process_auth_callback()
395 + */
396 + // phpcs:disable WordPress.Security.NonceVerification.Recommended
397 +
398 + $error = '';
399 + $reset_url = class_exists( '\WPMUDEV\Hub\Connector\Data' )
400 + ? \WPMUDEV\Hub\Connector\Data::get()->server_url( 'forgot-password' )
401 + : 'https://wpmudev.com/forgot-password';
402 + $skip_trial_url = \WPMUDEV\Hub\Connector\Data::get()->server_url( 'hub/account/?skip_trial' );
403 + $trial_info_url = \WPMUDEV\Hub\Connector\Data::get()->server_url( 'docs/getting-started/how-free-trials-work/' );
404 + $websites_url = \WPMUDEV\Hub\Connector\Data::get()->server_url( 'hub2/' );
405 + $security_info_url = \WPMUDEV\Hub\Connector\Data::get()->server_url( 'manuals/hub-security/' );
406 + $support_url = \WPMUDEV\Hub\Connector\Data::get()->server_url( 'hub/support/' );
407 + $account_details_url = \WPMUDEV\Hub\Connector\Data::get()->server_url( 'hub2/account/details/' );
408 +
409 + if ( isset( $_GET['api_error'] ) ) {
410 + // Get errors.
411 + $api_error = sanitize_key( wp_unslash( $_GET['api_error'] ) );
412 + $auth_error = sanitize_key( wp_unslash( $_GET['auth_error'] ?? '' ) );
413 +
414 + if ( 1 === (int) $api_error || 'auth' === $api_error ) {
415 + switch ( $auth_error ) {
416 + case 'google_linked':
417 + $error = sprintf(
418 + // translators: %s Account detail URL.
419 + __(
420 + 'You are currently using your Google account as your preferred login method. If you wish to login with your WPMU DEV email & password instead, please change the <strong>Login Method</strong> in <a href="%s" target="_blank">your WPMU DEV account</a>.',
421 + 'defender-security'
422 + ),
423 + $account_details_url
424 + );
425 + break;
426 + case 'google_unlinked':
427 + $error = sprintf(
428 + // translators: %s Account detail URL.
429 + __(
430 + 'You are currently using your WPMU DEV email & password as your preferred login method. If you wish to login with your Google account instead, please change the <strong>Login Method</strong> in <a href="%s" target="_blank">your WPMU DEV account</a>.',
431 + 'defender-security'
432 + ),
433 + $account_details_url
434 + );
435 + break;
436 + case 'reauth_google':
437 + $error = sprintf(
438 + // translators: %1$s Account detail URL, %2$s Reset URL.
439 + __(
440 + 'Due to security improvements, you will need to re-link your Google account in the Hub. Please log in with your WPMU DEV email & password for now, then set up your preferred <strong>Login Method</strong> in <a href="%1$s" target="_blank">your WPMU DEV account</a>. Forgot your password? You can <a href="%2$s" target="_blank">reset it here</a>.',
441 + 'defender-security'
442 + ),
443 + $account_details_url,
444 + $reset_url
445 + );
446 + break;
447 + default:
448 + // Invalid credentials.
449 + $error = sprintf(
450 + '%s<br><a href="%s" target="_blank"><strong>%s</strong></a>',
451 + esc_html__( 'Your login details were incorrect. Please make sure you\'re using your WPMU DEV email and password and try again.', 'defender-security' ),
452 + $reset_url,
453 + esc_html__( 'Forgot your password?', 'defender-security' )
454 + );
455 + break;
456 + }
457 + } else {
458 + switch ( $api_error ) {
459 + case 'in_trial':
460 + $error = sprintf(
461 + '%s<br><a href="%s" target="_blank">%s</a>',
462 + sprintf(
463 + // translators: %1$s Rest URL, %2$s Upgrade URL, %3$s Trial URL.
464 + __(
465 + 'This domain has previously been registered with us by the user %1$s. To use WPMU DEV on this domain, you can either log in with the original account (you can <a target="_blank" href="%2$s">reset your password</a>) or <a target="_blank" href="%3$s">upgrade your trial</a> to a full membership. Trial accounts can\'t use previously registered domains - <a target="_blank" href="%4$s">here\'s why</a>.',
466 + 'defender-security'
467 + ),
468 + '<strong style="word-break: break-all;">' . esc_html( $_GET['display_name'] ) . '</strong>', // phpcs:ignore
469 + $reset_url,
470 + $skip_trial_url,
471 + $trial_info_url
472 + ),
473 + $support_url,
474 + __( 'Contact support if you need further assistance &raquo;', 'defender-security' )
475 + );
476 + break;
477 + case 'already_registered':
478 + $error = sprintf(
479 + // translators: %1$d Account name, %2$s Security info, %3$s Hub URL, %4$s Support URL.
480 + __(
481 + 'This site is currently registered to %1$s. For <a target="_blank" href="%2$s">security reasons</a> they will need to go to the <a target="_blank" href="%3$s">WPMU DEV Hub</a> and remove this domain before you can log in. If you do not have access to that account, and have no way of contacting that user, please <a target="_blank" href="%4$s">contact support for assistance</a>.',
482 + 'defender-security'
483 + ),
484 + '<strong style="word-break: break-all;">' . esc_html( $_GET['display_name'] ) . '</strong>', // phpcs:ignore.
485 + $security_info_url,
486 + $websites_url,
487 + $support_url
488 + );
489 + break;
490 + case 'banned_account':
491 + $error = sprintf(
492 + // translators: %s Support URL.
493 + __( 'This domain cannot be registered to your WPMU DEV account.<br><a href="%s">Contact Accounts & Billing if you need further assistance »</a>', 'defender-security' ),
494 + \WPMUDEV\Hub\Connector\Data::get()->server_url( 'hub2/#ask-question' )
495 + );
496 + break;
497 + case 'expired_membership':
498 + $error = sprintf(
499 + // translators: %1$s Hub Account URL, %2$s: Switch to Free URL.
500 + __(
501 + 'Login failed — your WPMU DEV membership has expired. Renew now to regain full access, or switch to our free plan to continue managing all your site in the Hub.<br/><br/><a class="sui-button sui-button-blue" href="%1$s" target="_blank">Renew Membership</a>&nbsp;<a class="sui-button sui-button-ghost" href="%2$s" target="_blank">Switch to Free</a>',
502 + 'defender-security'
503 + ),
504 + \WPMUDEV\Hub\Connector\Data::get()->server_url( 'hub2/account/' ),
505 + \WPMUDEV\Hub\Connector\Data::get()->server_url( 'hub2/?switch-free=1' )
506 + );
507 + break;
508 + case 'invalid_nonce':
509 + case 'invalid_double_submit_cookie':
510 + case 'invalid_google_creds':
511 + case '':
512 + $error = __( 'Google login failed. Please try again.', 'defender-security' );
513 + break;
514 + default:
515 + // Use the actual error message passed through from the sync poll if available.
516 + if ( isset( $_GET['api_error_msg'] ) && '' !== $_GET['api_error_msg'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
517 + $error = sanitize_text_field( wp_unslash( $_GET['api_error_msg'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
518 + } else {
519 + $error = __( 'Unknown error. Please update the WPMU DEV Dashboard plugin and try again.', 'defender-security' );
520 + }
521 + break;
522 + }
233 523 }
524 + } elseif ( isset( $_REQUEST['connection_error'] ) && '' !== $_REQUEST['connection_error'] ) {
525 + // Variable `$connection_error` is set by the UI function `render_dashboard`.
526 + $error = sprintf(
527 + '%s<br>%s<br><em>%s</em>',
528 + __( 'Your server had a problem connecting to WPMU DEV. Please try again.', 'defender-security' ),
529 + __( 'If this problem continues, please contact your host with this error message and ask:', 'defender-security' ),
530 + sprintf(
531 + // translators: url to API.
532 + __( '"Is PHP on my server properly configured to be able to contact %s with a POST HTTP request via fsockopen or CURL?"', 'defender-security' ),
533 + \WPMUDEV\Hub\Connector\Data::get()->server_url()
534 + )
535 + );
536 + } elseif ( isset( $_REQUEST['invalid_key'] ) && '' !== $_REQUEST['invalid_key'] ) {
537 + // Invalid API key.
538 + $error = __( 'Your API Key was invalid. Please try again.', 'defender-security' );
539 + }
234 540
235 - return add_query_arg( $args, network_admin_url( 'admin.php' ) );
541 + /**
542 + * Filter to modify auth error text.
543 + *
544 + * @since 1.0.0
545 + *
546 + * @param string $error Error message.
547 + * @param string $plugin Plugin identifier.
548 + */
549 + return apply_filters( 'wpmudev_hub_connector_get_auth_error', $error, self::PLUGIN_IDENTIFIER );
550 + // phpcs:enable WordPress.Security.NonceVerification.Recommended
551 + }
552 +
553 + /**
554 + * Get Hub Google login URL (https://wpmudev.com/api/dashboard/v2/google-auth).
555 + *
556 + * @return string
557 + */
558 + public static function get_hub_google_login_url(): string {
559 + return \WPMUDEV\Hub\Connector\Data::get()->server_url( 'api/dashboard/v2/google-auth' );
560 + }
561 +
562 + /**
563 + * Get connection URL for Hub.
564 + *
565 + * @param string $target_page The target page to connect to.
566 + * @param string $utm_campaign The UTM campaign to append to the URL.
567 + *
568 + * @return string The connection URL.
569 + */
570 + public static function get_connect_site_url( string $target_page = 'wp-defender', string $utm_campaign = '' ): string {
571 + $args = array();
572 +
573 + if ( self::should_redirect_to_dashboard() ) {
574 + $args['page'] = 'wpmudev';
575 + } else {
576 + $args = self::get_connection_args( $target_page );
236 577 }
237 578
238 - $query = array(
239 - '_def_nonce' => wp_create_nonce( self::CONNECTION_ACTION ),
579 + if ( '' !== trim( $utm_campaign ) ) {
580 + $args['utm_campaign'] = sanitize_text_field( $utm_campaign );
581 + }
582 +
583 + $admin_url = self::get_admin_url();
584 +
585 + return add_query_arg( $args, $admin_url );
586 + }
587 +
588 + /**
589 + * Get the Hub Connector callback URL inside Defender.
590 + *
591 + * @param string $target_page The target page to connect to.
592 + * @param string $utm_campaign The UTM campaign to append to the URL.
593 + *
594 + * @return string
595 + */
596 + public static function get_hub_connector_callback_url( string $target_page = 'wp-defender', string $utm_campaign = '' ): string {
597 + $args = self::get_connection_args( $target_page );
598 +
599 + if ( '' !== trim( $utm_campaign ) ) {
600 + $args['utm_campaign'] = sanitize_text_field( $utm_campaign );
601 + }
602 +
603 + return add_query_arg( $args, self::get_admin_url() );
604 + }
605 +
606 + /**
607 + * Check if should redirect to WPMUDEV Dashboard.
608 + *
609 + * @return bool
610 + */
611 + private static function should_redirect_to_dashboard(): bool {
612 + return ! self::is_wpmudev_dashboard_connected() && class_exists( '\WPMUDEV_Dashboard' );
613 + }
614 +
615 + /**
616 + * Check if WPMUDEV Dashboard is connected.
617 + *
618 + * @return bool
619 + */
620 + public static function is_wpmudev_dashboard_connected(): bool {
621 + if ( ! class_exists( '\WPMUDEV_Dashboard' ) ) {
622 + return false;
623 + }
624 +
625 + $dashboard_api = \WPMUDEV_Dashboard::$api ?? null;
626 +
627 + return is_object( $dashboard_api ) &&
628 + method_exists( $dashboard_api, 'get_membership_status' ) &&
629 + method_exists( $dashboard_api, 'has_key' ) &&
630 + $dashboard_api->has_key();
631 + }
632 +
633 + /**
634 + * Get connection arguments for URL.
635 + *
636 + * @param string $target_page The target page.
637 + * @param string $module_slug Optional module slug to include for post-connection auto-enable.
638 + * @return array
639 + */
640 + private static function get_connection_args( string $target_page, string $module_slug = '' ): array {
641 + $args = array(
642 + 'page' => sanitize_text_field( $target_page ),
643 + '_def_nonce' => wp_create_nonce( self::CONNECTION_ACTION ),
644 + 'page_action' => self::CONNECTION_ACTION,
645 + 'hub_connector_callback' => 1,
240 646 );
241 - if ( ! empty( $page ) ) {
242 - $query['page'] = $page;
647 +
648 + if ( '' !== $module_slug ) {
649 + $args['module_slug'] = sanitize_text_field( $module_slug );
243 650 }
244 - if ( ! empty( $view ) ) {
245 - $query['view'] = $view;
651 +
652 + return $args;
653 + }
654 +
655 + /**
656 + * Get appropriate admin URL.
657 + *
658 + * @return string
659 + */
660 + private static function get_admin_url(): string {
661 + return is_network_admin() ? network_admin_url( 'admin.php' ) : admin_url( 'admin.php' );
662 + }
663 +
664 + /**
665 + * Get the site domain.
666 + *
667 + * @return string
668 + */
669 + public static function get_site_domain(): string {
670 + return \WPMUDEV\Hub\Connector\Data::get()->network_site_url();
671 + }
672 +
673 + /**
674 + * Get Hub Login Auth URL (https://wpmudev.com/api/dashboard/v2/site-authenticate).
675 + *
676 + * @return string
677 + */
678 + public static function get_hub_site_login_auth_url(): string {
679 + return \WPMUDEV\Hub\Connector\API::get()->rest_url( 'site-authenticate' );
680 + }
681 +
682 + /**
683 + * Hub member profile for React UIs (same API as other WPMU DEV plugins: Data::profile_data).
684 + *
685 + * @return array<string, string>|null
686 + */
687 + public static function get_profile_data_for_ui(): ?array {
688 + if ( ! class_exists( '\WPMUDEV\Hub\Connector\Data' ) ) {
689 + return null;
246 690 }
247 691
248 - return add_query_arg( $query, network_admin_url( 'admin.php' ) );
692 + $api_logged_in = self::is_logged_in();
693 + $dashboard_connected = self::is_wpmudev_dashboard_connected();
694 + $is_pro = wd_di()->get( WPMUDEV::class )->is_pro();
695 +
696 + if ( ! $api_logged_in && ! $dashboard_connected && ! $is_pro ) {
697 + return null;
698 + }
699 +
700 + if (
701 + $dashboard_connected &&
702 + ! $api_logged_in &&
703 + is_object( \WPMUDEV_Dashboard::$settings ) &&
704 + method_exists( \WPMUDEV_Dashboard::$settings, 'get' )
705 + ) {
706 + $dashboard_profile = \WPMUDEV_Dashboard::$settings->get( 'profile_data' );
707 + $raw = $dashboard_profile['profile'] ?? array();
708 + } else {
709 + $raw = \WPMUDEV\Hub\Connector\Data::get()->profile_data( true );
710 + }
711 +
712 + if ( ! is_array( $raw ) || ! isset( $raw['user_name'] ) || '' === trim( $raw['user_name'] ) ) {
713 + return null;
714 + }
715 +
716 + $display_name = isset( $raw['name'] ) ? (string) $raw['name'] : '';
717 + $user_name = isset( $raw['user_name'] ) ? (string) $raw['user_name'] : '';
718 + $avatar = isset( $raw['avatar'] ) ? (string) $raw['avatar'] : '';
719 +
720 + $initial = '' !== $display_name ? strtoupper( substr( $display_name, 0, 1 ) ) : '';
721 +
722 + return array(
723 + 'initials' => $initial,
724 + 'avatar' => $avatar,
725 + 'profileBackgroundColor' => '' !== $avatar ? '#f8f8f8' : '#0059ff',
726 + 'profileFontColor' => '#ffffff',
727 + 'userName' => $user_name,
728 + 'email' => is_email( $user_name ) ? $user_name : '',
729 + 'displayName' => $display_name,
730 + );
731 + }
732 +
733 + /**
734 + * Get Hub register URL with site connection parameters.
735 + *
736 + * @param string $current_page The current page.
737 + * @param string $post_sync_redirect Optional. URL to redirect to after sync completes. Default is empty string.
738 + *
739 + * @return string
740 + */
741 + public static function get_hub_register_url( string $current_page = 'wp-defender', string $post_sync_redirect = '' ): string {
742 + // Fallback base URL when the Hub Connector library is not loaded.
743 + $base_url = 'https://wpmudev.com/register';
744 +
745 + if ( class_exists( '\WPMUDEV\Hub\Connector\Data' ) ) {
746 + $base_url = \WPMUDEV\Hub\Connector\Data::get()->server_url( 'register' );
747 + }
748 +
749 + // Get the hub connection URL.
750 + $hub_connect_url = self::get_hub_connector_callback_url( $current_page );
751 +
752 + // Prepare redirect URL with callback and auth nonce.
753 + $auth_nonce = wp_create_nonce( 'auth_nonce' );
754 + $callback_args = array(
755 + 'hub_connector_callback' => 1,
756 + 'auth_nonce' => $auth_nonce,
757 + );
758 + // rawurlencode() needed so the nested URL doesn't bleed into the outer query string.
759 + if ( '' !== $post_sync_redirect ) {
760 + $callback_args['post_sync_redirect'] = rawurlencode( esc_url_raw( $post_sync_redirect ) );
761 + }
762 + $redirect_url = add_query_arg( $callback_args, $hub_connect_url );
763 +
764 + // URL arguments for registration URL.
765 + // Do NOT REMOVE rawurlencode() site_connect_url — it is required.
766 + $register_args = array(
767 + 'signup' => 'site-connect',
768 + 'site_connect_url' => rawurlencode( $redirect_url ),
769 + 'utm_medium' => 'plugin',
770 + 'utm_source' => self::PLUGIN_IDENTIFIER,
771 + 'utm_campaign' => 'defender_firewall_antibot',
772 + 'utm_content' => 'hub-connector',
773 + );
774 +
775 + return add_query_arg( $register_args, $base_url );
776 + }
777 +
778 + /**
779 + * Get Hub forgot password URL.
780 + *
781 + * @return string
782 + */
783 + public static function get_hub_forgot_password_url(): string {
784 + $base_url = 'https://wpmudev.com/forgot-password';
785 +
786 + if ( class_exists( '\WPMUDEV\Hub\Connector\Data' ) ) {
787 + $base_url = \WPMUDEV\Hub\Connector\Data::get()->server_url( 'forgot-password' );
788 + }
789 +
790 + $hub_connect_url = self::get_hub_connector_callback_url( 'wp-defender', 'defender_forgot_password' );
791 + $auth_nonce = wp_create_nonce( 'auth_nonce' );
792 + $redirect_url = add_query_arg(
793 + array(
794 + 'hub_connector_callback' => 1,
795 + 'auth_nonce' => $auth_nonce,
796 + ),
797 + $hub_connect_url
798 + );
799 +
800 + // Do NOT rawurlencode() site_connect_url — add_query_arg() encodes values internally; double-encoding breaks the callback URL on the Hub side.
801 + return add_query_arg(
802 + array(
803 + 'site_connect_url' => $redirect_url,
804 + 'utm_medium' => 'plugin',
805 + 'utm_source' => self::PLUGIN_IDENTIFIER,
806 + 'utm_campaign' => 'defender_forgot_password',
807 + 'utm_content' => 'hub-connector',
808 + ),
809 + $base_url
810 + );
249 811 }
250 812 }