PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / 6.2.2
MainWP Dashboard: Self-hosted WordPress Management for Agencies v6.2.2
6.2.2 6.2.1 6.2 6.1.8 6.1.7 6.1.6 6.1.5 6.1.4 6.1.3 6.1.2 6.1.1 6.1 6.0.12 6.0.11 4.6.0.1 5.0 5.0.1 5.0.2 5.0.3 5.0.3.1 5.0.3.2 5.1 5.1.1 5.2 5.2.1 All 155 releases
← All changes | class/class-mainwp-connect.php +574 -152 6.0.11 → 6.2.2 View file →
@@ -205,10 +205,15 @@
205 205 $host = wp_parse_url( ( empty( $realurl ) ? $url : $realurl ), PHP_URL_HOST );
206 206 $ip = false;
207 207 $target = false;
208 208
209 + // Ask only for the record types read below. dns_get_record() defaults to DNS_ANY, which
210 + // most resolvers now refuse or answer with a stub (RFC 8482), so it buys retries and
211 + // timeouts instead of answers. Names that exist only in the hosts file are not resolved
212 + // here at all -- dns_get_record() never reads the hosts file -- they fall through to the
213 + // gethostbynamel() call below.
209 214 $found = false;
210 - $dnsRecord = @dns_get_record( $host );
215 + $dnsRecord = @dns_get_record( $host, DNS_A | DNS_AAAA | DNS_CNAME );
211 216 MainWP_Logger::instance()->debug( ' :: tryVisit :: [dnsRecord=' . MainWP_Utility::value_to_string( $dnsRecord, 1 ) . ']' );
212 217
213 218 if ( false !== $dnsRecord && is_array( $dnsRecord ) ) {
214 219 if ( ! isset( $dnsRecord['ip'] ) ) {
@@ -371,11 +376,14 @@
371 376 if ( is_object( $website ) && isset( $website->url ) ) {
372 377 $url = $website->url;
373 378 $verifyCertificate = isset( $website->verify_certificate ) ? (int) $website->verify_certificate : null;
374 379 $forceUseIPv4 = $website->force_use_ipv4;
375 - $http_user = $website->http_user;
376 - $http_pass = $website->http_pass;
377 - $sslVersion = $website->ssl_version;
380 + // MWP-1548: decrypt at the boundary so HTTP Basic Auth gets
381 + // the plaintext credentials. Legacy plaintext rows pass
382 + // through unchanged via the helper's fallback.
383 + $http_user = MainWP_Credential_Storage::decrypt_credential( $website->http_user );
384 + $http_pass = MainWP_Credential_Storage::decrypt_credential( $website->http_pass );
385 + $sslVersion = $website->ssl_version;
378 386 } else {
379 387 $url = $website;
380 388 }
381 389
@@ -404,20 +412,33 @@
404 412 *
405 413 * @param mixed $website Array of Child Site Info.
406 414 * @param mixed $what What we are posting.
407 415 * @param null $params Post parameters.
416 + * @param array $others Other data.
408 417 *
409 418 * @return mixed null|http_build_query()
410 419 */
411 - public static function get_post_data_authed( &$website, $what, $params = null ) { //phpcs:ignore -- NOSONAR - complex method.
420 + public static function get_post_data_authed( &$website, $what, $params = null, $others = array() ) { //phpcs:ignore -- NOSONAR - complex method.
421 +
422 + if ( ! is_array( $others ) ) {
423 + $others = array();
424 + }
425 +
426 + $verify_signature = ! empty( $others['verify_signature'] ) ? true : false;
427 + $http_user_plain = ! empty( $others['http_user_plain'] ) ? $others['http_user_plain'] : '';
428 + $http_pass_plain = ! empty( $others['http_pass_plain'] ) ? $others['http_pass_plain'] : '';
429 +
412 430 if ( $website && '' !== $what ) {
431 + // The nonce must never repeat. The Child stores every legacy signature it accepts, forever,
432 + // and refuses a repeat as "This request has already been used"; signing is deterministic,
433 + // so the nonce is the only thing that makes two requests for the same action differ.
413 434 $data = array();
414 435 $data['user'] = $website->adminname;
415 436 $data['function'] = $what;
416 - $data['nonce'] = wp_rand( 0, 9999 );
437 + $data['nonce'] = bin2hex( random_bytes( 16 ) );
417 438 $data['mainwpver'] = MainWP_System::$version;
418 439
419 - $params_filter = apply_filters( 'mainwp_pre_fetch_authed_data', false, $params, $what, $website );
440 + $params_filter = apply_filters( 'mainwp_pre_fetch_authed_data', false, $params, $what, $website, $verify_signature );
420 441 if ( is_array( $params_filter ) && ! empty( $params_filter ) ) {
421 442 $data = array_merge( $data, $params_filter );
422 443 }
423 444
@@ -428,48 +449,12 @@
428 449 $alg = false;
429 450 $sign_success = null;
430 451 $use_seclib = false;
431 452
432 - $data = apply_filters( 'mainwp_get_post_data_authed', $data, $website, $what, $params );
433 - if ( MainWP_Connect_Lib::is_use_fallback_sec_lib( $website ) ) {
434 - $sign_success = MainWP_Connect_Lib::connect_sign( $what . $data['nonce'], $signature, base64_decode( $website->privkey ), $website->id ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
435 - $use_seclib = true;
436 - } elseif ( function_exists( 'openssl_verify' ) ) {
437 - $alg = MainWP_System_Utility::get_connect_sign_algorithm( $website );
438 - $sign_success = static::connect_sign( $what . $data['nonce'], $signature, base64_decode( $website->privkey ), $alg, $website->id ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
439 - if ( false !== $alg ) {
440 - $data['sign_algo'] = $alg;
441 - }
442 - }
453 + $data = apply_filters( 'mainwp_get_post_data_authed', $data, $website, $what, $params, $verify_signature );
443 454
444 - if ( $use_seclib ) {
445 - $data['verifylib'] = 1;
446 - }
455 + $alt_user = '';
447 456
448 - if ( null !== $sign_success && empty( $sign_success ) ) {
449 - $sign_error = '';
450 - while ( $msg = openssl_error_string() ) {
451 - if ( is_string( $msg ) ) {
452 - $sign_error .= $msg;
453 - }
454 - }
455 - $pk_info = ! empty( $website->privkey ) ? substr( $website->privkey, 0, 10 ) : '';
456 - MainWP_Logger::instance()->warning_for_website( $website, 'CONNECT SIGN', 'FAILED :: [what=' . ( is_string( $what ) ? $what : '' ) . '] :: [seclib=' . intval( $use_seclib ) . '] :: [algorithm=' . $alg . '] :: [openssl_sign error =' . $sign_error . '] :: [pkey start =' . $pk_info . '...]', false );
457 - }
458 -
459 - $data['mainwpsignature'] = ! empty( $signature ) ? base64_encode( $signature ) : ''; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
460 -
461 - /** This filter is documented in ../widgets/widget-mainwp-recent-posts.php */
462 - $recent_number = apply_filters( 'mainwp_recent_posts_pages_number', 5 );
463 - if ( 5 !== $recent_number ) {
464 - $data['recent_number'] = $recent_number;
465 - }
466 -
467 - $scan_dir = apply_filters( 'mainwp_stats_scan_dir', false, $website );
468 - if ( ! empty( $scan_dir ) ) {
469 - $data['scan_dir'] = 1;
470 - }
471 -
472 457 /**
473 458 * Current user global.
474 459 *
475 460 * @global string
@@ -486,14 +471,125 @@
486 471 * @param int $current_user->ID User ID.
487 472 *
488 473 * @since Unknown
489 474 */
490 - $alter_user = apply_filters( 'mainwp_alter_login_user', false, $website->id, $current_user->ID );
491 - if ( ! empty( $alter_user ) ) {
492 - $data['alt_user'] = rawurlencode( $alter_user );
475 + $alt_user = apply_filters( 'mainwp_alter_login_user', false, $website->id, $current_user->ID );
476 +
477 + }
478 +
479 + $child_support_adv_sign = 1 === (int) MainWP_DB::instance()->get_website_option( $website, 'support_advanced_sign' );
480 +
481 + $alg = false;
482 + $sign_value = $what . $data['nonce']; // Legacy signature data.
483 +
484 + if ( ! $verify_signature || ! $child_support_adv_sign ) {
485 + if ( MainWP_Connect_Lib::is_use_fallback_sec_lib( $website ) ) {
486 + $sign_success = MainWP_Connect_Lib::connect_sign( $sign_value, $signature, base64_decode( $website->privkey ), $website->id ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
487 + $use_seclib = true;
488 + } elseif ( function_exists( 'openssl_verify' ) ) {
489 + $alg = MainWP_System_Utility::get_connect_sign_algorithm( $website );
490 + $sign_success = static::connect_sign( $sign_value, $signature, base64_decode( $website->privkey ), $alg, $website->id ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
491 + if ( false !== $alg ) {
492 + $data['sign_algo'] = $alg;
493 + }
493 494 }
495 +
496 + if ( null !== $sign_success && empty( $sign_success ) ) {
497 + $sign_error = '';
498 + while ( $msg = openssl_error_string() ) {
499 + if ( is_string( $msg ) ) {
500 + $sign_error .= $msg;
501 + }
502 + }
503 + $pk_info = ! empty( $website->privkey ) ? substr( $website->privkey, 0, 10 ) : '';
504 + MainWP_Logger::instance()->warning_for_website( $website, 'CONNECT SIGN', 'FAILED :: [what=' . ( is_string( $what ) ? $what : '' ) . '] :: [seclib=' . intval( $use_seclib ) . '] :: [algorithm=' . $alg . '] :: [openssl_sign error =' . $sign_error . '] :: [pkey start =' . $pk_info . '...]', false );
505 + }
506 + } else {
507 + $signature = 'useadvancedmainwpsignature';
494 508 }
495 509
510 + $data['mainwpsignature'] = ! empty( $signature ) ? base64_encode( $signature ) : ''; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
511 +
512 + if ( $verify_signature ) {
513 + $ts = time();
514 + $data_sign_v2 = array(
515 + 'base_function' => $what,
516 + 'nonce' => $data['nonce'],
517 + 'expires' => $ts + 60,
518 + 'user' => $website->adminname,
519 + 'req_id' => wp_generate_uuid4(),
520 + );
521 +
522 + if ( ! empty( $alt_user ) ) {
523 + $data_sign_v2['alt_user'] = rawurlencode( $alt_user );
524 + }
525 +
526 + if ( 'process_premium_updates' === $what ) {
527 + $add_sign_params = array( 'premium_perform', 'premium_type', 'list' );
528 + foreach ( $add_sign_params as $_name ) {
529 + if ( isset( $params[ $_name ] ) ) {
530 + $data_sign_v2[ $_name ] = $params[ $_name ];
531 + }
532 + }
533 + }
534 +
535 + $sign_success_v2 = null;
536 +
537 + $sign_value_v2 = wp_json_encode( $data_sign_v2 );
538 +
539 + if ( MainWP_Connect_Lib::is_use_fallback_sec_lib( $website ) ) {
540 + $use_seclib = true;
541 + $sign_success_v2 = MainWP_Connect_Lib::connect_sign( $sign_value_v2, $signature_v2, base64_decode( $website->privkey ), $website->id ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
542 + } elseif ( function_exists( 'openssl_verify' ) ) {
543 + $alg = MainWP_System_Utility::get_connect_sign_algorithm( $website );
544 + $sign_success_v2 = static::connect_sign( $sign_value_v2, $signature_v2, base64_decode( $website->privkey ), $alg, $website->id ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
545 + if ( false !== $alg ) {
546 + $data['sign_algo'] = $alg;
547 + }
548 + }
549 +
550 + $data['data_signature'] = $sign_value_v2;
551 + $data['mainwpsignature_adv'] = ! empty( $signature_v2 ) ? base64_encode( $signature_v2 ) : ''; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
552 +
553 + if ( null !== $sign_success_v2 && empty( $sign_success_v2 ) ) {
554 + $sign_error = '';
555 + while ( $msg = openssl_error_string() ) {
556 + if ( is_string( $msg ) ) {
557 + $sign_error .= $msg;
558 + }
559 + }
560 + $pk_info = ! empty( $website->privkey ) ? substr( $website->privkey, 0, 10 ) : '';
561 + MainWP_Logger::instance()->warning_for_website( $website, 'CONNECT ADV SIGN', 'FAILED :: [what=' . ( is_string( $what ) ? $what : '' ) . '] :: [seclib=' . intval( $use_seclib ) . '] :: [algorithm=' . $alg . '] :: [openssl_sign error =' . $sign_error . '] :: [pkey start =' . $pk_info . '...]', false );
562 + }
563 + }
564 +
565 + if ( $use_seclib ) {
566 + $data['verifylib'] = 1;
567 + }
568 +
569 + if ( ! empty( $alt_user ) ) {
570 + $data['alt_user'] = rawurlencode( $alt_user );
571 + }
572 +
573 + /** This filter is documented in ../widgets/widget-mainwp-recent-posts.php */
574 + $recent_number = apply_filters( 'mainwp_recent_posts_pages_number', 5 );
575 + if ( 5 !== $recent_number ) {
576 + $data['recent_number'] = $recent_number;
577 + }
578 +
579 + $scan_dir = apply_filters( 'mainwp_stats_scan_dir', false, $website );
580 + if ( ! empty( $scan_dir ) ) {
581 + $data['scan_dir'] = 1;
582 + }
583 +
584 + if ( 'process_premium_updates' === $what ) {
585 + if ( ! empty( $http_user_plain ) && ! empty( $http_pass_plain ) ) {
586 + // For post data.
587 + $data['wp_http_user'] = $http_user_plain;
588 + $data['wp_http_pass'] = $http_pass_plain;
589 + }
590 + $data['sslVerify'] = $website->verify_certificate ? 1 : 0;
591 + }
496 592 return http_build_query( $data, '', '&' );
497 593 }
498 594
499 595 return null;
@@ -505,20 +601,29 @@
505 601 * Get authorized $_POST data & build query for renew connection action only.
506 602 *
507 603 * @param mixed $website Array of Child Site Info.
508 604 * @param mixed $what What we are posting.
605 + * @param array $others Other data.
509 606 *
510 607 * @return mixed null|http_build_query()
511 608 */
512 - private static function get_renew_post_data_authed( &$website, $what ) { // phpcs:ignore -- NOSONAR - complex.
609 + private static function get_renew_post_data_authed( &$website, $what, $others = array() ) { // phpcs:ignore -- NOSONAR - complex.
513 610
611 + if ( ! is_array( $others ) ) {
612 + $others = array();
613 + }
614 +
615 + $verify_signature = ! empty( $others['verify_signature'] ) ? true : false;
616 +
514 617 if ( $website && '' !== $what ) {
515 618 $compat_what = 'disconnect'; // to compatible, renew will call disconnect.
516 619 $data = array();
517 620 $data['user'] = $website->adminname;
518 621 $data['function'] = $compat_what;
519 - $data['nonce'] = wp_rand( 0, 9999 );
622 + $data['nonce'] = bin2hex( random_bytes( 16 ) );
520 623
624 + $sign_value = $compat_what . $data['nonce']; // compatible format.
625 +
521 626 $alg = false;
522 627 $sign_success = null;
523 628 $use_seclib = false;
524 629
@@ -523,17 +628,17 @@
523 628 $use_seclib = false;
524 629
525 630 if ( MainWP_Connect_Lib::is_use_fallback_sec_lib( $website ) ) {
526 631 // to disconnect.
527 - $sign_success = MainWP_Connect_Lib::connect_sign( $compat_what . $data['nonce'], $signature, base64_decode( $website->privkey ), $website->id ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
632 + $sign_success = MainWP_Connect_Lib::connect_sign( $sign_value, $signature, base64_decode( $website->privkey ), $website->id ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
528 633 $use_seclib = true;
529 634 } elseif ( function_exists( 'openssl_verify' ) ) {
530 635 $alg = MainWP_System_Utility::get_connect_sign_algorithm( $website );
531 - $sign_success = static::connect_sign( $compat_what . $data['nonce'], $signature, base64_decode( $website->privkey ), $alg, $website->id ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for keys encoding.
636 + $sign_success = static::connect_sign( $sign_value, $signature, base64_decode( $website->privkey ), $alg, $website->id ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for keys encoding.
532 637 if ( empty( $sign_success ) ) { // error from openssl, openssl_sign().
533 638 $alg = defined( 'OPENSSL_ALGO_SHA1' ) ? OPENSSL_ALGO_SHA1 : false; // to set default SHA1, to disconnect.
534 639 MainWP_Logger::instance()->debug_for_website( $website, 'get_renew_post_data_authed', '[' . $website->url . '] :: [openssl_sign:failed] :: Set sign_algo=SHA1' );
535 - $sign_success = static::connect_sign( $compat_what . $data['nonce'], $signature, base64_decode( $website->privkey ), $alg, $website->id ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for keys encoding.
640 + $sign_success = static::connect_sign( $sign_value, $signature, base64_decode( $website->privkey ), $alg, $website->id ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for keys encoding.
536 641 }
537 642
538 643 if ( false !== $alg ) {
539 644 $data['sign_algo'] = $alg;
@@ -539,11 +644,9 @@
539 644 $data['sign_algo'] = $alg;
540 645 }
541 646 }
542 647
543 - if ( $use_seclib ) {
544 - $data['verifylib'] = 1;
545 - }
648 + $data['mainwpsignature'] = ! empty( $signature ) ? base64_encode( $signature ) : ''; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
546 649
547 650 if ( null !== $sign_success && empty( $sign_success ) ) {
548 651 $sign_error = '';
549 652 while ( $msg = openssl_error_string() ) {
@@ -553,10 +656,55 @@
553 656 }
554 657 MainWP_Logger::instance()->warning_for_website( $website, 'CONNECT SIGN', 'FAILED :: [what=' . ( is_string( $what ) ? $what : '' ) . '] :: [seclib=' . intval( $use_seclib ) . '] :: [algorithm=' . $alg . '] :: [openssl_sign error =' . $sign_error . ']', false );
555 658 }
556 659
557 - $data['mainwpsignature'] = ! empty( $signature ) ? base64_encode( $signature ) : ''; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
660 + if ( $verify_signature ) {
661 + $ts = time();
662 + $data_sign_v2 = array(
663 + 'base_function' => $compat_what,
664 + 'nonce' => $data['nonce'],
665 + 'expires' => $ts + 60,
666 + 'user' => $website->adminname,
667 + 'req_id' => wp_generate_uuid4(),
668 + );
669 + $sign_value_v2 = wp_json_encode( $data_sign_v2 );
558 670
671 + $sign_success_v2 = null;
672 +
673 + if ( MainWP_Connect_Lib::is_use_fallback_sec_lib( $website ) ) {
674 + // to disconnect.
675 + $sign_success_v2 = MainWP_Connect_Lib::connect_sign( $sign_value_v2, $signature_v2, base64_decode( $website->privkey ), $website->id ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
676 + } elseif ( function_exists( 'openssl_verify' ) ) {
677 + $alg = MainWP_System_Utility::get_connect_sign_algorithm( $website );
678 + $sign_success_v2 = static::connect_sign( $sign_value_v2, $signature_v2, base64_decode( $website->privkey ), $alg, $website->id ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for keys encoding.
679 + if ( empty( $sign_success_v2 ) ) { // error from openssl, openssl_sign().
680 + $alg = defined( 'OPENSSL_ALGO_SHA1' ) ? OPENSSL_ALGO_SHA1 : false; // to set default SHA1, to disconnect.
681 + MainWP_Logger::instance()->debug_for_website( $website, 'get_renew_post_data_authed', '[' . $website->url . '] :: [openssl_sign:failed] :: Set sign_algo=SHA1' );
682 + $sign_success_v2 = static::connect_sign( $sign_value_v2, $signature_v2, base64_decode( $website->privkey ), $alg, $website->id ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for keys encoding.
683 + }
684 + if ( false !== $alg ) {
685 + $data['sign_algo'] = $alg;
686 + }
687 + }
688 +
689 + $data['data_signature'] = $sign_value_v2;
690 + $data['mainwpsignature_adv'] = ! empty( $signature_v2 ) ? base64_encode( $signature_v2 ) : ''; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
691 +
692 + if ( null !== $sign_success_v2 && empty( $sign_success_v2 ) ) {
693 + $sign_error = '';
694 + while ( $msg = openssl_error_string() ) {
695 + if ( is_string( $msg ) ) {
696 + $sign_error .= $msg;
697 + }
698 + }
699 + MainWP_Logger::instance()->warning_for_website( $website, 'CONNECT ADV SIGN', 'FAILED :: [what=' . ( is_string( $what ) ? $what : '' ) . '] :: [seclib=' . intval( $use_seclib ) . '] :: [algorithm=' . $alg . '] :: [openssl_sign error =' . $sign_error . ']', false );
700 + }
701 + }
702 +
703 + if ( $use_seclib ) {
704 + $data['verifylib'] = 1;
705 + }
706 +
559 707 return http_build_query( $data, '', '&' );
560 708 }
561 709 return null;
562 710 }
@@ -571,12 +719,16 @@
571 719 * @param mixed $paramValue OpenSSL parameter.
572 720 * @param string $paramName Parameter name.
573 721 * @param bool $asArray true|false Default is false.
574 722 * @param array $other_params other params.
723 + * @param string $custom_url Optional. Override the target base URL for browser-bound
724 + * requests ( see MainWP_Site_Url_Corrector::browser_target_url() ).
725 + * Default null keeps the stored URL � server-side callers
726 + * ( backups, premium updates ) must not pass this.
575 727 *
576 728 * @return string $url
577 729 */
578 - public static function get_get_data_authed( $website, $paramValue, $paramName = 'where', $asArray = false, $other_params = array() ) { //phpcs:ignore -- NOSONAR - complex method.
730 + public static function get_get_data_authed( $website, $paramValue, $paramName = 'where', $asArray = false, $other_params = array(), $custom_url = null ) { //phpcs:ignore -- NOSONAR - complex method.
579 731 $params = array();
580 732 if ( $website && '' !== $paramValue ) {
581 733
582 734 $sign_success = null;
@@ -581,27 +733,51 @@
581 733
582 734 $sign_success = null;
583 735 $alg = false;
584 736 $use_seclib = false;
585 - $nonce = wp_rand( 0, 9999 );
586 - if ( MainWP_Connect_Lib::is_use_fallback_sec_lib( $website ) ) {
587 - $sign_success = MainWP_Connect_Lib::connect_sign( $paramValue . $nonce, $signature, base64_decode( $website->privkey ), $website->id ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
588 - $use_seclib = true;
589 - } elseif ( function_exists( 'openssl_verify' ) ) {
590 - $alg = MainWP_System_Utility::get_connect_sign_algorithm( $website );
591 - $sign_success = static::connect_sign( $paramValue . $nonce, $signature, base64_decode( $website->privkey ), $alg, $website->id ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
737 + $nonce = bin2hex( random_bytes( 16 ) );
738 +
739 + /**
740 + * Current user global.
741 + *
742 + * @global string
743 + */
744 + global $current_user;
745 +
746 + $alt_user = '';
747 + if ( ( ( ! defined( 'DOING_CRON' ) || false === DOING_CRON ) && ( ! defined( 'WP_CLI' ) || false === WP_CLI ) ) && $current_user && $current_user->ID ) {
748 + /** This filter is documented in ../class/class-mainwp-connect.php */
749 + $alt_user = apply_filters( 'mainwp_alter_login_user', false, $website->id, $current_user->ID );
592 750 }
593 751
594 - $signature = ! empty( $signature ) ? base64_encode( $signature ) : ''; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
752 + $verify_signature = is_array( $other_params ) && ! empty( $other_params['verify_signature'] );
595 753
596 - if ( null !== $sign_success && empty( $sign_success ) ) {
597 - $sign_error = '';
598 - while ( $msg = openssl_error_string() ) {
599 - if ( is_string( $msg ) ) {
600 - $sign_error .= $msg;
754 + $child_support_adv_sign = 1 === (int) MainWP_DB::instance()->get_website_option( $website, 'support_advanced_sign' );
755 +
756 + if ( ! $verify_signature || ! $child_support_adv_sign ) {
757 + $sign_value = $paramValue . $nonce; // compatible format.
758 +
759 + if ( MainWP_Connect_Lib::is_use_fallback_sec_lib( $website ) ) {
760 + $sign_success = MainWP_Connect_Lib::connect_sign( $sign_value, $signature, base64_decode( $website->privkey ), $website->id ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
761 + $use_seclib = true;
762 + } elseif ( function_exists( 'openssl_verify' ) ) {
763 + $alg = MainWP_System_Utility::get_connect_sign_algorithm( $website );
764 + $sign_success = static::connect_sign( $sign_value, $signature, base64_decode( $website->privkey ), $alg, $website->id ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
765 + }
766 +
767 + $signature = ! empty( $signature ) ? base64_encode( $signature ) : ''; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
768 +
769 + if ( null !== $sign_success && empty( $sign_success ) ) {
770 + $sign_error = '';
771 + while ( $msg = openssl_error_string() ) {
772 + if ( is_string( $msg ) ) {
773 + $sign_error .= $msg;
774 + }
601 775 }
776 + MainWP_Logger::instance()->warning_for_website( $website, 'CONNECT SIGN', 'FAILED :: [login_required=1] :: [seclib=' . intval( $use_seclib ) . '] :: [algorithm=' . $alg . '] :: [openssl_sign error =' . $sign_error . ']', false );
602 777 }
603 - MainWP_Logger::instance()->warning_for_website( $website, 'CONNECT SIGN', 'FAILED :: [login_required=1] :: [seclib=' . intval( $use_seclib ) . '] :: [algorithm=' . $alg . '] :: [openssl_sign error =' . $sign_error . ']', false );
778 + } else {
779 + $signature = base64_encode( 'useadvancedmainwpsignature' ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
604 780 }
605 781
606 782 $params = array(
607 783 'login_required' => 1,
@@ -610,8 +786,62 @@
610 786 'nonce' => $nonce,
611 787 $paramName => rawurlencode( $paramValue ),
612 788 );
613 789
790 + if ( false !== $alg ) {
791 + $params['sign_algo'] = $alg;
792 + }
793 +
794 + if ( $verify_signature ) {
795 + $ts = time();
796 + $data_sign_v2 = array(
797 + 'base_function' => $paramValue,
798 + 'where' => rawurlencode( $paramName ),
799 + 'nonce' => $nonce,
800 + 'expires' => $ts + 60,
801 + 'user' => $website->adminname,
802 + 'req_id' => wp_generate_uuid4(),
803 + );
804 +
805 + if ( ! empty( $alt_user ) ) {
806 + $data_sign_v2['alt_user'] = rawurlencode( $alt_user );
807 + }
808 +
809 + $sign_success_v2 = null;
810 +
811 + $sign_value_v2 = wp_json_encode( $data_sign_v2 );
812 +
813 + if ( MainWP_Connect_Lib::is_use_fallback_sec_lib( $website ) ) {
814 + $sign_success_v2 = MainWP_Connect_Lib::connect_sign( $sign_value_v2, $signature_v2, base64_decode( $website->privkey ), $website->id ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
815 + $use_seclib = true;
816 + } elseif ( function_exists( 'openssl_verify' ) ) {
817 + $alg = MainWP_System_Utility::get_connect_sign_algorithm( $website );
818 + $sign_success_v2 = static::connect_sign( $sign_value_v2, $signature_v2, base64_decode( $website->privkey ), $alg, $website->id ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
819 + if ( false !== $alg ) {
820 + $params['sign_algo'] = $alg;
821 + }
822 + }
823 +
824 + $signature_v2 = ! empty( $signature_v2 ) ? base64_encode( $signature_v2 ) : ''; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
825 +
826 + $params['data_signature'] = rawurlencode( $sign_value_v2 );
827 + $params['mainwpsignature_adv'] = rawurlencode( $signature_v2 );
828 +
829 + if ( null !== $sign_success_v2 && empty( $sign_success_v2 ) ) {
830 + $sign_error = '';
831 + while ( $msg = openssl_error_string() ) {
832 + if ( is_string( $msg ) ) {
833 + $sign_error .= $msg;
834 + }
835 + }
836 + MainWP_Logger::instance()->warning_for_website( $website, 'CONNECT ADV SIGN', 'FAILED :: [login_required=1] :: [seclib=' . intval( $use_seclib ) . '] :: [algorithm=' . $alg . '] :: [openssl_sign error =' . $sign_error . ']', false );
837 + }
838 + }
839 +
840 + if ( ! empty( $alt_user ) ) {
841 + $params['alt_user'] = rawurlencode( $alt_user );
842 + }
843 +
614 844 if ( is_array( $other_params ) ) {
615 845 foreach ( $other_params as $name => $value ) {
616 846 if ( is_string( $name ) && ! empty( $name ) && is_scalar( $value ) ) {
617 847 $params[ sanitize_text_field( wp_unslash( $name ) ) ] = rawurlencode( sanitize_text_field( wp_unslash( $value ) ) );
@@ -631,30 +861,11 @@
631 861 }
632 862 }
633 863 }
634 864
635 - if ( false !== $alg ) {
636 - $params['sign_algo'] = $alg;
637 - }
638 -
639 865 if ( ! empty( $use_seclib ) ) {
640 866 $params['verifylib'] = 1;
641 867 }
642 -
643 - /**
644 - * Current user global.
645 - *
646 - * @global string
647 - */
648 - global $current_user;
649 -
650 - if ( ( ( ! defined( 'DOING_CRON' ) || false === DOING_CRON ) && ( ! defined( 'WP_CLI' ) || false === WP_CLI ) ) && $current_user && $current_user->ID ) {
651 - /** This filter is documented in ../class/class-mainwp-connect.php */
652 - $alter_user = apply_filters( 'mainwp_alter_login_user', false, $website->id, $current_user->ID );
653 - if ( ! empty( $alter_user ) ) {
654 - $params['alt_user'] = rawurlencode( $alter_user );
655 - }
656 - }
657 868 }
658 869
659 870 if ( $asArray ) {
660 871 return $params;
@@ -659,9 +870,13 @@
659 870 if ( $asArray ) {
660 871 return $params;
661 872 }
662 873
663 - $url = ( isset( $website->url ) && '' !== $website->url ? $website->url : $website->siteurl );
874 + if ( null !== $custom_url && '' !== $custom_url ) {
875 + $url = $custom_url;
876 + } else {
877 + $url = ( isset( $website->url ) && '' !== $website->url ? $website->url : $website->siteurl );
878 + }
664 879 $url .= ( substr( $url, - 1 ) !== '/' ? '/' : '' );
665 880 $url .= '?';
666 881
667 882 foreach ( $params as $key => $value ) {
@@ -729,8 +944,27 @@
729 944 return null;
730 945 }
731 946
732 947 /**
948 + * Format a privacy-safe diagnostic for an unexpected Child response.
949 + *
950 + * @param mixed $website Website information.
951 + * @param mixed $data Raw response data.
952 + *
953 + * @return string Redacted diagnostic message.
954 + */
955 + private static function format_unexpected_response_log( $website, $data ) {
956 + $site_id = 0;
957 + if ( is_object( $website ) && property_exists( $website, 'id' ) && 0 < (int) $website->id ) {
958 + $site_id = (int) $website->id;
959 + }
960 +
961 + $response_bytes = is_string( $data ) ? strlen( $data ) : 0;
962 +
963 + return 'curl_multi_getcontent :: unexpected response :: [siteid=' . $site_id . '] :: [response_bytes=' . $response_bytes . ']';
964 + }
965 +
966 + /**
733 967 * Method fetch_urls_authed()
734 968 *
735 969 * Fetches data from child sites if authenticated.
736 970 *
@@ -775,9 +1009,26 @@
775 1009
776 1010 $agent = 'Mozilla/5.0 (compatible; MainWP/' . MainWP_System::$version . '; +http://mainwp.com)';
777 1011 $mh = curl_multi_init();
778 1012
779 - $timeout = 20 * 60 * 60;
1013 + /**
1014 + * Filter: mainwp_fetch_url_site_timeout
1015 + *
1016 + * Filters the request timeout ( CURLOPT_TIMEOUT + PHP time limit ) used
1017 + * for child site requests. Defaults to 20 hours to accommodate the
1018 + * longest operations ( upgrades, backups ); short-lived callers such as
1019 + * the URL-correction verify probe bound it much tighter.
1020 + *
1021 + * @param int $timeout Timeout in seconds. Default 72000 ( 20 hours ).
1022 + * Values below 1 are ignored ( 0 would disable the
1023 + * cURL timeout entirely ) and fall back to the default.
1024 + *
1025 + * @since 6.2
1026 + */
1027 + $timeout = (int) apply_filters( 'mainwp_fetch_url_site_timeout', 20 * 60 * 60 );
1028 + if ( $timeout <= 0 ) {
1029 + $timeout = 20 * 60 * 60;
1030 + }
780 1031
781 1032 $disabled_functions = ini_get( 'disable_functions' );
782 1033 $handleToWebsite = array();
783 1034 $requestUrls = array();
@@ -811,13 +1062,16 @@
811 1062 } else {
812 1063 $url .= 'admin-ajax.php';
813 1064 }
814 1065
1066 + $http_user = null;
1067 + $http_pass = null;
1068 +
815 1069 if ( property_exists( $website, 'http_user' ) ) {
816 - $http_user = $website->http_user;
1070 + $http_user = MainWP_Credential_Storage::decrypt_credential( $website->http_user );
817 1071 }
818 1072 if ( property_exists( $website, 'http_pass' ) ) {
819 - $http_pass = $website->http_pass;
1073 + $http_pass = MainWP_Credential_Storage::decrypt_credential( $website->http_pass );
820 1074 }
821 1075
822 1076 if ( isset( $params ) && isset( $params['new_post'] ) ) {
823 1077
@@ -880,9 +1134,10 @@
880 1134 // to fix.
881 1135 if ( defined( 'LOGGED_IN_SALT' ) && defined( 'NONCE_SALT' ) ) {
882 1136 $cookie_salt = sha1( sha1( 'mainwp' . LOGGED_IN_SALT . $website->id ) . NONCE_SALT . 'WP_Cookie' ); // NOSONAR - safe for salt file name.
883 1137 } else {
884 - $cookie_salt = sha1( sha1( 'mainwp' . $website->id ) . 'WP_Cookie' ); // NOSONAR - safe for salt file name.
1138 + // MWP-1558: misconfigured WP installs (no salts) previously used unsalted SHA1, which is enumerable. Fall back to the per-install MainWP filename secret instead.
1139 + $cookie_salt = MainWP_System_Utility::get_private_filename( 'cookies', $website->id, 'WP_Cookie' );
885 1140 }
886 1141 $cookieFile = $cookieDir . '/' . $cookie_salt;
887 1142 if ( ! file_exists( $cookieFile ) ) {
888 1143 @file_put_contents( $cookieFile, '' );
@@ -964,10 +1219,12 @@
964 1219 }
965 1220 }
966 1221
967 1222 if ( empty( $disabled_functions ) || ( false === stristr( $disabled_functions, 'curl_multi_exec' ) ) ) {
968 - $lastRun = 0;
1223 + $lastRun = 0;
1224 + $retry_added = false;
969 1225 do {
1226 + $retry_added = false;
970 1227 if ( 20 < time() - $lastRun ) {
971 1228 MainWP_System_Utility::set_time_limit( $timeout );
972 1229 $lastRun = time();
973 1230 }
@@ -983,13 +1240,27 @@
983 1240 if ( ! $contains && isset( $requestUrls[ $rid ] ) ) {
984 1241 curl_setopt( $info['handle'], CURLOPT_URL, $requestUrls[ $rid ] );
985 1242 curl_setopt( $info['handle'], CURLOPT_FRESH_CONNECT, true );
986 1243 curl_setopt( $info['handle'], CURLOPT_FORBID_REUSE, true );
987 - curl_multi_add_handle( $mh, $info['handle'] );
988 - unset( $requestUrls[ $rid ] );
989 - continue; // libcurl updates $running automatically.
1244 + $add_retry = curl_multi_add_handle( $mh, $info['handle'] );
1245 + if ( CURLM_OK === $add_retry ) {
1246 + $mrc = curl_multi_exec( $mh, $running );
1247 +
1248 + if ( CURLM_OK === $mrc ) {
1249 + $retry_added = true;
1250 + unset( $requestUrls[ $rid ] );
1251 + continue; // libcurl updates $running automatically.
1252 + }
1253 +
1254 + curl_multi_remove_handle( $mh, $info['handle'] );
1255 + }
990 1256 }
991 1257
1258 + if ( ! $contains ) {
1259 + $log_website = isset( $handleToWebsite[ $rid ] ) ? $handleToWebsite[ $rid ] : null;
1260 + MainWP_Logger::instance()->debug( static::format_unexpected_response_log( $log_website, $data ) );
1261 + }
1262 +
992 1263 if ( null !== $handler ) {
993 1264 $site = &$handleToWebsite[ static::get_resource_id( $info['handle'] ) ];
994 1265 call_user_func_array( $handler, array( $data, $site, &$output, $params ) );
995 1266 }
@@ -1000,9 +1271,9 @@
1000 1271 }
1001 1272 unset( $info['handle'] );
1002 1273 }
1003 1274 usleep( 10000 );
1004 - } while ( $running > 0 );
1275 + } while ( $running > 0 || $retry_added );
1005 1276
1006 1277 if ( static::is_valid_curl_handle( $mh ) ) {
1007 1278 curl_multi_close( $mh );
1008 1279 }
@@ -1163,8 +1434,9 @@
1163 1434 * @param bool $checkConstraints Whether or not to check constraints.
1164 1435 * @param bool $pForceFetch Whether or not to force the fetch.
1165 1436 * @param bool $pRetryFailed Whether or not to retry the fetch process.
1166 1437 * @param null $rawResponse Raw response.
1438 + * @param bool $verify_signature Wether verify signature data.
1167 1439 *
1168 1440 * @return mixed $information
1169 1441 *
1170 1442 * @uses \MainWP\Dashboard\MainWP_Monitoring_Handler::handle_check_website()
@@ -1177,9 +1449,10 @@
1177 1449 $params = null,
1178 1450 $checkConstraints = false,
1179 1451 $pForceFetch = false,
1180 1452 $pRetryFailed = true,
1181 - $rawResponse = null
1453 + $rawResponse = null,
1454 + $verify_signature = false
1182 1455 ) {
1183 1456 unset( $pForceFetch );
1184 1457
1185 1458 // to support demo data.
@@ -1214,9 +1487,9 @@
1214 1487 * @return mixed Array to return early, false to proceed normally.
1215 1488 */
1216 1489 $is_phpunit_env = defined( 'WP_TESTS_DOMAIN' ) || defined( 'PHPUNIT_COMPOSER_INSTALL' ) || ( defined( 'WP_TESTS_DIR' ) && WP_TESTS_DIR );
1217 1490 if ( defined( 'MAINWP_TESTING_MODE' ) && MAINWP_TESTING_MODE && $is_phpunit_env ) {
1218 - $pre_result = apply_filters( 'mainwp_fetch_url_authed_pre', false, $website, $what, $params );
1491 + $pre_result = apply_filters( 'mainwp_fetch_url_authed_pre', false, $website, $what, $params, $verify_signature );
1219 1492 if ( false !== $pre_result ) {
1220 1493 return $pre_result;
1221 1494 }
1222 1495 }
@@ -1229,10 +1502,8 @@
1229 1502 'force_use_ipv4' => $website->force_use_ipv4,
1230 1503 'upgrade' => ( 'upgradeplugintheme' === $what || 'upgrade' === $what || 'upgradetranslation' === $what ),
1231 1504 );
1232 1505
1233 - $request_update = MainWP_Premium_Update::maybe_request_premium_updates( $website, $what, $params );
1234 -
1235 1506 if ( isset( $rawResponse ) && $rawResponse ) {
1236 1507 $others['raw_response'] = 'yes';
1237 1508 }
1238 1509
@@ -1240,11 +1511,17 @@
1240 1511
1241 1512 $updating_website = false;
1242 1513 $type = '';
1243 1514 $list = '';
1244 - if ( 'upgradeplugintheme' === $what || 'upgrade' === $what || 'upgradetranslation' === $what ) {
1515 +
1516 + $premium_update = 'process_premium_updates' === $what && ! empty( $params['premium_perform'] ) && 'premium_update' === $params['premium_perform'] ? true : false;
1517 +
1518 + if ( 'upgradeplugintheme' === $what || 'upgrade' === $what || 'upgradetranslation' === $what || $premium_update ) {
1245 1519 $updating_website = true;
1246 - if ( 'upgradeplugintheme' === $what || 'upgradetranslation' === $what ) {
1520 + if ( $premium_update ) {
1521 + $type = $params['premium_type'];
1522 + $list = $params['list'];
1523 + } elseif ( 'upgradeplugintheme' === $what || 'upgradetranslation' === $what ) {
1247 1524 $type = $params['type'];
1248 1525 $list = $params['list'];
1249 1526 } else {
1250 1527 $type = 'wp';
@@ -1266,47 +1543,80 @@
1266 1543 */
1267 1544 do_action( 'mainwp_website_before_updated', $website, $type, $list );
1268 1545 }
1269 1546
1547 + $information = array();
1548 + $output = array();
1549 +
1550 + if ( 'stats' === $what || ( 'upgradeplugintheme' === $what && isset( $params['type'] ) ) ) {
1551 + $request_update = MainWP_Premium_Update::maybe_request_premium_updates( $website, $what, $params, $output_result );
1552 + if ( $request_update ) {
1553 + // Return the information here.
1554 + return $output_result;
1555 + }
1556 + }
1557 +
1558 + // MWP-1548: decrypt http_user / http_pass before they hit the
1559 + // outbound HTTP Basic Auth header. Legacy plaintext rows pass
1560 + // through unchanged.
1561 + $http_user_plain = MainWP_Credential_Storage::decrypt_credential( $website->http_user );
1562 + $http_pass_plain = MainWP_Credential_Storage::decrypt_credential( $website->http_pass );
1563 +
1270 1564 if ( 'renew' === $what ) {
1271 - $postdata = static::get_renew_post_data_authed( $website, $what );
1565 + $postdata = static::get_renew_post_data_authed(
1566 + $website,
1567 + $what,
1568 + array(
1569 + 'verify_signature' => $verify_signature,
1570 + )
1571 + );
1272 1572 } else {
1273 - $postdata = static::get_post_data_authed( $website, $what, $params );
1573 + $postdata = static::get_post_data_authed(
1574 + $website,
1575 + $what,
1576 + $params,
1577 + array(
1578 + 'verify_signature' => $verify_signature,
1579 + 'http_user_plain' => $http_user_plain,
1580 + 'http_pass_plain' => $http_pass_plain,
1581 + )
1582 + );
1274 1583
1275 1584 }
1585 +
1276 1586 $others['function'] = $what;
1277 1587
1278 - $information = array();
1279 - $output = array();
1588 + $information = static::fetch_url( $website, $website->url, $postdata, $checkConstraints, $website->verify_certificate, $pRetryFailed, $http_user_plain, $http_pass_plain, $website->ssl_version, $others, $output );
1280 1589
1281 - if ( ! $request_update ) {
1282 - $information = static::fetch_url( $website, $website->url, $postdata, $checkConstraints, $website->verify_certificate, $pRetryFailed, $website->http_user, $website->http_pass, $website->ssl_version, $others, $output );
1283 - if ( ! empty( $output ) ) {
1284 - if ( ! is_array( $information ) ) {
1285 - $information = array();
1286 - }
1287 - $information['fetch_url_output'] = $output;
1590 + if ( ! empty( $output ) ) {
1591 + if ( ! is_array( $information ) ) {
1592 + $information = array();
1288 1593 }
1289 - /**
1290 - * Fires immediately after fetch url action.
1291 - *
1292 - * @param object $website website.
1293 - * @param array $information information result data.
1294 - * @param string $what action.
1295 - * @param array $params params input array.
1296 - * @param array $others others input array.
1297 - *
1298 - * @since 4.5.1.1
1299 - */
1300 - do_action( 'mainwp_fetch_url_authed', $website, $information, $what, $params, $others );
1301 - } else {
1302 - $slug = $params['list'];
1303 - $information['upgrades'] = array( $slug => 1 );
1594 + $information['fetch_url_output'] = $output;
1304 1595 }
1596 + /**
1597 + * Fires immediately after fetch url action.
1598 + *
1599 + * @param object $website website.
1600 + * @param array $information information result data.
1601 + * @param string $what action.
1602 + * @param array $params params input array.
1603 + * @param array $others others input array.
1604 + *
1605 + * @since 4.5.1.1
1606 + */
1607 + do_action( 'mainwp_fetch_url_authed', $website, $information, $what, $params, $others );
1305 1608
1306 - if ( is_array( $information ) && isset( $information['sync'] ) && ! empty( $information['sync'] ) ) {
1307 - MainWP_Sync::sync_information_array( $website, $information['sync'] );
1308 - unset( $information['sync'] );
1609 + if ( is_array( $information ) ) {
1610 + if ( isset( $information['sync'] ) && ! empty( $information['sync'] ) ) {
1611 + MainWP_Sync::sync_information_array( $website, $information['sync'] );
1612 + unset( $information['sync'] );
1613 + }
1614 +
1615 + if ( isset( $information['sync_partial'] ) && is_array( $information['sync_partial'] ) && ! empty( $information['sync_partial'] ) ) {
1616 + MainWP_Sync::sync_partial_information( $website, $information['sync_partial'] );
1617 + unset( $information['sync_partial'] );
1618 + }
1309 1619 }
1310 1620
1311 1621 if ( $updating_website ) {
1312 1622 /**
@@ -1361,8 +1671,72 @@
1361 1671 $sslVersion = 0,
1362 1672 $others = array(),
1363 1673 &$output = array()
1364 1674 ) {
1675 +
1676 + /**
1677 + * Filter to mock the fetch_url_not_authed response before any HTTP request occurs.
1678 + *
1679 + * This filter fires early, before the HTTP request is performed, allowing
1680 + * tests to bypass child site communication entirely.
1681 + *
1682 + * SECURITY WARNING - TEST ONLY:
1683 + * This filter ONLY fires when ALL of the following conditions are met:
1684 + * 1. MAINWP_TESTING_MODE constant is defined and true.
1685 + * 2. A PHPUnit test harness constant is present (WP_TESTS_DOMAIN,
1686 + * PHPUNIT_COMPOSER_INSTALL, or WP_TESTS_DIR).
1687 + *
1688 + * These checks prevent malicious code from defining MAINWP_TESTING_MODE
1689 + * in production to spoof child site responses.
1690 + *
1691 + * IMPORTANT: MAINWP_TESTING_MODE must ONLY be defined in the PHPUnit bootstrap
1692 + * file (tests/bootstrap.php). Defining it in production code, wp-config.php,
1693 + * or plugin files would create a security vulnerability allowing response
1694 + * spoofing.
1695 + *
1696 + * @since 6.2.1
1697 + *
1698 + * @param mixed $pre_result Return non-false to short-circuit the request and return this value.
1699 + * @param string $url URL to fetch.
1700 + * @param string $admin Admin username or identifier.
1701 + * @param string $what Action being performed.
1702 + * @param array|null $params Request parameters.
1703 + * @param bool $pForceFetch Whether to force the fetch.
1704 + * @param bool|null $verifyCertificate Whether to verify the SSL certificate.
1705 + * @param string|null $http_user HTTP authentication username.
1706 + * @param string|null $http_pass HTTP authentication password.
1707 + * @param int $sslVersion SSL version.
1708 + * @param array $others Additional request options.
1709 + * @param array $output Output data passed by reference.
1710 + *
1711 + * @return mixed Array or other value to return early, false to proceed normally.
1712 + */
1713 + $is_phpunit_env = defined( 'WP_TESTS_DOMAIN' ) || defined( 'PHPUNIT_COMPOSER_INSTALL' ) || ( defined( 'WP_TESTS_DIR' ) && WP_TESTS_DIR );
1714 +
1715 + if ( defined( 'MAINWP_TESTING_MODE' ) && MAINWP_TESTING_MODE && $is_phpunit_env ) {
1716 + $pre_result = apply_filters_ref_array(
1717 + 'mainwp_fetch_url_not_authed_pre',
1718 + array(
1719 + false,
1720 + $url,
1721 + $admin,
1722 + $what,
1723 + $params,
1724 + $pForceFetch,
1725 + $verifyCertificate,
1726 + $http_user,
1727 + $http_pass,
1728 + $sslVersion,
1729 + $others,
1730 + &$output,
1731 + )
1732 + );
1733 +
1734 + if ( false !== $pre_result ) {
1735 + return $pre_result;
1736 + }
1737 + }
1738 +
1365 1739 unset( $pForceFetch );
1366 1740
1367 1741 if ( empty( $params ) ) {
1368 1742 $params = array();
@@ -1479,8 +1853,17 @@
1479 1853
1480 1854 /**
1481 1855 * Enables data to be returned prior to connecting to the site.
1482 1856 *
1857 + * Dev/test override only. Gated behind the MAINWP_DEV_FILTERS_ENABLED
1858 + * constant so the filter does not dispatch in production. The filter
1859 + * receives plaintext HTTP Basic Auth credentials and the full $website
1860 + * DB row (including privkey); enabling it in production would expose
1861 + * those values to any 3rd-party plugin hooking the filter.
1862 + *
1863 + * To enable in a dev/test environment, add to wp-config.php:
1864 + * define( 'MAINWP_DEV_FILTERS_ENABLED', true );
1865 + *
1483 1866 * @since 5.5
1484 1867 *
1485 1868 * @param mixed false
1486 1869 * @param mixed $website
@@ -1493,11 +1876,13 @@
1493 1876 * @param mixed $sslVersion
1494 1877 * @param mixed $others
1495 1878 * @param mixed $output
1496 1879 */
1497 - $dev_data = apply_filters( 'mainwp_dev_return_data_before_connect_site', false, $website, $url, $postdata, $checkConstraints, $verifyCertificate, $http_user, $http_pass, $sslVersion, $others, $output );
1498 - if ( false !== $dev_data ) {
1499 - return $dev_data;
1880 + if ( defined( 'MAINWP_DEV_FILTERS_ENABLED' ) && MAINWP_DEV_FILTERS_ENABLED ) {
1881 + $dev_data = apply_filters( 'mainwp_dev_return_data_before_connect_site', false, $website, $url, $postdata, $checkConstraints, $verifyCertificate, $http_user, $http_pass, $sslVersion, $others, $output );
1882 + if ( false !== $dev_data ) {
1883 + return $dev_data;
1884 + }
1500 1885 }
1501 1886
1502 1887 $agent = 'Mozilla/5.0 (compatible; MainWP/' . MainWP_System::$version . '; +http://mainwp.com)';
1503 1888
@@ -1543,9 +1928,10 @@
1543 1928 // to fix.
1544 1929 if ( defined( 'LOGGED_IN_SALT' ) && defined( 'NONCE_SALT' ) ) {
1545 1930 $cookie_salt = sha1( sha1( 'mainwp' . LOGGED_IN_SALT . $website->id ) . NONCE_SALT . 'WP_Cookie' ); // NOSONAR - safe for salt file name.
1546 1931 } else {
1547 - $cookie_salt = sha1( sha1( 'mainwp' . $website->id ) . 'WP_Cookie' ); // NOSONAR - safe for salt file name.
1932 + // MWP-1558: misconfigured WP installs (no salts) previously used unsalted SHA1, which is enumerable. Fall back to the per-install MainWP filename secret instead.
1933 + $cookie_salt = MainWP_System_Utility::get_private_filename( 'cookies', $website->id, 'WP_Cookie' );
1548 1934 }
1549 1935 $cookieFile = $cookieDir . '/' . $cookie_salt;
1550 1936 if ( ! file_exists( $cookieFile ) ) {
1551 1937 @file_put_contents( $cookieFile, '' );
@@ -1642,9 +2028,25 @@
1642 2028 if ( $force_use_ipv4 && defined( 'CURLOPT_IPRESOLVE' ) && defined( 'CURL_IPRESOLVE_V4' ) ) {
1643 2029 curl_setopt( $ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4 );
1644 2030 }
1645 2031
1646 - $timeout = 20 * 60 * 60;
2032 + $what = '';
2033 + if ( is_array( $others ) && isset( $others['function'] ) ) {
2034 + $what = $others['function'];
2035 + }
2036 +
2037 + if ( 'deactivate' === $what ) {
2038 + $timeout = 120; // 2 minutes.
2039 + } else {
2040 + $timeout = 20 * 60 * 60;
2041 + }
2042 +
2043 + /** This filter is documented in class/class-mainwp-connect.php */
2044 + $timeout = (int) apply_filters( 'mainwp_fetch_url_site_timeout', $timeout, $what );
2045 + if ( $timeout <= 0 ) {
2046 + $timeout = 20 * 60 * 60; // values below 1 would disable the cURL timeout entirely.
2047 + }
2048 +
1647 2049 curl_setopt( $ch, CURLOPT_TIMEOUT, $timeout );
1648 2050 MainWP_System_Utility::set_time_limit( $timeout );
1649 2051
1650 2052 MainWP_Utility::end_session();
@@ -1668,9 +2070,8 @@
1668 2070 do {
1669 2071 $mrc = curl_multi_exec( $mh, $running );
1670 2072 } while ( CURLM_CALL_MULTI_PERFORM === $mrc );
1671 2073
1672 -
1673 2074 $rc = curl_multi_select( $mh, 1.0 );
1674 2075 if ( -1 === $rc ) {
1675 2076 usleep( 100000 );
1676 2077 }
@@ -1731,16 +2132,17 @@
1731 2132 MainWP_Logger::instance()->log_execution_time( 'fetch_url_site :: [url=' . $url . ']' );
1732 2133
1733 2134 $thr_error = null;
1734 2135
1735 - if ( isset( $others['function'] ) ) {
1736 - $what = $others['function'];
1737 - if ( in_array( $what, array( 'installplugintheme', 'upgradeplugintheme', 'upgradetranslation', 'upgrade', 'stats', 'renew', 'reconnect' ), true ) ) {
1738 - MainWP_Cache_Helper::invalidate_cache_group( MainWP_Cache_Helper::CGR_UPDATES );
1739 - MainWP_Cache_Warm_Helper::invalidate_pages_by_site_actions( $what );
1740 - }
2136 + if ( in_array( $what, array( 'installplugintheme', 'upgradeplugintheme', 'upgradetranslation', 'process_premium_updates', 'upgrade', 'stats', 'renew', 'reconnect' ), true ) ) {
2137 + MainWP_Cache_Helper::invalidate_cache_group( MainWP_Cache_Helper::CGR_UPDATES );
2138 + MainWP_Cache_Warm_Helper::invalidate_pages_by_site_actions( $what );
1741 2139 }
1742 2140
2141 + if ( 'process_premium_updates' === $what ) {
2142 + MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url_site', 'function: [process_premium_updates] response data: [' . MainWP_Utility::value_to_string( $data ) . ']' );
2143 + }
2144 +
1743 2145 if ( ( false === $data ) && empty( $http_status ) ) {
1744 2146 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url', '[' . $url . '] HTTP Error: [status=0][' . $err . ']' );
1745 2147 $thr_error = new MainWP_Exception( 'HTTPERROR', $err ); //phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
1746 2148 $output['error_category'] = 'http_error';
@@ -1762,8 +2164,27 @@
1762 2164 $error_code = is_array( $information ) && isset( $information['error_code'] ) ? sanitize_text_field( wp_unslash( $information['error_code'] ) ) : '';
1763 2165 if ( ! empty( $error_code ) ) {
1764 2166 $output['child_error_code'] = $error_code;
1765 2167 }
2168 +
2169 + if ( is_array( $information ) && $website ) {
2170 + // Process to ensure compatibility with old and new Child versions.
2171 + $new_sync = isset( $information['support_advanced_sign'] );
2172 + $old_sync = ! $new_sync && ( isset( $information['mainwpdir'], $information['uniqueId'] ) || isset( $information['version'], $information['wpversion'], $information['wpe'] ) );
2173 + if ( $new_sync || $old_sync ) {
2174 + $adv_sign_support = MainWP_DB::instance()->get_website_option( $website, 'support_advanced_sign' );
2175 + if ( ! empty( $information['support_advanced_sign'] ) && 0 === (int) $adv_sign_support ) {
2176 + MainWP_DB::instance()->update_website_option( $website, 'support_advanced_sign', 1 );
2177 + } elseif ( empty( $information['support_advanced_sign'] ) && 1 === (int) $adv_sign_support ) {
2178 + MainWP_DB::instance()->update_website_option( $website, 'support_advanced_sign', 0 );
2179 + }
2180 + }
2181 + }
2182 +
2183 + if ( 'process_premium_updates' === $what ) {
2184 + MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url_site', 'function: [process_premium_updates] decoded data: [' . MainWP_Utility::value_to_string( $information ) . ']' );
2185 + }
2186 +
1766 2187 return $information;
1767 2188 } elseif ( 200 === (int) $http_status && ! empty( $err ) ) {
1768 2189 $thr_error = new MainWP_Exception( 'HTTPERROR', $err ); //phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
1769 2190 $output['error_category'] = 'http_error';
@@ -1768,8 +2189,9 @@
1768 2189 $thr_error = new MainWP_Exception( 'HTTPERROR', $err ); //phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
1769 2190 $output['error_category'] = 'http_error';
1770 2191 $output['error_code'] = 'http_request_failed';
1771 2192 $output['error_message'] = $err;
2193 + MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url_site', '[' . $url . '] error [' . $err . ']' ); //phpcs:ignore -- ok.
1772 2194 } elseif ( $raw_response ) {
1773 2195 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url_site', 'Response: [RAW]' );
1774 2196 return $data;
1775 2197 } else {
@@ -1937,9 +2359,9 @@
1937 2359 $wp_filesystem->delete( $file );
1938 2360 }
1939 2361
1940 2362 if ( ! $wp_filesystem->exists( dirname( $file ) ) ) {
1941 - $wp_filesystem->mkdir( dirname( $file ), 0777 );
2363 + $wp_filesystem->mkdir( dirname( $file ), 0750 ); // MWP-1558: tightened from 0777; downloaded files may contain backup data.
1942 2364 }
1943 2365
1944 2366 if ( ! $wp_filesystem->exists( dirname( $file ) ) ) {
1945 2367 throw new MainWP_Exception( esc_html__( 'MainWP plugin could not create directory in order to download the file.', 'mainwp' ) );
@@ -2016,9 +2438,9 @@
2016 2438
2017 2439 if ( $hasWPFileSystem && ! empty( $wp_filesystem ) ) {
2018 2440
2019 2441 if ( ! $wp_filesystem->is_dir( $cookieDir ) ) {
2020 - $wp_filesystem->mkdir( $cookieDir, 0777 );
2442 + $wp_filesystem->mkdir( $cookieDir, 0750 ); // MWP-1558: tightened from 0777; cookies/ holds child wp-admin session cookies.
2021 2443 }
2022 2444
2023 2445 if ( ! file_exists( $cookieDir . '/.htaccess' ) ) {
2024 2446 $file_htaccess = $cookieDir . '/.htaccess';
@@ -2031,9 +2453,9 @@
2031 2453 }
2032 2454 } else {
2033 2455
2034 2456 if ( ! file_exists( $cookieDir ) ) {
2035 - @mkdir( $cookieDir, 0777, true );
2457 + @mkdir( $cookieDir, 0750, true ); // MWP-1558: tightened from 0777; cookies/ holds child wp-admin session cookies.
2036 2458 }
2037 2459
2038 2460 if ( ! file_exists( $cookieDir . '/.htaccess' ) ) {
2039 2461 $file_htaccess = @fopen( $cookieDir . '/.htaccess', 'w+' );
@@ -2095,9 +2517,9 @@
2095 2517
2096 2518 /**
2097 2519 * Method is_valid_curl_handle
2098 2520 *
2099 - * @param mixed $ch
2521 + * @param mixed $ch cURL handle to validate.
2100 2522 * @return bool Valid curl handle.
2101 2523 */
2102 2524 public static function is_valid_curl_handle( $ch ) {
2103 2525 return is_resource( $ch )