PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / trunk
MainWP Dashboard: Self-hosted WordPress Management for Agencies vtrunk
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 5.2.2 5.3 All 153 releases
← All changes | class/class-mainwp-connect.php +732 -177 5.3trunk View file →
@@ -8,8 +8,13 @@
8 8 */
9 9
10 10 namespace MainWP\Dashboard;
11 11
12 +// Exit if accessed directly.
13 +if ( ! defined( 'ABSPATH' ) ) {
14 + exit;
15 +}
16 +
12 17 /**
13 18 * Class MainWP_Connect
14 19 *
15 20 * @package MainWP\Dashboard
@@ -71,10 +76,13 @@
71 76 curl_setopt( $ch, CURLOPT_URL, $url );
72 77 if ( $no_body ) {
73 78 curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, 'HEAD' ); // HTTP request is 'HEAD', but sometime return 4xx - error code.
74 79 }
80 +
81 + $follow_loc = apply_filters( 'mainwp_try_visit_follow_location', false ); // to support for case compatible.
82 +
75 83 curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
76 - curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, true );
84 + curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, $follow_loc ? true : false );
77 85 curl_setopt( $ch, CURLOPT_POST, true );
78 86 curl_setopt( $ch, CURLOPT_POSTFIELDS, $postdata );
79 87 curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, 10 );
80 88 curl_setopt( $ch, CURLOPT_USERAGENT, $agent );
@@ -107,8 +115,9 @@
107 115 }
108 116
109 117 $headers = array( 'X-Requested-With' => 'XMLHttpRequest' );
110 118 $headers['Expect'] = static::get_expect_header( $postdata );
119 + $headers = apply_filters( 'mainwp_connect_http_request_headers', $headers, false );
111 120
112 121 if ( class_exists( '\WpOrg\Requests\Requests' ) ) {
113 122 $headers = \WpOrg\Requests\Requests::flatten( $headers );
114 123 } else {
@@ -114,9 +123,9 @@
114 123 } else {
115 124 $headers = \Requests::flatten( $headers );
116 125 }
117 126
118 - curl_setopt( $ch, CURLOPT_HTTPHEADER, array( 'X-Requested-With: XMLHttpRequest' ) );
127 + curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
119 128 curl_setopt( $ch, CURLOPT_REFERER, get_option( 'siteurl' ) );
120 129
121 130 $force_use_ipv4 = false;
122 131 if ( null !== $forceUseIPv4 ) {
@@ -136,8 +145,10 @@
136 145 }
137 146
138 147 MainWP_Logger::instance()->debug( ' :: trying Visit :: [url=' . $url . ']' );
139 148
149 + $http_version = false;
150 +
140 151 $disabled_functions = ini_get( 'disable_functions' );
141 152 if ( empty( $disabled_functions ) || ( stristr( $disabled_functions, 'curl_multi_exec' ) === false ) ) {
142 153 MainWP_Logger::instance()->debug( ' :: trying Visit :: curl_multi_exec => enabled.' );
143 154 $mh = curl_multi_init();
@@ -143,22 +154,36 @@
143 154 $mh = curl_multi_init();
144 155 @curl_multi_add_handle( $mh, $ch );
145 156
146 157 do {
147 - curl_multi_exec( $mh, $running );
148 - curl_multi_select( $mh );
158 + do {
159 + $mrc = curl_multi_exec( $mh, $running );
160 + } while ( CURLM_CALL_MULTI_PERFORM === $mrc );
161 +
162 + if ( $running ) {
163 + $rc = curl_multi_select( $mh, 1.0 );
164 + if ( -1 === $rc ) {
165 + usleep( 100000 );
166 + }
167 + }
168 +
149 169 while ( $info = curl_multi_info_read( $mh ) ) {
150 170 $data = curl_multi_getcontent( $info['handle'] );
151 171 $err = curl_error( $info['handle'] );
152 172 $http_status = curl_getinfo( $info['handle'], CURLINFO_HTTP_CODE );
153 173 $realurl = curl_getinfo( $info['handle'], CURLINFO_EFFECTIVE_URL );
174 + if ( defined( 'CURLINFO_HTTP_VERSION' ) ) {
175 + $http_version = curl_getinfo( $info['handle'], CURLINFO_HTTP_VERSION );
176 + }
177 +
154 178 curl_multi_remove_handle( $mh, $info['handle'] );
179 + curl_close( $info['handle'] );
155 180 }
156 181 usleep( 10000 );
157 182
158 183 } while ( $running > 0 );
159 184
160 - if ( 'resource' === gettype( $mh ) ) {
185 + if ( static::is_valid_curl_handle( $mh ) ) {
161 186 curl_multi_close( $mh );
162 187 }
163 188 } else {
164 189 $data = curl_exec( $ch );
@@ -164,14 +189,18 @@
164 189 $data = curl_exec( $ch );
165 190 $err = curl_error( $ch );
166 191 $http_status = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
167 192 $realurl = curl_getinfo( $ch, CURLINFO_EFFECTIVE_URL );
168 - if ( 'resource' === gettype( $ch ) ) {
193 +
194 + if ( defined( 'CURLINFO_HTTP_VERSION' ) ) {
195 + $http_version = curl_getinfo( $ch, CURLINFO_HTTP_VERSION );
196 + }
197 +
198 + if ( static::is_valid_curl_handle( $ch ) ) {
169 199 curl_close( $ch );
170 200 }
171 201 }
172 202
173 - MainWP_Logger::instance()->debug( ' :: tryVisit :: [url=' . $url . '] [http_status=' . $http_status . '] [error=' . $err . '] [data-start]' . $data . '[data-end]' );
174 203 MainWP_Logger::instance()->log_execution_time( 'tryVisit :: [url=' . $url . '] [http_status=' . $http_status . ']' );
175 204
176 205 $host = wp_parse_url( ( empty( $realurl ) ? $url : $realurl ), PHP_URL_HOST );
177 206 $ip = false;
@@ -176,10 +205,15 @@
176 205 $host = wp_parse_url( ( empty( $realurl ) ? $url : $realurl ), PHP_URL_HOST );
177 206 $ip = false;
178 207 $target = false;
179 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.
180 214 $found = false;
181 - $dnsRecord = @dns_get_record( $host );
215 + $dnsRecord = @dns_get_record( $host, DNS_A | DNS_AAAA | DNS_CNAME );
182 216 MainWP_Logger::instance()->debug( ' :: tryVisit :: [dnsRecord=' . MainWP_Utility::value_to_string( $dnsRecord, 1 ) . ']' );
183 217
184 218 if ( false !== $dnsRecord && is_array( $dnsRecord ) ) {
185 219 if ( ! isset( $dnsRecord['ip'] ) ) {
@@ -221,10 +255,19 @@
221 255 $out = array(
222 256 'host' => $host,
223 257 'httpCode' => $http_status,
224 258 'httpCodeString' => MainWP_Utility::get_http_codes( $http_status ),
259 + 'httpVersion' => $http_version,
225 260 );
226 261
262 + $hidden_data = '[hidden response data]';
263 +
264 + if ( ( false === $ip || $ip === $host || ! static::validate_ip( $ip ) ) && apply_filters( 'mainwp_hide_raw_connection_response_data', true ) ) { // Failed to resolve hostname.
265 + $data = $hidden_data;
266 + }
267 +
268 + MainWP_Logger::instance()->debug( ' :: tryVisit :: [url=' . $url . '] [http_status=' . $http_status . '] [http_version=' . ( false === $http_version ? 'N/A' : MainWP_System_Utility::get_http_version_const_str( $http_version ) ) . '] [error=' . $err . '] [data-start]' . $data . '[data-end]' );
269 +
227 270 if ( false !== $ip ) {
228 271 $out['ip'] = $ip;
229 272 $found = true;
230 273 }
@@ -233,9 +276,24 @@
233 276
234 277 return $out;
235 278 }
236 279
280 +
237 281 /**
282 + * Method validate_ip().
283 + *
284 + * @param string $ip IP check.
285 + * @return bool Check IP result.
286 + */
287 + public static function validate_ip( $ip ) {
288 + // Validate the IP and check for private and reserved ranges.
289 + if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
290 + return true;
291 + }
292 + return false;
293 + }
294 +
295 + /**
238 296 * Method check_ignored_http_code()
239 297 *
240 298 * Check if http error code is being ignored.
241 299 *
@@ -243,28 +301,32 @@
243 301 * @param object|false $website website.
244 302 *
245 303 * @return bolean True|False.
246 304 */
247 - public static function check_ignored_http_code( $value, $website = false ) {
248 - $value = (int) $value;
249 - if ( 200 === $value ) {
250 - return true;
251 - }
305 + public static function check_ignored_http_code( $value, $website = false ) { // phpcs:ignore -- NOSONAR -complex method.
306 + $value = (int) $value;
307 + $site_id = is_object( $website ) && ! empty( $website->id ) ? $website->id : 0;
308 + $global_settings = MainWP_Uptime_Monitoring_Handle::get_global_monitoring_settings();
252 309
253 - if ( ! is_object( $website ) || empty( $website->id ) ) {
254 - return false;
255 - }
310 + $ignored_code = '';
256 311
257 - $ignored_code = '';
258 - if ( ! property_exists( $website, 'monitor_id' ) ) {
312 + if ( $site_id ) {
313 +
259 314 $primary_monitor = MainWP_DB_Uptime_Monitoring::instance()->get_monitor_by( $site_id, 'issub', 0 );
315 + $global_settings = MainWP_Uptime_Monitoring_Handle::get_global_monitoring_settings();
260 316
317 + $mo_active = 0;
261 318 if ( $primary_monitor ) {
262 - $global_settings = MainWP_Uptime_Monitoring_Handle::get_global_monitoring_settings();
263 - $ignored_code = MainWP_Uptime_Monitoring_Connect::instance()->get_up_codes( $primary_monitor, $global_settings );
319 + $mo_active = MainWP_Uptime_Monitoring_Connect::get_apply_setting( 'active', (int) $primary_monitor->active, $global_settings, -1, 0 );
320 + }
321 +
322 + if ( $mo_active ) {
323 + $ignored_code = MainWP_Uptime_Monitoring_Connect::instance()->get_up_codes( $primary_monitor, $global_settings );
264 324 } else {
265 - return false;
325 + $ignored_code = is_array( $global_settings ) && isset( $global_settings['up_status_codes'] ) ? $global_settings['up_status_codes'] : '';
266 326 }
327 + } else {
328 + $ignored_code = ! empty( $global_settings['up_status_codes'] ) ? $global_settings['up_status_codes'] : '';
267 329 }
268 330
269 331 if ( ! empty( $ignored_code ) ) {
270 332 $ignored_code = explode( ',', $ignored_code );
@@ -283,20 +345,27 @@
283 345 *
284 346 * Check if the Website returns and http errors.
285 347 *
286 348 * @param object $website Child Site information.
349 + * @param bool $chk_http_site Check site http response.
287 350 *
288 351 * @return mixed False|try visit result.
289 352 *
290 353 * @uses \MainWP\Dashboard\MainWP_Utility::is_domain_valid()
291 354 */
292 - public static function check_website_status( $website ) { //phpcs:ignore -- NOSONAR - complexity.
355 + public static function check_website_status( $website, $chk_http_site = false ) { //phpcs:ignore -- NOSONAR - complexity.
293 356
294 357 if ( is_object( $website ) && isset( $website->id ) ) {
295 358 $primary_monitor = MainWP_DB_Uptime_Monitoring::instance()->get_monitor_by( $website->id, 'issub', 0 );
296 359 if ( $primary_monitor ) {
297 360 // return compatible uptime status here.
298 - return MainWP_Uptime_Monitoring_Handle::check_website_uptime_monitoring_status( $primary_monitor, array( 'ignore_compatible_save' => 1 ) );
361 + return MainWP_Uptime_Monitoring_Handle::check_website_uptime_monitoring_status(
362 + $primary_monitor,
363 + array(
364 + 'ignore_compatible_save' => 1,
365 + 'check_http_site' => $chk_http_site,
366 + )
367 + ); // to ignore save compatible uptime status.
299 368 }
300 369 }
301 370
302 371 $http_user = null;
@@ -307,11 +376,14 @@
307 376 if ( is_object( $website ) && isset( $website->url ) ) {
308 377 $url = $website->url;
309 378 $verifyCertificate = isset( $website->verify_certificate ) ? (int) $website->verify_certificate : null;
310 379 $forceUseIPv4 = $website->force_use_ipv4;
311 - $http_user = $website->http_user;
312 - $http_pass = $website->http_pass;
313 - $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;
314 386 } else {
315 387 $url = $website;
316 388 }
317 389
@@ -340,12 +412,22 @@
340 412 *
341 413 * @param mixed $website Array of Child Site Info.
342 414 * @param mixed $what What we are posting.
343 415 * @param null $params Post parameters.
416 + * @param array $others Other data.
344 417 *
345 418 * @return mixed null|http_build_query()
346 419 */
347 - 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 +
348 430 if ( $website && '' !== $what ) {
349 431 $data = array();
350 432 $data['user'] = $website->adminname;
351 433 $data['function'] = $what;
@@ -351,9 +433,9 @@
351 433 $data['function'] = $what;
352 434 $data['nonce'] = wp_rand( 0, 9999 );
353 435 $data['mainwpver'] = MainWP_System::$version;
354 436
355 - $params_filter = apply_filters( 'mainwp_pre_fetch_authed_data', false, $params, $what, $website );
437 + $params_filter = apply_filters( 'mainwp_pre_fetch_authed_data', false, $params, $what, $website, $verify_signature );
356 438 if ( is_array( $params_filter ) && ! empty( $params_filter ) ) {
357 439 $data = array_merge( $data, $params_filter );
358 440 }
359 441
@@ -364,47 +446,12 @@
364 446 $alg = false;
365 447 $sign_success = null;
366 448 $use_seclib = false;
367 449
368 - $data = apply_filters( 'mainwp_get_post_data_authed', $data, $website, $what, $params );
369 - if ( MainWP_Connect_Lib::is_use_fallback_sec_lib( $website ) ) {
370 - $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.
371 - $use_seclib = true;
372 - } elseif ( function_exists( 'openssl_verify' ) ) {
373 - $alg = MainWP_System_Utility::get_connect_sign_algorithm( $website );
374 - $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.
375 - if ( false !== $alg ) {
376 - $data['sign_algo'] = $alg;
377 - }
378 - }
450 + $data = apply_filters( 'mainwp_get_post_data_authed', $data, $website, $what, $params, $verify_signature );
379 451
380 - if ( $use_seclib ) {
381 - $data['verifylib'] = 1;
382 - }
452 + $alt_user = '';
383 453
384 - if ( null !== $sign_success && empty( $sign_success ) ) {
385 - $sign_error = '';
386 - while ( $msg = openssl_error_string() ) {
387 - if ( is_string( $msg ) ) {
388 - $sign_error .= $msg;
389 - }
390 - }
391 - 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 );
392 - }
393 -
394 - $data['mainwpsignature'] = ! empty( $signature ) ? base64_encode( $signature ) : ''; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
395 -
396 - /** This filter is documented in ../widgets/widget-mainwp-recent-posts.php */
397 - $recent_number = apply_filters( 'mainwp_recent_posts_pages_number', 5 );
398 - if ( 5 !== $recent_number ) {
399 - $data['recent_number'] = $recent_number;
400 - }
401 -
402 - $scan_dir = apply_filters( 'mainwp_stats_scan_dir', false, $website );
403 - if ( ! empty( $scan_dir ) ) {
404 - $data['scan_dir'] = 1;
405 - }
406 -
407 454 /**
408 455 * Current user global.
409 456 *
410 457 * @global string
@@ -421,14 +468,125 @@
421 468 * @param int $current_user->ID User ID.
422 469 *
423 470 * @since Unknown
424 471 */
425 - $alter_user = apply_filters( 'mainwp_alter_login_user', false, $website->id, $current_user->ID );
426 - if ( ! empty( $alter_user ) ) {
427 - $data['alt_user'] = rawurlencode( $alter_user );
472 + $alt_user = apply_filters( 'mainwp_alter_login_user', false, $website->id, $current_user->ID );
473 +
474 + }
475 +
476 + $child_support_adv_sign = 1 === (int) MainWP_DB::instance()->get_website_option( $website, 'support_advanced_sign' );
477 +
478 + $alg = false;
479 + $sign_value = $what . $data['nonce']; // Legacy signature data.
480 +
481 + if ( ! $verify_signature || ! $child_support_adv_sign ) {
482 + if ( MainWP_Connect_Lib::is_use_fallback_sec_lib( $website ) ) {
483 + $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.
484 + $use_seclib = true;
485 + } elseif ( function_exists( 'openssl_verify' ) ) {
486 + $alg = MainWP_System_Utility::get_connect_sign_algorithm( $website );
487 + $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.
488 + if ( false !== $alg ) {
489 + $data['sign_algo'] = $alg;
490 + }
428 491 }
492 +
493 + if ( null !== $sign_success && empty( $sign_success ) ) {
494 + $sign_error = '';
495 + while ( $msg = openssl_error_string() ) {
496 + if ( is_string( $msg ) ) {
497 + $sign_error .= $msg;
498 + }
499 + }
500 + $pk_info = ! empty( $website->privkey ) ? substr( $website->privkey, 0, 10 ) : '';
501 + 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 );
502 + }
503 + } else {
504 + $signature = 'useadvancedmainwpsignature';
429 505 }
430 506
507 + $data['mainwpsignature'] = ! empty( $signature ) ? base64_encode( $signature ) : ''; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
508 +
509 + if ( $verify_signature ) {
510 + $ts = time();
511 + $data_sign_v2 = array(
512 + 'base_function' => $what,
513 + 'nonce' => $data['nonce'],
514 + 'expires' => $ts + 60,
515 + 'user' => $website->adminname,
516 + 'req_id' => wp_generate_uuid4(),
517 + );
518 +
519 + if ( ! empty( $alt_user ) ) {
520 + $data_sign_v2['alt_user'] = rawurlencode( $alt_user );
521 + }
522 +
523 + if ( 'process_premium_updates' === $what ) {
524 + $add_sign_params = array( 'premium_perform', 'premium_type', 'list' );
525 + foreach ( $add_sign_params as $_name ) {
526 + if ( isset( $params[ $_name ] ) ) {
527 + $data_sign_v2[ $_name ] = $params[ $_name ];
528 + }
529 + }
530 + }
531 +
532 + $sign_success_v2 = null;
533 +
534 + $sign_value_v2 = wp_json_encode( $data_sign_v2 );
535 +
536 + if ( MainWP_Connect_Lib::is_use_fallback_sec_lib( $website ) ) {
537 + $use_seclib = true;
538 + $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.
539 + } elseif ( function_exists( 'openssl_verify' ) ) {
540 + $alg = MainWP_System_Utility::get_connect_sign_algorithm( $website );
541 + $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.
542 + if ( false !== $alg ) {
543 + $data['sign_algo'] = $alg;
544 + }
545 + }
546 +
547 + $data['data_signature'] = $sign_value_v2;
548 + $data['mainwpsignature_adv'] = ! empty( $signature_v2 ) ? base64_encode( $signature_v2 ) : ''; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
549 +
550 + if ( null !== $sign_success_v2 && empty( $sign_success_v2 ) ) {
551 + $sign_error = '';
552 + while ( $msg = openssl_error_string() ) {
553 + if ( is_string( $msg ) ) {
554 + $sign_error .= $msg;
555 + }
556 + }
557 + $pk_info = ! empty( $website->privkey ) ? substr( $website->privkey, 0, 10 ) : '';
558 + 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 );
559 + }
560 + }
561 +
562 + if ( $use_seclib ) {
563 + $data['verifylib'] = 1;
564 + }
565 +
566 + if ( ! empty( $alt_user ) ) {
567 + $data['alt_user'] = rawurlencode( $alt_user );
568 + }
569 +
570 + /** This filter is documented in ../widgets/widget-mainwp-recent-posts.php */
571 + $recent_number = apply_filters( 'mainwp_recent_posts_pages_number', 5 );
572 + if ( 5 !== $recent_number ) {
573 + $data['recent_number'] = $recent_number;
574 + }
575 +
576 + $scan_dir = apply_filters( 'mainwp_stats_scan_dir', false, $website );
577 + if ( ! empty( $scan_dir ) ) {
578 + $data['scan_dir'] = 1;
579 + }
580 +
581 + if ( 'process_premium_updates' === $what ) {
582 + if ( ! empty( $http_user_plain ) && ! empty( $http_pass_plain ) ) {
583 + // For post data.
584 + $data['wp_http_user'] = $http_user_plain;
585 + $data['wp_http_pass'] = $http_pass_plain;
586 + }
587 + $data['sslVerify'] = $website->verify_certificate ? 1 : 0;
588 + }
431 589 return http_build_query( $data, '', '&' );
432 590 }
433 591
434 592 return null;
@@ -440,13 +598,20 @@
440 598 * Get authorized $_POST data & build query for renew connection action only.
441 599 *
442 600 * @param mixed $website Array of Child Site Info.
443 601 * @param mixed $what What we are posting.
602 + * @param array $others Other data.
444 603 *
445 604 * @return mixed null|http_build_query()
446 605 */
447 - private static function get_renew_post_data_authed( &$website, $what ) { // phpcs:ignore -- NOSONAR - complex.
606 + private static function get_renew_post_data_authed( &$website, $what, $others = array() ) { // phpcs:ignore -- NOSONAR - complex.
448 607
608 + if ( ! is_array( $others ) ) {
609 + $others = array();
610 + }
611 +
612 + $verify_signature = ! empty( $others['verify_signature'] ) ? true : false;
613 +
449 614 if ( $website && '' !== $what ) {
450 615 $compat_what = 'disconnect'; // to compatible, renew will call disconnect.
451 616 $data = array();
452 617 $data['user'] = $website->adminname;
@@ -452,8 +617,10 @@
452 617 $data['user'] = $website->adminname;
453 618 $data['function'] = $compat_what;
454 619 $data['nonce'] = wp_rand( 0, 9999 );
455 620
621 + $sign_value = $compat_what . $data['nonce']; // compatible format.
622 +
456 623 $alg = false;
457 624 $sign_success = null;
458 625 $use_seclib = false;
459 626
@@ -458,17 +625,17 @@
458 625 $use_seclib = false;
459 626
460 627 if ( MainWP_Connect_Lib::is_use_fallback_sec_lib( $website ) ) {
461 628 // to disconnect.
462 - $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.
629 + $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.
463 630 $use_seclib = true;
464 631 } elseif ( function_exists( 'openssl_verify' ) ) {
465 632 $alg = MainWP_System_Utility::get_connect_sign_algorithm( $website );
466 - $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.
633 + $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.
467 634 if ( empty( $sign_success ) ) { // error from openssl, openssl_sign().
468 635 $alg = defined( 'OPENSSL_ALGO_SHA1' ) ? OPENSSL_ALGO_SHA1 : false; // to set default SHA1, to disconnect.
469 636 MainWP_Logger::instance()->debug_for_website( $website, 'get_renew_post_data_authed', '[' . $website->url . '] :: [openssl_sign:failed] :: Set sign_algo=SHA1' );
470 - $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.
637 + $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.
471 638 }
472 639
473 640 if ( false !== $alg ) {
474 641 $data['sign_algo'] = $alg;
@@ -474,11 +641,9 @@
474 641 $data['sign_algo'] = $alg;
475 642 }
476 643 }
477 644
478 - if ( $use_seclib ) {
479 - $data['verifylib'] = 1;
480 - }
645 + $data['mainwpsignature'] = ! empty( $signature ) ? base64_encode( $signature ) : ''; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
481 646
482 647 if ( null !== $sign_success && empty( $sign_success ) ) {
483 648 $sign_error = '';
484 649 while ( $msg = openssl_error_string() ) {
@@ -488,10 +653,55 @@
488 653 }
489 654 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 );
490 655 }
491 656
492 - $data['mainwpsignature'] = ! empty( $signature ) ? base64_encode( $signature ) : ''; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
657 + if ( $verify_signature ) {
658 + $ts = time();
659 + $data_sign_v2 = array(
660 + 'base_function' => $compat_what,
661 + 'nonce' => $data['nonce'],
662 + 'expires' => $ts + 60,
663 + 'user' => $website->adminname,
664 + 'req_id' => wp_generate_uuid4(),
665 + );
666 + $sign_value_v2 = wp_json_encode( $data_sign_v2 );
493 667
668 + $sign_success_v2 = null;
669 +
670 + if ( MainWP_Connect_Lib::is_use_fallback_sec_lib( $website ) ) {
671 + // to disconnect.
672 + $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.
673 + } elseif ( function_exists( 'openssl_verify' ) ) {
674 + $alg = MainWP_System_Utility::get_connect_sign_algorithm( $website );
675 + $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.
676 + if ( empty( $sign_success_v2 ) ) { // error from openssl, openssl_sign().
677 + $alg = defined( 'OPENSSL_ALGO_SHA1' ) ? OPENSSL_ALGO_SHA1 : false; // to set default SHA1, to disconnect.
678 + MainWP_Logger::instance()->debug_for_website( $website, 'get_renew_post_data_authed', '[' . $website->url . '] :: [openssl_sign:failed] :: Set sign_algo=SHA1' );
679 + $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.
680 + }
681 + if ( false !== $alg ) {
682 + $data['sign_algo'] = $alg;
683 + }
684 + }
685 +
686 + $data['data_signature'] = $sign_value_v2;
687 + $data['mainwpsignature_adv'] = ! empty( $signature_v2 ) ? base64_encode( $signature_v2 ) : ''; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
688 +
689 + if ( null !== $sign_success_v2 && empty( $sign_success_v2 ) ) {
690 + $sign_error = '';
691 + while ( $msg = openssl_error_string() ) {
692 + if ( is_string( $msg ) ) {
693 + $sign_error .= $msg;
694 + }
695 + }
696 + 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 );
697 + }
698 + }
699 +
700 + if ( $use_seclib ) {
701 + $data['verifylib'] = 1;
702 + }
703 +
494 704 return http_build_query( $data, '', '&' );
495 705 }
496 706 return null;
497 707 }
@@ -506,12 +716,16 @@
506 716 * @param mixed $paramValue OpenSSL parameter.
507 717 * @param string $paramName Parameter name.
508 718 * @param bool $asArray true|false Default is false.
509 719 * @param array $other_params other params.
720 + * @param string $custom_url Optional. Override the target base URL for browser-bound
721 + * requests ( see MainWP_Site_Url_Corrector::browser_target_url() ).
722 + * Default null keeps the stored URL � server-side callers
723 + * ( backups, premium updates ) must not pass this.
510 724 *
511 725 * @return string $url
512 726 */
513 - public static function get_get_data_authed( $website, $paramValue, $paramName = 'where', $asArray = false, $other_params = array() ) { //phpcs:ignore -- NOSONAR - complex method.
727 + public static function get_get_data_authed( $website, $paramValue, $paramName = 'where', $asArray = false, $other_params = array(), $custom_url = null ) { //phpcs:ignore -- NOSONAR - complex method.
514 728 $params = array();
515 729 if ( $website && '' !== $paramValue ) {
516 730
517 731 $sign_success = null;
@@ -517,26 +731,50 @@
517 731 $sign_success = null;
518 732 $alg = false;
519 733 $use_seclib = false;
520 734 $nonce = wp_rand( 0, 9999 );
521 - if ( MainWP_Connect_Lib::is_use_fallback_sec_lib( $website ) ) {
522 - $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.
523 - $use_seclib = true;
524 - } elseif ( function_exists( 'openssl_verify' ) ) {
525 - $alg = MainWP_System_Utility::get_connect_sign_algorithm( $website );
526 - $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.
735 +
736 + /**
737 + * Current user global.
738 + *
739 + * @global string
740 + */
741 + global $current_user;
742 +
743 + $alt_user = '';
744 + if ( ( ( ! defined( 'DOING_CRON' ) || false === DOING_CRON ) && ( ! defined( 'WP_CLI' ) || false === WP_CLI ) ) && $current_user && $current_user->ID ) {
745 + /** This filter is documented in ../class/class-mainwp-connect.php */
746 + $alt_user = apply_filters( 'mainwp_alter_login_user', false, $website->id, $current_user->ID );
527 747 }
528 748
529 - $signature = ! empty( $signature ) ? base64_encode( $signature ) : ''; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
749 + $verify_signature = is_array( $other_params ) && ! empty( $other_params['verify_signature'] );
530 750
531 - if ( null !== $sign_success && empty( $sign_success ) ) {
532 - $sign_error = '';
533 - while ( $msg = openssl_error_string() ) {
534 - if ( is_string( $msg ) ) {
535 - $sign_error .= $msg;
751 + $child_support_adv_sign = 1 === (int) MainWP_DB::instance()->get_website_option( $website, 'support_advanced_sign' );
752 +
753 + if ( ! $verify_signature || ! $child_support_adv_sign ) {
754 + $sign_value = $paramValue . $nonce; // compatible format.
755 +
756 + if ( MainWP_Connect_Lib::is_use_fallback_sec_lib( $website ) ) {
757 + $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.
758 + $use_seclib = true;
759 + } elseif ( function_exists( 'openssl_verify' ) ) {
760 + $alg = MainWP_System_Utility::get_connect_sign_algorithm( $website );
761 + $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.
762 + }
763 +
764 + $signature = ! empty( $signature ) ? base64_encode( $signature ) : ''; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
765 +
766 + if ( null !== $sign_success && empty( $sign_success ) ) {
767 + $sign_error = '';
768 + while ( $msg = openssl_error_string() ) {
769 + if ( is_string( $msg ) ) {
770 + $sign_error .= $msg;
771 + }
536 772 }
773 + 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 );
537 774 }
538 - 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 );
775 + } else {
776 + $signature = base64_encode( 'useadvancedmainwpsignature' ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
539 777 }
540 778
541 779 $params = array(
542 780 'login_required' => 1,
@@ -545,8 +783,62 @@
545 783 'nonce' => $nonce,
546 784 $paramName => rawurlencode( $paramValue ),
547 785 );
548 786
787 + if ( false !== $alg ) {
788 + $params['sign_algo'] = $alg;
789 + }
790 +
791 + if ( $verify_signature ) {
792 + $ts = time();
793 + $data_sign_v2 = array(
794 + 'base_function' => $paramValue,
795 + 'where' => rawurlencode( $paramName ),
796 + 'nonce' => $nonce,
797 + 'expires' => $ts + 60,
798 + 'user' => $website->adminname,
799 + 'req_id' => wp_generate_uuid4(),
800 + );
801 +
802 + if ( ! empty( $alt_user ) ) {
803 + $data_sign_v2['alt_user'] = rawurlencode( $alt_user );
804 + }
805 +
806 + $sign_success_v2 = null;
807 +
808 + $sign_value_v2 = wp_json_encode( $data_sign_v2 );
809 +
810 + if ( MainWP_Connect_Lib::is_use_fallback_sec_lib( $website ) ) {
811 + $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.
812 + $use_seclib = true;
813 + } elseif ( function_exists( 'openssl_verify' ) ) {
814 + $alg = MainWP_System_Utility::get_connect_sign_algorithm( $website );
815 + $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.
816 + if ( false !== $alg ) {
817 + $params['sign_algo'] = $alg;
818 + }
819 + }
820 +
821 + $signature_v2 = ! empty( $signature_v2 ) ? base64_encode( $signature_v2 ) : ''; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
822 +
823 + $params['data_signature'] = rawurlencode( $sign_value_v2 );
824 + $params['mainwpsignature_adv'] = rawurlencode( $signature_v2 );
825 +
826 + if ( null !== $sign_success_v2 && empty( $sign_success_v2 ) ) {
827 + $sign_error = '';
828 + while ( $msg = openssl_error_string() ) {
829 + if ( is_string( $msg ) ) {
830 + $sign_error .= $msg;
831 + }
832 + }
833 + 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 );
834 + }
835 + }
836 +
837 + if ( ! empty( $alt_user ) ) {
838 + $params['alt_user'] = rawurlencode( $alt_user );
839 + }
840 +
549 841 if ( is_array( $other_params ) ) {
550 842 foreach ( $other_params as $name => $value ) {
551 843 if ( is_string( $name ) && ! empty( $name ) && is_scalar( $value ) ) {
552 844 $params[ sanitize_text_field( wp_unslash( $name ) ) ] = rawurlencode( sanitize_text_field( wp_unslash( $value ) ) );
@@ -566,30 +858,11 @@
566 858 }
567 859 }
568 860 }
569 861
570 - if ( false !== $alg ) {
571 - $params['sign_algo'] = $alg;
572 - }
573 -
574 862 if ( ! empty( $use_seclib ) ) {
575 863 $params['verifylib'] = 1;
576 864 }
577 -
578 - /**
579 - * Current user global.
580 - *
581 - * @global string
582 - */
583 - global $current_user;
584 -
585 - if ( ( ( ! defined( 'DOING_CRON' ) || false === DOING_CRON ) && ( ! defined( 'WP_CLI' ) || false === WP_CLI ) ) && $current_user && $current_user->ID ) {
586 - /** This filter is documented in ../class/class-mainwp-connect.php */
587 - $alter_user = apply_filters( 'mainwp_alter_login_user', false, $website->id, $current_user->ID );
588 - if ( ! empty( $alter_user ) ) {
589 - $params['alt_user'] = rawurlencode( $alter_user );
590 - }
591 - }
592 865 }
593 866
594 867 if ( $asArray ) {
595 868 return $params;
@@ -594,9 +867,13 @@
594 867 if ( $asArray ) {
595 868 return $params;
596 869 }
597 870
598 - $url = ( isset( $website->url ) && '' !== $website->url ? $website->url : $website->siteurl );
871 + if ( null !== $custom_url && '' !== $custom_url ) {
872 + $url = $custom_url;
873 + } else {
874 + $url = ( isset( $website->url ) && '' !== $website->url ? $website->url : $website->siteurl );
875 + }
599 876 $url .= ( substr( $url, - 1 ) !== '/' ? '/' : '' );
600 877 $url .= '?';
601 878
602 879 foreach ( $params as $key => $value ) {
@@ -621,8 +898,12 @@
621 898 public static function connect_sign( $data, &$signature, $privkey, $algorithm, $site_id ) {
622 899 $de_privkey = MainWP_Encrypt_Data_Lib::instance()->decrypt_privkey( $privkey, $site_id );
623 900
624 901 if ( empty( $de_privkey ) ) {
902 + MainWP_Logger::instance()->debug( 'Error: Failed to decrypt the priv key.' );
903 + }
904 +
905 + if ( empty( $de_privkey ) ) {
625 906 $de_privkey = $privkey; // compatible.
626 907 }
627 908 if ( false === $algorithm ) {
628 909 return openssl_sign( $data, $signature, $de_privkey ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
@@ -660,11 +941,30 @@
660 941 return null;
661 942 }
662 943
663 944 /**
945 + * Format a privacy-safe diagnostic for an unexpected Child response.
946 + *
947 + * @param mixed $website Website information.
948 + * @param mixed $data Raw response data.
949 + *
950 + * @return string Redacted diagnostic message.
951 + */
952 + private static function format_unexpected_response_log( $website, $data ) {
953 + $site_id = 0;
954 + if ( is_object( $website ) && property_exists( $website, 'id' ) && 0 < (int) $website->id ) {
955 + $site_id = (int) $website->id;
956 + }
957 +
958 + $response_bytes = is_string( $data ) ? strlen( $data ) : 0;
959 +
960 + return 'curl_multi_getcontent :: unexpected response :: [siteid=' . $site_id . '] :: [response_bytes=' . $response_bytes . ']';
961 + }
962 +
963 + /**
664 964 * Method fetch_urls_authed()
665 965 *
666 - * Fetch authorized URLs.
966 + * Fetches data from child sites if authenticated.
667 967 *
668 968 * @param object $websites Websites information.
669 969 * @param string $what Action to perform.
670 970 * @param array $params Request parameters.
@@ -687,9 +987,12 @@
687 987 if ( ! is_array( $params ) ) {
688 988 $params = array();
689 989 }
690 990
691 - $chunkSize = apply_filters( 'mainwp_fetch_urls_chunk_size', 10 );
991 + $sleep_int = (int) get_option( 'mainwp_chunksleepinterval', 5 );
992 + $chunkSize = (int) get_option( 'mainwp_chunksitesnumber', 10 );
993 +
994 + $chunkSize = apply_filters( 'mainwp_fetch_urls_chunk_size', $chunkSize );
692 995 if ( count( $websites ) > $chunkSize ) {
693 996 $total = count( $websites );
694 997 $loops = ceil( $total / $chunkSize );
695 998 for ( $i = 0; $i < $loops; $i++ ) {
@@ -694,9 +997,9 @@
694 997 $loops = ceil( $total / $chunkSize );
695 998 for ( $i = 0; $i < $loops; $i++ ) {
696 999 $newSites = array_slice( $websites, $i * $chunkSize, $chunkSize, true );
697 1000 static::fetch_urls_authed( $newSites, $what, $params, $handler, $output, $whatPage, $others );
698 - sleep( 5 );
1001 + sleep( $sleep_int );
699 1002 }
700 1003
701 1004 return false;
702 1005 }
@@ -703,9 +1006,26 @@
703 1006
704 1007 $agent = 'Mozilla/5.0 (compatible; MainWP/' . MainWP_System::$version . '; +http://mainwp.com)';
705 1008 $mh = curl_multi_init();
706 1009
707 - $timeout = 20 * 60 * 60;
1010 + /**
1011 + * Filter: mainwp_fetch_url_site_timeout
1012 + *
1013 + * Filters the request timeout ( CURLOPT_TIMEOUT + PHP time limit ) used
1014 + * for child site requests. Defaults to 20 hours to accommodate the
1015 + * longest operations ( upgrades, backups ); short-lived callers such as
1016 + * the URL-correction verify probe bound it much tighter.
1017 + *
1018 + * @param int $timeout Timeout in seconds. Default 72000 ( 20 hours ).
1019 + * Values below 1 are ignored ( 0 would disable the
1020 + * cURL timeout entirely ) and fall back to the default.
1021 + *
1022 + * @since 6.2
1023 + */
1024 + $timeout = (int) apply_filters( 'mainwp_fetch_url_site_timeout', 20 * 60 * 60 );
1025 + if ( $timeout <= 0 ) {
1026 + $timeout = 20 * 60 * 60;
1027 + }
708 1028
709 1029 $disabled_functions = ini_get( 'disable_functions' );
710 1030 $handleToWebsite = array();
711 1031 $requestUrls = array();
@@ -739,13 +1059,16 @@
739 1059 } else {
740 1060 $url .= 'admin-ajax.php';
741 1061 }
742 1062
1063 + $http_user = null;
1064 + $http_pass = null;
1065 +
743 1066 if ( property_exists( $website, 'http_user' ) ) {
744 - $http_user = $website->http_user;
1067 + $http_user = MainWP_Credential_Storage::decrypt_credential( $website->http_user );
745 1068 }
746 1069 if ( property_exists( $website, 'http_pass' ) ) {
747 - $http_pass = $website->http_pass;
1070 + $http_pass = MainWP_Credential_Storage::decrypt_credential( $website->http_pass );
748 1071 }
749 1072
750 1073 if ( isset( $params ) && isset( $params['new_post'] ) ) {
751 1074
@@ -808,9 +1131,10 @@
808 1131 // to fix.
809 1132 if ( defined( 'LOGGED_IN_SALT' ) && defined( 'NONCE_SALT' ) ) {
810 1133 $cookie_salt = sha1( sha1( 'mainwp' . LOGGED_IN_SALT . $website->id ) . NONCE_SALT . 'WP_Cookie' ); // NOSONAR - safe for salt file name.
811 1134 } else {
812 - $cookie_salt = sha1( sha1( 'mainwp' . $website->id ) . 'WP_Cookie' ); // NOSONAR - safe for salt file name.
1135 + // MWP-1558: misconfigured WP installs (no salts) previously used unsalted SHA1, which is enumerable. Fall back to the per-install MainWP filename secret instead.
1136 + $cookie_salt = MainWP_System_Utility::get_private_filename( 'cookies', $website->id, 'WP_Cookie' );
813 1137 }
814 1138 $cookieFile = $cookieDir . '/' . $cookie_salt;
815 1139 if ( ! file_exists( $cookieFile ) ) {
816 1140 @file_put_contents( $cookieFile, '' );
@@ -831,8 +1155,9 @@
831 1155 $postdata = static::get_post_data_authed( $website, $what, $params );
832 1156 curl_setopt( $ch, CURLOPT_POSTFIELDS, $postdata );
833 1157 curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, 10 );
834 1158 curl_setopt( $ch, CURLOPT_USERAGENT, $agent );
1159 + curl_setopt( $ch, CURLOPT_REFERER, get_option( 'siteurl' ) );
835 1160 curl_setopt( $ch, CURLOPT_ENCODING, 'none' );
836 1161 if ( ! empty( $http_user ) && ! empty( $http_pass ) ) {
837 1162 $http_pass = stripslashes( $http_pass );
838 1163 curl_setopt( $ch, CURLOPT_USERPWD, "$http_user:$http_pass" );
@@ -891,10 +1216,12 @@
891 1216 }
892 1217 }
893 1218
894 1219 if ( empty( $disabled_functions ) || ( false === stristr( $disabled_functions, 'curl_multi_exec' ) ) ) {
895 - $lastRun = 0;
1220 + $lastRun = 0;
1221 + $retry_added = false;
896 1222 do {
1223 + $retry_added = false;
897 1224 if ( 20 < time() - $lastRun ) {
898 1225 MainWP_System_Utility::set_time_limit( $timeout );
899 1226 $lastRun = time();
900 1227 }
@@ -905,16 +1232,32 @@
905 1232 $data = curl_multi_getcontent( $info['handle'] );
906 1233 $contains = ( 0 < preg_match( '/<mainwp>(.*)<\/mainwp>/', $data, $results ) );
907 1234 curl_multi_remove_handle( $mh, $info['handle'] );
908 1235
909 - if ( ! $contains && isset( $requestUrls[ static::get_resource_id( $info['handle'] ) ] ) ) {
910 - curl_setopt( $info['handle'], CURLOPT_URL, $requestUrls[ static::get_resource_id( $info['handle'] ) ] );
911 - curl_multi_add_handle( $mh, $info['handle'] );
912 - unset( $requestUrls[ static::get_resource_id( $info['handle'] ) ] );
913 - ++$running;
914 - continue;
1236 + $rid = static::get_resource_id( $info['handle'] );
1237 + if ( ! $contains && isset( $requestUrls[ $rid ] ) ) {
1238 + curl_setopt( $info['handle'], CURLOPT_URL, $requestUrls[ $rid ] );
1239 + curl_setopt( $info['handle'], CURLOPT_FRESH_CONNECT, true );
1240 + curl_setopt( $info['handle'], CURLOPT_FORBID_REUSE, true );
1241 + $add_retry = curl_multi_add_handle( $mh, $info['handle'] );
1242 + if ( CURLM_OK === $add_retry ) {
1243 + $mrc = curl_multi_exec( $mh, $running );
1244 +
1245 + if ( CURLM_OK === $mrc ) {
1246 + $retry_added = true;
1247 + unset( $requestUrls[ $rid ] );
1248 + continue; // libcurl updates $running automatically.
1249 + }
1250 +
1251 + curl_multi_remove_handle( $mh, $info['handle'] );
1252 + }
915 1253 }
916 1254
1255 + if ( ! $contains ) {
1256 + $log_website = isset( $handleToWebsite[ $rid ] ) ? $handleToWebsite[ $rid ] : null;
1257 + MainWP_Logger::instance()->debug( static::format_unexpected_response_log( $log_website, $data ) );
1258 + }
1259 +
917 1260 if ( null !== $handler ) {
918 1261 $site = &$handleToWebsite[ static::get_resource_id( $info['handle'] ) ];
919 1262 call_user_func_array( $handler, array( $data, $site, &$output, $params ) );
920 1263 }
@@ -919,17 +1262,17 @@
919 1262 call_user_func_array( $handler, array( $data, $site, &$output, $params ) );
920 1263 }
921 1264
922 1265 unset( $handleToWebsite[ static::get_resource_id( $info['handle'] ) ] );
923 - if ( 'resource' === gettype( $info['handle'] ) ) {
1266 + if ( static::is_valid_curl_handle( $info['handle'] ) ) {
924 1267 curl_close( $info['handle'] );
925 1268 }
926 1269 unset( $info['handle'] );
927 1270 }
928 1271 usleep( 10000 );
929 - } while ( $running > 0 );
1272 + } while ( $running > 0 || $retry_added );
930 1273
931 - if ( 'resource' === gettype( $mh ) ) {
1274 + if ( static::is_valid_curl_handle( $mh ) ) {
932 1275 curl_multi_close( $mh );
933 1276 }
934 1277 } else {
935 1278 foreach ( $requestHandles as $ch ) {
@@ -952,9 +1295,9 @@
952 1295 *
953 1296 * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD.
954 1297 * @return string The "Expect" header.
955 1298 */
956 - protected static function get_expect_header( $data ) {
1299 + public static function get_expect_header( $data ) {
957 1300 if ( ! is_array( $data ) ) {
958 1301 return strlen( (string) $data ) >= 1048576 ? '100-Continue' : '';
959 1302 }
960 1303
@@ -1088,8 +1431,9 @@
1088 1431 * @param bool $checkConstraints Whether or not to check constraints.
1089 1432 * @param bool $pForceFetch Whether or not to force the fetch.
1090 1433 * @param bool $pRetryFailed Whether or not to retry the fetch process.
1091 1434 * @param null $rawResponse Raw response.
1435 + * @param bool $verify_signature Wether verify signature data.
1092 1436 *
1093 1437 * @return mixed $information
1094 1438 *
1095 1439 * @uses \MainWP\Dashboard\MainWP_Monitoring_Handler::handle_check_website()
@@ -1102,9 +1446,10 @@
1102 1446 $params = null,
1103 1447 $checkConstraints = false,
1104 1448 $pForceFetch = false,
1105 1449 $pRetryFailed = true,
1106 - $rawResponse = null
1450 + $rawResponse = null,
1451 + $verify_signature = false
1107 1452 ) {
1108 1453 unset( $pForceFetch );
1109 1454
1110 1455 // to support demo data.
@@ -1111,8 +1456,42 @@
1111 1456 if ( MainWP_Demo_Handle::get_instance()->is_demo_website( $website ) ) {
1112 1457 return MainWP_Demo_Handle::get_instance()->handle_action_demo( $website, $what );
1113 1458 }
1114 1459
1460 + /**
1461 + * Filter to mock fetch_url_authed response before any HTTP/signing occurs.
1462 + *
1463 + * This filter fires early, before OpenSSL signing or HTTP requests, allowing
1464 + * tests to bypass child site communication entirely.
1465 + *
1466 + * SECURITY WARNING - TEST ONLY:
1467 + * This filter ONLY fires when ALL of the following conditions are met:
1468 + * 1. MAINWP_TESTING_MODE constant is defined and true
1469 + * 2. A PHPUnit test harness constant is present (WP_TESTS_DOMAIN, PHPUNIT_COMPOSER_INSTALL, or WP_TESTS_DIR)
1470 + *
1471 + * This triple-check prevents malicious code from defining MAINWP_TESTING_MODE
1472 + * in production to spoof child site responses.
1473 + *
1474 + * IMPORTANT: MAINWP_TESTING_MODE must ONLY be defined in the PHPUnit bootstrap
1475 + * file (tests/bootstrap.php). Defining it in production code, wp-config.php, or
1476 + * plugin files would create a security vulnerability allowing response spoofing.
1477 + *
1478 + * @since 5.4
1479 + *
1480 + * @param mixed $pre_result Return non-false to short-circuit and return this value.
1481 + * @param object $website Website object being communicated with.
1482 + * @param string $what Action being performed (e.g., 'plugin_action').
1483 + * @param array $params Request parameters.
1484 + * @return mixed Array to return early, false to proceed normally.
1485 + */
1486 + $is_phpunit_env = defined( 'WP_TESTS_DOMAIN' ) || defined( 'PHPUNIT_COMPOSER_INSTALL' ) || ( defined( 'WP_TESTS_DIR' ) && WP_TESTS_DIR );
1487 + if ( defined( 'MAINWP_TESTING_MODE' ) && MAINWP_TESTING_MODE && $is_phpunit_env ) {
1488 + $pre_result = apply_filters( 'mainwp_fetch_url_authed_pre', false, $website, $what, $params, $verify_signature );
1489 + if ( false !== $pre_result ) {
1490 + return $pre_result;
1491 + }
1492 + }
1493 +
1115 1494 if ( ! is_array( $params ) ) {
1116 1495 $params = array();
1117 1496 }
1118 1497
@@ -1120,10 +1499,8 @@
1120 1499 'force_use_ipv4' => $website->force_use_ipv4,
1121 1500 'upgrade' => ( 'upgradeplugintheme' === $what || 'upgrade' === $what || 'upgradetranslation' === $what ),
1122 1501 );
1123 1502
1124 - $request_update = MainWP_Premium_Update::maybe_request_premium_updates( $website, $what, $params );
1125 -
1126 1503 if ( isset( $rawResponse ) && $rawResponse ) {
1127 1504 $others['raw_response'] = 'yes';
1128 1505 }
1129 1506
@@ -1131,11 +1508,17 @@
1131 1508
1132 1509 $updating_website = false;
1133 1510 $type = '';
1134 1511 $list = '';
1135 - if ( 'upgradeplugintheme' === $what || 'upgrade' === $what || 'upgradetranslation' === $what ) {
1512 +
1513 + $premium_update = 'process_premium_updates' === $what && ! empty( $params['premium_perform'] ) && 'premium_update' === $params['premium_perform'] ? true : false;
1514 +
1515 + if ( 'upgradeplugintheme' === $what || 'upgrade' === $what || 'upgradetranslation' === $what || $premium_update ) {
1136 1516 $updating_website = true;
1137 - if ( 'upgradeplugintheme' === $what || 'upgradetranslation' === $what ) {
1517 + if ( $premium_update ) {
1518 + $type = $params['premium_type'];
1519 + $list = $params['list'];
1520 + } elseif ( 'upgradeplugintheme' === $what || 'upgradetranslation' === $what ) {
1138 1521 $type = $params['type'];
1139 1522 $list = $params['list'];
1140 1523 } else {
1141 1524 $type = 'wp';
@@ -1157,36 +1540,69 @@
1157 1540 */
1158 1541 do_action( 'mainwp_website_before_updated', $website, $type, $list );
1159 1542 }
1160 1543
1544 + $information = array();
1545 + $output = array();
1546 +
1547 + if ( 'stats' === $what || ( 'upgradeplugintheme' === $what && isset( $params['type'] ) ) ) {
1548 + $request_update = MainWP_Premium_Update::maybe_request_premium_updates( $website, $what, $params, $output_result );
1549 + if ( $request_update ) {
1550 + // Return the information here.
1551 + return $output_result;
1552 + }
1553 + }
1554 +
1555 + // MWP-1548: decrypt http_user / http_pass before they hit the
1556 + // outbound HTTP Basic Auth header. Legacy plaintext rows pass
1557 + // through unchanged.
1558 + $http_user_plain = MainWP_Credential_Storage::decrypt_credential( $website->http_user );
1559 + $http_pass_plain = MainWP_Credential_Storage::decrypt_credential( $website->http_pass );
1560 +
1161 1561 if ( 'renew' === $what ) {
1162 - $postdata = static::get_renew_post_data_authed( $website, $what );
1562 + $postdata = static::get_renew_post_data_authed(
1563 + $website,
1564 + $what,
1565 + array(
1566 + 'verify_signature' => $verify_signature,
1567 + )
1568 + );
1163 1569 } else {
1164 - $postdata = static::get_post_data_authed( $website, $what, $params );
1570 + $postdata = static::get_post_data_authed(
1571 + $website,
1572 + $what,
1573 + $params,
1574 + array(
1575 + 'verify_signature' => $verify_signature,
1576 + 'http_user_plain' => $http_user_plain,
1577 + 'http_pass_plain' => $http_pass_plain,
1578 + )
1579 + );
1165 1580
1166 1581 }
1582 +
1167 1583 $others['function'] = $what;
1168 1584
1169 - $information = array();
1585 + $information = static::fetch_url( $website, $website->url, $postdata, $checkConstraints, $website->verify_certificate, $pRetryFailed, $http_user_plain, $http_pass_plain, $website->ssl_version, $others, $output );
1170 1586
1171 - if ( ! $request_update ) {
1172 - $information = static::fetch_url( $website, $website->url, $postdata, $checkConstraints, $website->verify_certificate, $pRetryFailed, $website->http_user, $website->http_pass, $website->ssl_version, $others );
1173 - /**
1174 - * Fires immediately after fetch url action.
1175 - *
1176 - * @param object $website website.
1177 - * @param array $information information result data.
1178 - * @param string $what action.
1179 - * @param array $params params input array.
1180 - * @param array $others others input array.
1181 - *
1182 - * @since 4.5.1.1
1183 - */
1184 - do_action( 'mainwp_fetch_url_authed', $website, $information, $what, $params, $others );
1185 - } else {
1186 - $slug = $params['list'];
1187 - $information['upgrades'] = array( $slug => 1 );
1587 + if ( ! empty( $output ) ) {
1588 + if ( ! is_array( $information ) ) {
1589 + $information = array();
1590 + }
1591 + $information['fetch_url_output'] = $output;
1188 1592 }
1593 + /**
1594 + * Fires immediately after fetch url action.
1595 + *
1596 + * @param object $website website.
1597 + * @param array $information information result data.
1598 + * @param string $what action.
1599 + * @param array $params params input array.
1600 + * @param array $others others input array.
1601 + *
1602 + * @since 4.5.1.1
1603 + */
1604 + do_action( 'mainwp_fetch_url_authed', $website, $information, $what, $params, $others );
1189 1605
1190 1606 if ( is_array( $information ) && isset( $information['sync'] ) && ! empty( $information['sync'] ) ) {
1191 1607 MainWP_Sync::sync_information_array( $website, $information['sync'] );
1192 1608 unset( $information['sync'] );
@@ -1206,9 +1622,9 @@
1206 1622 * @since Unknown
1207 1623 */
1208 1624 do_action( 'mainwp_website_updated', $website, $type, $list, $information );
1209 1625 if ( 1 === (int) get_option( 'mainwp_check_http_response', 0 ) ) {
1210 - MainWP_Monitoring_Handler::handle_check_website( $website );
1626 + MainWP_Monitoring_Handler::handle_check_website( $website, true );
1211 1627 }
1212 1628 }
1213 1629
1214 1630 return $information;
@@ -1232,9 +1648,9 @@
1232 1648 * @param array $output Output values.
1233 1649 *
1234 1650 * @return mixed static::fetch_url() Fetch URL.
1235 1651 */
1236 - public static function fetch_url_not_authed( // NOSONAR - compatible.
1652 + public static function fetch_url_not_authed( // phpcs:ignore -- NOSONAR - compatible.
1237 1653 $url,
1238 1654 $admin,
1239 1655 $what,
1240 1656 $params = null,
@@ -1360,8 +1776,41 @@
1360 1776 $others = array(),
1361 1777 &$output = array()
1362 1778 ) {
1363 1779
1780 + /**
1781 + * Enables data to be returned prior to connecting to the site.
1782 + *
1783 + * Dev/test override only. Gated behind the MAINWP_DEV_FILTERS_ENABLED
1784 + * constant so the filter does not dispatch in production. The filter
1785 + * receives plaintext HTTP Basic Auth credentials and the full $website
1786 + * DB row (including privkey); enabling it in production would expose
1787 + * those values to any 3rd-party plugin hooking the filter.
1788 + *
1789 + * To enable in a dev/test environment, add to wp-config.php:
1790 + * define( 'MAINWP_DEV_FILTERS_ENABLED', true );
1791 + *
1792 + * @since 5.5
1793 + *
1794 + * @param mixed false
1795 + * @param mixed $website
1796 + * @param mixed $url
1797 + * @param mixed $postdata
1798 + * @param mixed $checkConstraints
1799 + * @param mixed $verifyCertificate
1800 + * @param mixed $http_user
1801 + * @param mixed $http_pass
1802 + * @param mixed $sslVersion
1803 + * @param mixed $others
1804 + * @param mixed $output
1805 + */
1806 + if ( defined( 'MAINWP_DEV_FILTERS_ENABLED' ) && MAINWP_DEV_FILTERS_ENABLED ) {
1807 + $dev_data = apply_filters( 'mainwp_dev_return_data_before_connect_site', false, $website, $url, $postdata, $checkConstraints, $verifyCertificate, $http_user, $http_pass, $sslVersion, $others, $output );
1808 + if ( false !== $dev_data ) {
1809 + return $dev_data;
1810 + }
1811 + }
1812 +
1364 1813 $agent = 'Mozilla/5.0 (compatible; MainWP/' . MainWP_System::$version . '; +http://mainwp.com)';
1365 1814
1366 1815 if ( ! empty( $website ) ) {
1367 1816 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url_site', 'Request to [' . $url . '] [' . MainWP_Utility::value_to_string( $postdata, 1 ) . ']' );
@@ -1384,8 +1833,10 @@
1384 1833 $cookieDir = $dirs[0] . 'cookies';
1385 1834
1386 1835 static::init_cookiesdir( $cookieDir );
1387 1836
1837 + $fetch_track_id = MainWP_Execution_Helper::execute_call_track( 'start_point', $website, $postdata );
1838 +
1388 1839 $ch = curl_init();
1389 1840
1390 1841 $proxy = new \WP_HTTP_Proxy();
1391 1842 if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) {
@@ -1403,9 +1854,10 @@
1403 1854 // to fix.
1404 1855 if ( defined( 'LOGGED_IN_SALT' ) && defined( 'NONCE_SALT' ) ) {
1405 1856 $cookie_salt = sha1( sha1( 'mainwp' . LOGGED_IN_SALT . $website->id ) . NONCE_SALT . 'WP_Cookie' ); // NOSONAR - safe for salt file name.
1406 1857 } else {
1407 - $cookie_salt = sha1( sha1( 'mainwp' . $website->id ) . 'WP_Cookie' ); // NOSONAR - safe for salt file name.
1858 + // MWP-1558: misconfigured WP installs (no salts) previously used unsalted SHA1, which is enumerable. Fall back to the per-install MainWP filename secret instead.
1859 + $cookie_salt = MainWP_System_Utility::get_private_filename( 'cookies', $website->id, 'WP_Cookie' );
1408 1860 }
1409 1861 $cookieFile = $cookieDir . '/' . $cookie_salt;
1410 1862 if ( ! file_exists( $cookieFile ) ) {
1411 1863 @file_put_contents( $cookieFile, '' );
@@ -1473,8 +1925,10 @@
1473 1925
1474 1926 $headers = array( 'X-Requested-With' => 'XMLHttpRequest' );
1475 1927 $headers['Expect'] = static::get_expect_header( $postdata );
1476 1928
1929 + $headers = apply_filters( 'mainwp_connect_http_request_headers', $headers, $website );
1930 +
1477 1931 if ( class_exists( '\WpOrg\Requests\Requests' ) ) {
1478 1932 $headers = \WpOrg\Requests\Requests::flatten( $headers );
1479 1933 } else {
1480 1934 $headers = \Requests::flatten( $headers );
@@ -1500,9 +1954,25 @@
1500 1954 if ( $force_use_ipv4 && defined( 'CURLOPT_IPRESOLVE' ) && defined( 'CURL_IPRESOLVE_V4' ) ) {
1501 1955 curl_setopt( $ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4 );
1502 1956 }
1503 1957
1504 - $timeout = 20 * 60 * 60;
1958 + $what = '';
1959 + if ( is_array( $others ) && isset( $others['function'] ) ) {
1960 + $what = $others['function'];
1961 + }
1962 +
1963 + if ( 'deactivate' === $what ) {
1964 + $timeout = 120; // 2 minutes.
1965 + } else {
1966 + $timeout = 20 * 60 * 60;
1967 + }
1968 +
1969 + /** This filter is documented in class/class-mainwp-connect.php */
1970 + $timeout = (int) apply_filters( 'mainwp_fetch_url_site_timeout', $timeout, $what );
1971 + if ( $timeout <= 0 ) {
1972 + $timeout = 20 * 60 * 60; // values below 1 would disable the cURL timeout entirely.
1973 + }
1974 +
1505 1975 curl_setopt( $ch, CURLOPT_TIMEOUT, $timeout );
1506 1976 MainWP_System_Utility::set_time_limit( $timeout );
1507 1977
1508 1978 MainWP_Utility::end_session();
@@ -1514,27 +1984,38 @@
1514 1984 $mh = @curl_multi_init();
1515 1985 @curl_multi_add_handle( $mh, $ch );
1516 1986
1517 1987 $lastRun = 0;
1988 + $running = null;
1989 +
1518 1990 do {
1519 1991 if ( 20 < time() - $lastRun ) {
1520 1992 MainWP_System_Utility::set_time_limit( $timeout );
1521 1993 $lastRun = time();
1522 1994 }
1523 - @curl_multi_exec( $mh, $running );
1524 - @curl_multi_select( $mh );
1995 +
1996 + do {
1997 + $mrc = curl_multi_exec( $mh, $running );
1998 + } while ( CURLM_CALL_MULTI_PERFORM === $mrc );
1999 +
2000 + $rc = curl_multi_select( $mh, 1.0 );
2001 + if ( -1 === $rc ) {
2002 + usleep( 100000 );
2003 + }
2004 +
1525 2005 while ( $info = @curl_multi_info_read( $mh ) ) {
1526 - $data = @curl_multi_getcontent( $info['handle'] );
1527 -
2006 + $data = @curl_multi_getcontent( $info['handle'] );
1528 2007 $http_status = @curl_getinfo( $info['handle'], CURLINFO_HTTP_CODE );
1529 2008 $err = @curl_error( $info['handle'] );
1530 2009 $real_url = @curl_getinfo( $info['handle'], CURLINFO_EFFECTIVE_URL );
1531 2010
1532 2011 @curl_multi_remove_handle( $mh, $info['handle'] );
2012 + curl_close( $info['handle'] );
1533 2013 }
1534 2014 usleep( 10000 );
1535 2015 } while ( $running > 0 );
1536 - if ( 'resource' === gettype( $mh ) ) {
2016 +
2017 + if ( static::is_valid_curl_handle( $mh ) ) {
1537 2018 @curl_multi_close( $mh );
1538 2019 }
1539 2020 } else {
1540 2021 $data = @curl_exec( $ch );
@@ -1540,13 +2021,16 @@
1540 2021 $data = @curl_exec( $ch );
1541 2022 $http_status = @curl_getinfo( $ch, CURLINFO_HTTP_CODE );
1542 2023 $err = @curl_error( $ch );
1543 2024 $real_url = @curl_getinfo( $ch, CURLINFO_EFFECTIVE_URL );
2025 + curl_close( $ch );
1544 2026 }
1545 2027
1546 2028 $host = wp_parse_url( $real_url, PHP_URL_HOST );
1547 2029 $ip = gethostbyname( $host );
1548 2030
2031 + MainWP_Execution_Helper::execute_call_track( 'end_point', $website, $postdata, $fetch_track_id, 'fetch site' );
2032 +
1549 2033 if ( null !== $website ) {
1550 2034 MainWP_DB_Common::instance()->insert_or_update_request_log( $website->id, $ip, null, microtime( true ) );
1551 2035 }
1552 2036
@@ -1551,9 +2035,20 @@
1551 2035 }
1552 2036
1553 2037 $raw_response = isset( $others['raw_response'] ) && 'yes' === $others['raw_response'] ? true : false;
1554 2038
1555 - $output['fetch_data'] = $data;
2039 + $hidden_data = '[hidden response data]';
2040 +
2041 + if ( ! apply_filters( 'mainwp_hide_raw_connection_response_data', true ) ) {
2042 + $hidden_data = $data;
2043 + }
2044 +
2045 + if ( ! is_array( $output ) ) {
2046 + $output = array();
2047 + }
2048 +
2049 + $output['fetch_data'] = $hidden_data;
2050 +
1556 2051 $output['http_status'] = (int) $http_status;
1557 2052
1558 2053 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url_site', 'http status: [' . $http_status . '] err: [' . $err . ']' );
1559 2054 if ( '400' === $http_status ) {
@@ -1563,39 +2058,85 @@
1563 2058 MainWP_Logger::instance()->log_execution_time( 'fetch_url_site :: [url=' . $url . ']' );
1564 2059
1565 2060 $thr_error = null;
1566 2061
2062 + if ( in_array( $what, array( 'installplugintheme', 'upgradeplugintheme', 'upgradetranslation', 'upgrade', 'stats', 'renew', 'reconnect' ), true ) ) {
2063 + MainWP_Cache_Helper::invalidate_cache_group( MainWP_Cache_Helper::CGR_UPDATES );
2064 + MainWP_Cache_Warm_Helper::invalidate_pages_by_site_actions( $what );
2065 + }
2066 +
2067 + if ( 'process_premium_updates' === $what ) {
2068 + MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url_site', 'function: [process_premium_updates] response data: [' . MainWP_Utility::value_to_string( $data ) . ']' );
2069 + }
2070 +
1567 2071 if ( ( false === $data ) && empty( $http_status ) ) {
1568 2072 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url', '[' . $url . '] HTTP Error: [status=0][' . $err . ']' );
1569 - $thr_error = new MainWP_Exception( 'HTTPERROR', $err ); //phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
2073 + $thr_error = new MainWP_Exception( 'HTTPERROR', $err ); //phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
2074 + $output['error_category'] = 'http_error';
1570 2075 } elseif ( empty( $data ) && ! empty( $err ) ) {
1571 2076 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url', '[' . $url . '] HTTP Error: [status=' . $http_status . '][' . $err . ']' );
1572 - $thr_error = new MainWP_Exception( 'HTTPERROR', $err ); //phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
2077 + $thr_error = new MainWP_Exception( 'HTTPERROR', $err ); //phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
2078 + $output['error_category'] = 'http_error';
2079 + $output['error_code'] = 'http_request_failed';
2080 + $output['error_message'] = $err;
1573 2081 } elseif ( 0 < preg_match( '/<mainwp>(.*)<\/mainwp>/', $data, $results ) ) {
1574 - $result = $results[1];
1575 - $information = MainWP_System_Utility::get_child_response( base64_decode( $result ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
2082 + $output['connection_step'] = 'verify_credentials';
2083 + $result = $results[1];
2084 + $information = MainWP_System_Utility::get_child_response( base64_decode( $result ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
1576 2085 unset( $output['fetch_data'] ); // hide the data.
1577 2086 $pdt = is_string( $postdata ) ? $postdata : '';
1578 2087 $data_log = is_array( $postdata ) ? print_r( $postdata, true ) : $pdt; //phpcs:ignore -- good.
1579 2088 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url_site', '[' . $url . '] postdata [' . $data_log . '] information: [OK]' ); //phpcs:ignore -- ok.
2089 +
2090 + $error_code = is_array( $information ) && isset( $information['error_code'] ) ? sanitize_text_field( wp_unslash( $information['error_code'] ) ) : '';
2091 + if ( ! empty( $error_code ) ) {
2092 + $output['child_error_code'] = $error_code;
2093 + }
2094 +
2095 + if ( is_array( $information ) && $website ) {
2096 + // Process to ensure compatibility with old and new Child versions.
2097 + $new_sync = isset( $information['support_advanced_sign'] );
2098 + $old_sync = ! $new_sync && ( isset( $information['mainwpdir'], $information['uniqueId'] ) || isset( $information['version'], $information['wpversion'], $information['wpe'] ) );
2099 + if ( $new_sync || $old_sync ) {
2100 + $adv_sign_support = MainWP_DB::instance()->get_website_option( $website, 'support_advanced_sign' );
2101 + if ( ! empty( $information['support_advanced_sign'] ) && 0 === (int) $adv_sign_support ) {
2102 + MainWP_DB::instance()->update_website_option( $website, 'support_advanced_sign', 1 );
2103 + } elseif ( empty( $information['support_advanced_sign'] ) && 1 === (int) $adv_sign_support ) {
2104 + MainWP_DB::instance()->update_website_option( $website, 'support_advanced_sign', 0 );
2105 + }
2106 + }
2107 + }
2108 +
2109 + if ( 'process_premium_updates' === $what ) {
2110 + MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url_site', 'function: [process_premium_updates] decoded data: [' . MainWP_Utility::value_to_string( $information ) . ']' );
2111 + }
2112 +
1580 2113 return $information;
1581 2114 } elseif ( 200 === (int) $http_status && ! empty( $err ) ) {
1582 - $thr_error = new MainWP_Exception( 'HTTPERROR', $err ); //phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
2115 + $thr_error = new MainWP_Exception( 'HTTPERROR', $err ); //phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
2116 + $output['error_category'] = 'http_error';
2117 + $output['error_code'] = 'http_request_failed';
2118 + $output['error_message'] = $err;
2119 + MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url_site', '[' . $url . '] error [' . $err . ']' ); //phpcs:ignore -- ok.
1583 2120 } elseif ( $raw_response ) {
1584 2121 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url_site', 'Response: [RAW]' );
1585 2122 return $data;
1586 2123 } else {
1587 - MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url', '[' . $url . '] Error: NOMAINWP [data=' . ( is_string( $data ) ? $data : 'OBJECT' ) . ']' );
2124 + MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url', '[' . $url . '] Error: NOMAINWP [data=' . $hidden_data . ']' );
1588 2125 $detect_wsidchk = is_string( $data ) ? strpos( $data, 'wsidchk' ) : false;
1589 2126 if ( false !== $detect_wsidchk ) {
1590 - $thr_error = new MainWP_Exception( 'ERROR:Connection Failed. We suspect that Imunify360, a security layer added by your host, is causing this problem. Please contact your host to whitelist your Dashboard IP in their system. If you need help determining your MainWP Dashboard site IP address, check with your hosting provider.', $url );
2127 + $err_msg = 'Connection Failed. We suspect that Imunify360, a security layer added by your host, is causing this problem. Please contact your host to whitelist your Dashboard IP in their system. If you need help determining your MainWP Dashboard site IP address, check with your hosting provider.';
2128 + $thr_error = new MainWP_Exception( 'ERROR:' . $err_msg, $url );
1591 2129 } else {
1592 2130 $thr_error = new MainWP_Exception( 'NOMAINWP', $url ); //phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
2131 + $err_msg = 'Connection Failed. Please ensure that the MainWP Child plugin is installed and activated on the child site.';
1593 2132 }
2133 + $output['error_category'] = 'child_plugin_missing';
2134 + $output['error_message'] = $err_msg;
1594 2135 }
1595 2136
1596 2137 if ( null !== $thr_error ) {
1597 - $thr_error->set_data( $data );
2138 + $thr_error->set_data( $hidden_data ); // to compatible.
1598 2139 throw $thr_error;
1599 2140 }
1600 2141 }
1601 2142
@@ -1683,9 +2224,9 @@
1683 2224 $lastRequest = MainWP_DB_Common::instance()->get_last_request_timestamp( $ip );
1684 2225 if ( $lastRequest > ( ( microtime( true ) ) - $minimumDelay ) ) {
1685 2226 static::release( $identifier );
1686 2227 $sleep = ( $minimumDelay - ( ( microtime( true ) ) - $lastRequest ) ) * 1000 * 1000;
1687 - $sleep = intval( $sleep );
2228 + $sleep = max( 0, intval( $sleep ) );
1688 2229 usleep( $sleep );
1689 2230 return true;
1690 2231 }
1691 2232 return false;
@@ -1744,9 +2285,9 @@
1744 2285 $wp_filesystem->delete( $file );
1745 2286 }
1746 2287
1747 2288 if ( ! $wp_filesystem->exists( dirname( $file ) ) ) {
1748 - $wp_filesystem->mkdir( dirname( $file ), 0777 );
2289 + $wp_filesystem->mkdir( dirname( $file ), 0750 ); // MWP-1558: tightened from 0777; downloaded files may contain backup data.
1749 2290 }
1750 2291
1751 2292 if ( ! $wp_filesystem->exists( dirname( $file ) ) ) {
1752 2293 throw new MainWP_Exception( esc_html__( 'MainWP plugin could not create directory in order to download the file.', 'mainwp' ) );
@@ -1791,9 +2332,9 @@
1791 2332 $http_pass = stripslashes( $http_pass );
1792 2333 curl_setopt( $ch, CURLOPT_USERPWD, "$http_user:$http_pass" );
1793 2334 }
1794 2335 curl_exec( $ch );
1795 - if ( 'resource' === gettype( $ch ) ) {
2336 + if ( static::is_valid_curl_handle( $ch ) ) {
1796 2337 curl_close( $ch );
1797 2338 }
1798 2339 fclose( $fp );
1799 2340 }
@@ -1823,9 +2364,9 @@
1823 2364
1824 2365 if ( $hasWPFileSystem && ! empty( $wp_filesystem ) ) {
1825 2366
1826 2367 if ( ! $wp_filesystem->is_dir( $cookieDir ) ) {
1827 - $wp_filesystem->mkdir( $cookieDir, 0777 );
2368 + $wp_filesystem->mkdir( $cookieDir, 0750 ); // MWP-1558: tightened from 0777; cookies/ holds child wp-admin session cookies.
1828 2369 }
1829 2370
1830 2371 if ( ! file_exists( $cookieDir . '/.htaccess' ) ) {
1831 2372 $file_htaccess = $cookieDir . '/.htaccess';
@@ -1838,9 +2379,9 @@
1838 2379 }
1839 2380 } else {
1840 2381
1841 2382 if ( ! file_exists( $cookieDir ) ) {
1842 - @mkdir( $cookieDir, 0777, true );
2383 + @mkdir( $cookieDir, 0750, true ); // MWP-1558: tightened from 0777; cookies/ holds child wp-admin session cookies.
1843 2384 }
1844 2385
1845 2386 if ( ! file_exists( $cookieDir . '/.htaccess' ) ) {
1846 2387 $file_htaccess = @fopen( $cookieDir . '/.htaccess', 'w+' );
@@ -1889,9 +2430,9 @@
1889 2430 curl_setopt( $ch, CURLOPT_ENCODING, 'none' );
1890 2431
1891 2432 $data = @curl_exec( $ch );
1892 2433 $httpCode = @curl_getinfo( $ch, CURLINFO_HTTP_CODE );
1893 - if ( 'resource' === gettype( $ch ) ) {
2434 + if ( static::is_valid_curl_handle( $ch ) ) {
1894 2435 curl_close( $ch );
1895 2436 }
1896 2437 if ( 200 === (int) $httpCode ) {
1897 2438 return $data;
@@ -1897,8 +2438,22 @@
1897 2438 return $data;
1898 2439 } else {
1899 2440 return false;
1900 2441 }
2442 + }
2443 +
2444 + /**
2445 + * Method is_valid_curl_handle
2446 + *
2447 + * @param mixed $ch cURL handle to validate.
2448 + * @return bool Valid curl handle.
2449 + */
2450 + public static function is_valid_curl_handle( $ch ) {
2451 + return is_resource( $ch )
2452 + || ( is_object( $ch )
2453 + && class_exists( 'CurlHandle', false )
2454 + && $ch instanceof \CurlHandle
2455 + );
1901 2456 }
1902 2457
1903 2458 /**
1904 2459 * Method get_favico_url()