PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.2.2
Defender Security – Malware Scanner, Login Security & Firewall v6.2.2
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 / component / class-hub-connector.php

class-hub-connector.php in Defender Security – Malware Scanner, Login Security & Firewall 6.2.2, at src/component/class-hub-connector.php

813 lines 27.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Hub Connector class.
4 *
5 * @package WP_Defender\Component
6 */
7
8 namespace WP_Defender\Component;
9
10 use WPMUDEV\Hub\Connector;
11 use Calotes\Base\Component;
12 use WP_Defender\Behavior\WPMUDEV;
13 use WP_Defender\Traits\Defender_Dashboard_Client;
14 use WPMUDEV\Hub\Connector\API;
15
16 /**
17 * Handles the functionality related to the Hub Connector module.
18 *
19 * @since 4.10.0
20 */
21 class Hub_Connector extends Component {
22 use Defender_Dashboard_Client;
23
24 /**
25 * The identifier for the WP Defender plugin in the Hub.
26 *
27 * @const string
28 */
29 public const PLUGIN_IDENTIFIER = 'defender';
30
31 /**
32 * The action name used for the Hub connection.
33 *
34 * @const string
35 */
36 public const CONNECTION_ACTION = 'hub_connection';
37
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 /**
46 * Checks if the Hub connector should render its UI.
47 *
48 * Verifies the nonce from the global-ip page to determine if the Hub connector should render its UI.
49 *
50 * @return bool The nonce value on success, false on failure.
51 */
52 public static function should_render() {
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 );
66 }
67
68 /**
69 * Initialize the Hub Connector module and set its options.
70 *
71 * The `extra/hub-connector/connector.php` file is required, and the options are set for the Hub Connector module.
72 *
73 * @return void
74 */
75 public function init() {
76 require_once defender_path( 'extra/hub-connector/connector.php' );
77 $options = array(
78 'screens' => array(
79 'toplevel_page_wp-defender',
80 'toplevel_page_wp-defender-network',
81 'defender_page_wdf-ip-lockout',
82 'defender_page_wdf-ip-lockout-network',
83 'defender-pro_page_wdf-ip-lockout',
84 'defender-pro_page_wdf-ip-lockout-network',
85 ),
86 'extra_args' => array(
87 'register' => array(
88 'connect_ref' => self::PLUGIN_IDENTIFIER,
89 'utm_medium' => 'plugin',
90 'utm_source' => self::PLUGIN_IDENTIFIER,
91 ),
92 ),
93 );
94 // Get advanced params.
95 $page = defender_get_data_from_request( 'page', 'g' );
96 $view = defender_get_data_from_request( 'view', 'g' );
97
98 $result = $this->maybe_summary_box_trigger( $view );
99 // Get advanced params.
100 $res = $this->get_utm_tags( $page, $result['view'], $result['is_summary'] );
101 if ( '' !== $res['utm_campaign'] ) {
102 $options['extra_args']['register']['utm_campaign'] = $res['utm_campaign'];
103 }
104 if ( '' !== $res['utm_content'] ) {
105 $options['extra_args']['register']['utm_content'] = $res['utm_content'];
106 }
107
108 Connector::get()->set_options( self::PLUGIN_IDENTIFIER, $options );
109 }
110
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 /**
126 * Gets the label for the button to connect the site to the Hub.
127 *
128 * @return string The label for the button.
129 */
130 public function get_button_label(): string {
131 if ( $this->is_dash_installed() && ! $this->is_dash_activated() ) {
132 return __( 'Activate WPMU DEV Dashboard', 'defender-security' );
133 }
134 if ( $this->is_dash_activated() ) {
135 return __( 'Log in to WPMU DEV', 'defender-security' );
136 }
137 return __( 'Connect site for Full protection', 'defender-security' );
138 }
139
140 /**
141 * Modify text string vars.
142 *
143 * @param array $texts Vars.
144 * @param string $plugin Plugin identifier.
145 *
146 * @return array
147 */
148 public static function customize_text_vars( $texts, $plugin ): array {
149 if ( self::PLUGIN_IDENTIFIER === $plugin ) {
150 $feature_name = self::get_feature_name();
151 $texts['create_account_desc'] = sprintf(
152 /* translators: 1. Feature name. 2. Opened tag. 3. Closed tag. */
153 esc_html__( 'Create a free account to connect your site to WPMU DEV and activate %1$s. %2$sIt`s fast, seamless, and free%3$s.', 'defender-security' ),
154 '<strong>' . __( 'Defender - ', 'defender-security' ) . $feature_name . '</strong>',
155 '<i>',
156 '</i>'
157 );
158 $texts['login_desc'] = sprintf(
159 /* translators: %s: Feature name. */
160 esc_html__( 'Log in with your WPMU DEV account credentials to activate %s.', 'defender-security' ),
161 $feature_name
162 );
163 }
164
165 return $texts;
166 }
167
168 /**
169 * Get feature's name.
170 *
171 * @return string
172 */
173 public static function get_feature_name(): string {
174 $feature_name = defender_get_data_from_request( 'module_name', 'g' );
175 if ( ! is_null( $feature_name ) && '' !== $feature_name ) {
176 return $feature_name;
177 }
178
179 $view = defender_get_data_from_request( 'view', 'g' );
180 switch ( $view ) {
181 case 'blocklist':
182 return __( 'Custom IP Allow/Block list', 'defender-security' );
183 default:
184 return __( 'AntiBot Global Firewall', 'defender-security' );
185 }
186 }
187
188 /**
189 * Get UTM tags.
190 *
191 * @param string $page The page to load.
192 * @param string $view The view to load.
193 * @param bool $is_summary Is this from the Summary section? Default false.
194 *
195 * @return array
196 */
197 private function get_utm_tags( string $page = '', string $view = '', bool $is_summary = false ): array {
198 $utm_campaign = '';
199 $utm_content = '';
200 if ( '' !== $page ) {
201 switch ( $page ) {
202 // There are buttons on notice or widget on the Dashboard page.
203 case 'wp-defender':
204 $utm_content = 'hub-connector';
205 $utm_campaign = ( 'dashboard' === $view )
206 ? 'defender_dashboard_firewall_antibot'
207 : 'defender_onboarding_antibot';
208 break;
209 case 'wdf-ip-lockout':
210 $utm_content = 'hub-connector';
211 if ( $is_summary ) {
212 $utm_campaign = 'defender_firewall_antibot_summary';
213 } else {
214 $utm_campaign = ( 'blocklist' === $view )
215 ? 'defender_firewall_centralip'
216 : 'defender_firewall_antibot';
217 }
218 break;
219 default:
220 break;
221 }
222 }
223
224 return array(
225 'utm_campaign' => $utm_campaign,
226 'utm_content' => $utm_content,
227 );
228 }
229
230 /**
231 * Update data if a trigger is the Summary section.
232 *
233 * @param string $view The view to load.
234 *
235 * @return array
236 */
237 private function maybe_summary_box_trigger( string $view ): array {
238 return array(
239 'view' => 'summary-box' === $view ? 'global-ip' : $view,
240 'is_summary' => 'summary-box' === $view,
241 );
242 }
243
244 /**
245 * Retrieve the Hub connector URL.
246 *
247 * @param string $page Optional. The page to load. Default is empty string.
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.
252 *
253 * @return string
254 */
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 );
257
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;
312 }
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 }
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 }
540
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 );
577 }
578
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,
646 );
647
648 if ( '' !== $module_slug ) {
649 $args['module_slug'] = sanitize_text_field( $module_slug );
650 }
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;
690 }
691
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 );
811 }
812 }
813