| @@ -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'; |
| @@ -212,48 +247,566 @@ | ||
| 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. |
| 214 | 249 | * @param string $utm_campaign Optional. The UTM campaign parameter. Default is empty string. |
| 215 | 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. | |
| 216 | 252 | * |
| 217 | 253 | * @return string |
| 218 | 254 | */ |
| 219 | - public function get_url( string $page = '', string $view = '', string $utm_campaign = '', string $utm_content = '' ): string { | |
| 220 | - if ( $this->is_dash_activated() ) { | |
| 221 | - $args = array( | |
| 222 | - 'page' => 'wpmudev', | |
| 223 | - 'utm_source' => self::PLUGIN_IDENTIFIER, | |
| 224 | - 'utm_medium' => 'plugin', | |
| 225 | - ); | |
| 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 ); | |
| 226 | 257 | |
| 227 | - $result = $this->maybe_summary_box_trigger( $view ); | |
| 228 | - // Get advanced params. | |
| 229 | - $res = $this->get_utm_tags( $page, $result['view'], $result['is_summary'] ); | |
| 258 | + if ( '' !== $view ) { | |
| 259 | + $args['view'] = $view; | |
| 260 | + } | |
| 230 | 261 | |
| 231 | - if ( '' !== $utm_campaign ) { | |
| 232 | - $args['utm_campaign'] = $utm_campaign; | |
| 262 | + $result = $this->maybe_summary_box_trigger( $view ); | |
| 263 | + // Get advanced params. | |
| 264 | + $res = $this->get_utm_tags( $page, $result['view'], $result['is_summary'] ); | |
| 233 | 265 | |
| 234 | - } elseif ( '' !== $res['utm_campaign'] ) { | |
| 235 | - $args['utm_campaign'] = $res['utm_campaign']; | |
| 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; | |
| 236 | 312 | } |
| 313 | + } | |
| 237 | 314 | |
| 238 | - if ( '' !== $utm_content ) { | |
| 239 | - $args['utm_content'] = $utm_content; | |
| 240 | - } elseif ( '' !== $res['utm_content'] ) { | |
| 241 | - $args['utm_content'] = $res['utm_content']; | |
| 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 »', '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> <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 | + } | |
| 242 | 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 | + } | |
| 243 | 540 | |
| 244 | - 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 ); | |
| 245 | 577 | } |
| 246 | 578 | |
| 247 | - $query = array( | |
| 248 | - '_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, | |
| 249 | 646 | ); |
| 250 | - if ( ! empty( $page ) ) { | |
| 251 | - $query['page'] = $page; | |
| 647 | + | |
| 648 | + if ( '' !== $module_slug ) { | |
| 649 | + $args['module_slug'] = sanitize_text_field( $module_slug ); | |
| 252 | 650 | } |
| 253 | - if ( ! empty( $view ) ) { | |
| 254 | - $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; | |
| 255 | 690 | } |
| 256 | 691 | |
| 257 | - 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 | + ); | |
| 258 | 811 | } |
| 259 | 812 | } |