PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / 6.2.1
MainWP Dashboard: Self-hosted WordPress Management for Agencies v6.2.1
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 +866 -193 5.1 → 6.2.1 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 ) {
@@ -134,27 +143,47 @@
134 143 if ( $force_use_ipv4 && defined( 'CURLOPT_IPRESOLVE' ) && defined( 'CURL_IPRESOLVE_V4' ) ) {
135 144 curl_setopt( $ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4 );
136 145 }
137 146
147 + MainWP_Logger::instance()->debug( ' :: trying Visit :: [url=' . $url . ']' );
148 +
149 + $http_version = false;
150 +
138 151 $disabled_functions = ini_get( 'disable_functions' );
139 152 if ( empty( $disabled_functions ) || ( stristr( $disabled_functions, 'curl_multi_exec' ) === false ) ) {
153 + MainWP_Logger::instance()->debug( ' :: trying Visit :: curl_multi_exec => enabled.' );
140 154 $mh = curl_multi_init();
141 155 @curl_multi_add_handle( $mh, $ch );
142 156
143 157 do {
144 - curl_multi_exec( $mh, $running );
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 +
145 169 while ( $info = curl_multi_info_read( $mh ) ) {
146 170 $data = curl_multi_getcontent( $info['handle'] );
147 171 $err = curl_error( $info['handle'] );
148 172 $http_status = curl_getinfo( $info['handle'], CURLINFO_HTTP_CODE );
149 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 + }
150 177
151 178 curl_multi_remove_handle( $mh, $info['handle'] );
179 + curl_close( $info['handle'] );
152 180 }
153 181 usleep( 10000 );
182 +
154 183 } while ( $running > 0 );
155 184
156 - if ( 'resource' === gettype( $mh ) ) {
185 + if ( static::is_valid_curl_handle( $mh ) ) {
157 186 curl_multi_close( $mh );
158 187 }
159 188 } else {
160 189 $data = curl_exec( $ch );
@@ -160,14 +189,18 @@
160 189 $data = curl_exec( $ch );
161 190 $err = curl_error( $ch );
162 191 $http_status = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
163 192 $realurl = curl_getinfo( $ch, CURLINFO_EFFECTIVE_URL );
164 - 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 ) ) {
165 199 curl_close( $ch );
166 200 }
167 201 }
168 202
169 - MainWP_Logger::instance()->debug( ' :: tryVisit :: [url=' . $url . '] [http_status=' . $http_status . '] [error=' . $err . '] [data-start]' . $data . '[data-end]' );
170 203 MainWP_Logger::instance()->log_execution_time( 'tryVisit :: [url=' . $url . '] [http_status=' . $http_status . ']' );
171 204
172 205 $host = wp_parse_url( ( empty( $realurl ) ? $url : $realurl ), PHP_URL_HOST );
173 206 $ip = false;
@@ -172,10 +205,15 @@
172 205 $host = wp_parse_url( ( empty( $realurl ) ? $url : $realurl ), PHP_URL_HOST );
173 206 $ip = false;
174 207 $target = false;
175 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.
176 214 $found = false;
177 - $dnsRecord = @dns_get_record( $host );
215 + $dnsRecord = @dns_get_record( $host, DNS_A | DNS_AAAA | DNS_CNAME );
178 216 MainWP_Logger::instance()->debug( ' :: tryVisit :: [dnsRecord=' . MainWP_Utility::value_to_string( $dnsRecord, 1 ) . ']' );
179 217
180 218 if ( false !== $dnsRecord && is_array( $dnsRecord ) ) {
181 219 if ( ! isset( $dnsRecord['ip'] ) ) {
@@ -217,10 +255,19 @@
217 255 $out = array(
218 256 'host' => $host,
219 257 'httpCode' => $http_status,
220 258 'httpCodeString' => MainWP_Utility::get_http_codes( $http_status ),
259 + 'httpVersion' => $http_version,
221 260 );
222 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 +
223 270 if ( false !== $ip ) {
224 271 $out['ip'] = $ip;
225 272 $found = true;
226 273 }
@@ -229,24 +276,59 @@
229 276
230 277 return $out;
231 278 }
232 279
280 +
233 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 + /**
234 296 * Method check_ignored_http_code()
235 297 *
236 298 * Check if http error code is being ignored.
237 299 *
238 - * @param mixed $value http error code.
300 + * @param mixed $value http error code.
301 + * @param object|false $website website.
239 302 *
240 303 * @return bolean True|False.
241 304 */
242 - public static function check_ignored_http_code( $value ) {
243 - $value = (int) $value;
244 - if ( 200 === $value ) {
245 - return true;
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();
309 +
310 + $ignored_code = '';
311 +
312 + if ( $site_id ) {
313 +
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();
316 +
317 + $mo_active = 0;
318 + if ( $primary_monitor ) {
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 );
324 + } else {
325 + $ignored_code = is_array( $global_settings ) && isset( $global_settings['up_status_codes'] ) ? $global_settings['up_status_codes'] : '';
326 + }
327 + } else {
328 + $ignored_code = ! empty( $global_settings['up_status_codes'] ) ? $global_settings['up_status_codes'] : '';
246 329 }
247 - $ignored_code = get_option( 'mainwp_ignore_HTTP_response_status', '' );
248 - $ignored_code = trim( $ignored_code );
330 +
249 331 if ( ! empty( $ignored_code ) ) {
250 332 $ignored_code = explode( ',', $ignored_code );
251 333 foreach ( $ignored_code as $code ) {
252 334 $code = trim( $code );
@@ -258,19 +340,35 @@
258 340 return false;
259 341 }
260 342
261 343 /**
262 - * Method check_website_status()
344 + * Method check website status.
263 345 *
264 346 * Check if the Website returns and http errors.
265 347 *
266 - * @param array $website Child Site information.
348 + * @param object $website Child Site information.
349 + * @param bool $chk_http_site Check site http response.
267 350 *
268 351 * @return mixed False|try visit result.
269 352 *
270 353 * @uses \MainWP\Dashboard\MainWP_Utility::is_domain_valid()
271 354 */
272 - public static function check_website_status( $website ) {
355 + public static function check_website_status( $website, $chk_http_site = false ) { //phpcs:ignore -- NOSONAR - complexity.
356 +
357 + if ( is_object( $website ) && isset( $website->id ) ) {
358 + $primary_monitor = MainWP_DB_Uptime_Monitoring::instance()->get_monitor_by( $website->id, 'issub', 0 );
359 + if ( $primary_monitor ) {
360 + // return compatible uptime status here.
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.
368 + }
369 + }
370 +
273 371 $http_user = null;
274 372 $http_pass = null;
275 373 $sslVersion = null;
276 374 $verifyCertificate = null;
@@ -278,11 +376,14 @@
278 376 if ( is_object( $website ) && isset( $website->url ) ) {
279 377 $url = $website->url;
280 378 $verifyCertificate = isset( $website->verify_certificate ) ? (int) $website->verify_certificate : null;
281 379 $forceUseIPv4 = $website->force_use_ipv4;
282 - $http_user = $website->http_user;
283 - $http_pass = $website->http_pass;
284 - $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;
285 386 } else {
286 387 $url = $website;
287 388 }
288 389
@@ -311,19 +412,33 @@
311 412 *
312 413 * @param mixed $website Array of Child Site Info.
313 414 * @param mixed $what What we are posting.
314 415 * @param null $params Post parameters.
416 + * @param array $others Other data.
315 417 *
316 418 * @return mixed null|http_build_query()
317 419 */
318 - 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 +
319 430 if ( $website && '' !== $what ) {
320 - $data = array();
321 - $data['user'] = $website->adminname;
322 - $data['function'] = $what;
323 - $data['nonce'] = wp_rand( 0, 9999 );
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.
434 + $data = array();
435 + $data['user'] = $website->adminname;
436 + $data['function'] = $what;
437 + $data['nonce'] = bin2hex( random_bytes( 16 ) );
438 + $data['mainwpver'] = MainWP_System::$version;
324 439
325 - $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 );
326 441 if ( is_array( $params_filter ) && ! empty( $params_filter ) ) {
327 442 $data = array_merge( $data, $params_filter );
328 443 }
329 444
@@ -334,47 +449,12 @@
334 449 $alg = false;
335 450 $sign_success = null;
336 451 $use_seclib = false;
337 452
338 - $data = apply_filters( 'mainwp_get_post_data_authed', $data, $website, $what, $params );
339 - if ( MainWP_Connect_Lib::is_use_fallback_sec_lib( $website ) ) {
340 - $sign_success = MainWP_Connect_Lib::connect_sign( $what . $data['nonce'], $signature, base64_decode( $website->privkey ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
341 - $use_seclib = true;
342 - } elseif ( function_exists( 'openssl_verify' ) ) {
343 - $alg = MainWP_System_Utility::get_connect_sign_algorithm( $website );
344 - $sign_success = static::connect_sign( $what . $data['nonce'], $signature, base64_decode( $website->privkey ), $alg ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
345 - if ( false !== $alg ) {
346 - $data['sign_algo'] = $alg;
347 - }
348 - }
453 + $data = apply_filters( 'mainwp_get_post_data_authed', $data, $website, $what, $params, $verify_signature );
349 454
350 - if ( $use_seclib ) {
351 - $data['verifylib'] = 1;
352 - }
455 + $alt_user = '';
353 456
354 - if ( null !== $sign_success && empty( $sign_success ) ) {
355 - $sign_error = '';
356 - while ( $msg = openssl_error_string() ) {
357 - if ( is_string( $msg ) ) {
358 - $sign_error .= $msg;
359 - }
360 - }
361 - 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 );
362 - }
363 -
364 - $data['mainwpsignature'] = ! empty( $signature ) ? base64_encode( $signature ) : ''; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
365 -
366 - /** This filter is documented in ../widgets/widget-mainwp-recent-posts.php */
367 - $recent_number = apply_filters( 'mainwp_recent_posts_pages_number', 5 );
368 - if ( 5 !== $recent_number ) {
369 - $data['recent_number'] = $recent_number;
370 - }
371 -
372 - $scan_dir = apply_filters( 'mainwp_stats_scan_dir', false, $website );
373 - if ( ! empty( $scan_dir ) ) {
374 - $data['scan_dir'] = 1;
375 - }
376 -
377 457 /**
378 458 * Current user global.
379 459 *
380 460 * @global string
@@ -391,14 +471,125 @@
391 471 * @param int $current_user->ID User ID.
392 472 *
393 473 * @since Unknown
394 474 */
395 - $alter_user = apply_filters( 'mainwp_alter_login_user', false, $website->id, $current_user->ID );
396 - if ( ! empty( $alter_user ) ) {
397 - $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 + }
398 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';
399 508 }
400 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 + }
401 592 return http_build_query( $data, '', '&' );
402 593 }
403 594
404 595 return null;
@@ -410,20 +601,29 @@
410 601 * Get authorized $_POST data & build query for renew connection action only.
411 602 *
412 603 * @param mixed $website Array of Child Site Info.
413 604 * @param mixed $what What we are posting.
605 + * @param array $others Other data.
414 606 *
415 607 * @return mixed null|http_build_query()
416 608 */
417 - 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.
418 610
611 + if ( ! is_array( $others ) ) {
612 + $others = array();
613 + }
614 +
615 + $verify_signature = ! empty( $others['verify_signature'] ) ? true : false;
616 +
419 617 if ( $website && '' !== $what ) {
420 618 $compat_what = 'disconnect'; // to compatible, renew will call disconnect.
421 619 $data = array();
422 620 $data['user'] = $website->adminname;
423 621 $data['function'] = $compat_what;
424 - $data['nonce'] = wp_rand( 0, 9999 );
622 + $data['nonce'] = bin2hex( random_bytes( 16 ) );
425 623
624 + $sign_value = $compat_what . $data['nonce']; // compatible format.
625 +
426 626 $alg = false;
427 627 $sign_success = null;
428 628 $use_seclib = false;
429 629
@@ -428,17 +628,17 @@
428 628 $use_seclib = false;
429 629
430 630 if ( MainWP_Connect_Lib::is_use_fallback_sec_lib( $website ) ) {
431 631 // to disconnect.
432 - $sign_success = MainWP_Connect_Lib::connect_sign( $compat_what . $data['nonce'], $signature, base64_decode( $website->privkey ) ); // 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.
433 633 $use_seclib = true;
434 634 } elseif ( function_exists( 'openssl_verify' ) ) {
435 635 $alg = MainWP_System_Utility::get_connect_sign_algorithm( $website );
436 - $sign_success = static::connect_sign( $compat_what . $data['nonce'], $signature, base64_decode( $website->privkey ), $alg ); // 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.
437 637 if ( empty( $sign_success ) ) { // error from openssl, openssl_sign().
438 638 $alg = defined( 'OPENSSL_ALGO_SHA1' ) ? OPENSSL_ALGO_SHA1 : false; // to set default SHA1, to disconnect.
439 639 MainWP_Logger::instance()->debug_for_website( $website, 'get_renew_post_data_authed', '[' . $website->url . '] :: [openssl_sign:failed] :: Set sign_algo=SHA1' );
440 - $sign_success = static::connect_sign( $compat_what . $data['nonce'], $signature, base64_decode( $website->privkey ), $alg ); // 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.
441 641 }
442 642
443 643 if ( false !== $alg ) {
444 644 $data['sign_algo'] = $alg;
@@ -444,11 +644,9 @@
444 644 $data['sign_algo'] = $alg;
445 645 }
446 646 }
447 647
448 - if ( $use_seclib ) {
449 - $data['verifylib'] = 1;
450 - }
648 + $data['mainwpsignature'] = ! empty( $signature ) ? base64_encode( $signature ) : ''; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
451 649
452 650 if ( null !== $sign_success && empty( $sign_success ) ) {
453 651 $sign_error = '';
454 652 while ( $msg = openssl_error_string() ) {
@@ -458,10 +656,55 @@
458 656 }
459 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 );
460 658 }
461 659
462 - $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 );
463 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 +
464 707 return http_build_query( $data, '', '&' );
465 708 }
466 709 return null;
467 710 }
@@ -476,12 +719,16 @@
476 719 * @param mixed $paramValue OpenSSL parameter.
477 720 * @param string $paramName Parameter name.
478 721 * @param bool $asArray true|false Default is false.
479 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.
480 727 *
481 728 * @return string $url
482 729 */
483 - 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.
484 731 $params = array();
485 732 if ( $website && '' !== $paramValue ) {
486 733
487 734 $sign_success = null;
@@ -486,27 +733,51 @@
486 733
487 734 $sign_success = null;
488 735 $alg = false;
489 736 $use_seclib = false;
490 - $nonce = wp_rand( 0, 9999 );
491 - if ( MainWP_Connect_Lib::is_use_fallback_sec_lib( $website ) ) {
492 - $sign_success = MainWP_Connect_Lib::connect_sign( $paramValue . $nonce, $signature, base64_decode( $website->privkey ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
493 - $use_seclib = true;
494 - } elseif ( function_exists( 'openssl_verify' ) ) {
495 - $alg = MainWP_System_Utility::get_connect_sign_algorithm( $website );
496 - $sign_success = static::connect_sign( $paramValue . $nonce, $signature, base64_decode( $website->privkey ), $alg ); // 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 );
497 750 }
498 751
499 - $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'] );
500 753
501 - if ( null !== $sign_success && empty( $sign_success ) ) {
502 - $sign_error = '';
503 - while ( $msg = openssl_error_string() ) {
504 - if ( is_string( $msg ) ) {
505 - $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 + }
506 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 );
507 777 }
508 - 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.
509 780 }
510 781
511 782 $params = array(
512 783 'login_required' => 1,
@@ -515,8 +786,62 @@
515 786 'nonce' => $nonce,
516 787 $paramName => rawurlencode( $paramValue ),
517 788 );
518 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 +
519 844 if ( is_array( $other_params ) ) {
520 845 foreach ( $other_params as $name => $value ) {
521 846 if ( is_string( $name ) && ! empty( $name ) && is_scalar( $value ) ) {
522 847 $params[ sanitize_text_field( wp_unslash( $name ) ) ] = rawurlencode( sanitize_text_field( wp_unslash( $value ) ) );
@@ -536,30 +861,11 @@
536 861 }
537 862 }
538 863 }
539 864
540 - if ( false !== $alg ) {
541 - $params['sign_algo'] = $alg;
542 - }
543 -
544 865 if ( ! empty( $use_seclib ) ) {
545 866 $params['verifylib'] = 1;
546 867 }
547 -
548 - /**
549 - * Current user global.
550 - *
551 - * @global string
552 - */
553 - global $current_user;
554 -
555 - if ( ( ( ! defined( 'DOING_CRON' ) || false === DOING_CRON ) && ( ! defined( 'WP_CLI' ) || false === WP_CLI ) ) && $current_user && $current_user->ID ) {
556 - /** This filter is documented in ../class/class-mainwp-connect.php */
557 - $alter_user = apply_filters( 'mainwp_alter_login_user', false, $website->id, $current_user->ID );
558 - if ( ! empty( $alter_user ) ) {
559 - $params['alt_user'] = rawurlencode( $alter_user );
560 - }
561 - }
562 868 }
563 869
564 870 if ( $asArray ) {
565 871 return $params;
@@ -564,9 +870,13 @@
564 870 if ( $asArray ) {
565 871 return $params;
566 872 }
567 873
568 - $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 + }
569 879 $url .= ( substr( $url, - 1 ) !== '/' ? '/' : '' );
570 880 $url .= '?';
571 881
572 882 foreach ( $params as $key => $value ) {
@@ -583,16 +893,26 @@
583 893 * @param string $data Data sign.
584 894 * @param string $signature signature.
585 895 * @param string $privkey Private key.
586 896 * @param mixed $algorithm signature algorithm.
897 + * @param int $site_id site id.
587 898 *
588 899 * @return bool Success or not.
589 900 */
590 - public static function connect_sign( $data, &$signature, $privkey, $algorithm ) {
901 + public static function connect_sign( $data, &$signature, $privkey, $algorithm, $site_id ) {
902 + $de_privkey = MainWP_Encrypt_Data_Lib::instance()->decrypt_privkey( $privkey, $site_id );
903 +
904 + if ( empty( $de_privkey ) ) {
905 + MainWP_Logger::instance()->debug( 'Error: Failed to decrypt the priv key.' );
906 + }
907 +
908 + if ( empty( $de_privkey ) ) {
909 + $de_privkey = $privkey; // compatible.
910 + }
591 911 if ( false === $algorithm ) {
592 - return openssl_sign( $data, $signature, $privkey ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
912 + return openssl_sign( $data, $signature, $de_privkey ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
593 913 } else {
594 - return openssl_sign( $data, $signature, $privkey, $algorithm ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
914 + return openssl_sign( $data, $signature, $de_privkey, $algorithm ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
595 915 }
596 916 }
597 917
598 918 /**
@@ -608,11 +928,13 @@
608 928 * @return mixed null|http_build_query()
609 929 */
610 930 public static function get_post_data_not_authed( $url, $admin, $what, $params = null ) {
611 931 if ( '' !== $url && '' !== $admin && '' !== $what ) {
612 - $data = array();
613 - $data['user'] = $admin;
614 - $data['function'] = $what;
932 + $data = array();
933 + $data['user'] = $admin;
934 + $data['function'] = $what;
935 + $data['mainwpver'] = MainWP_System::$version;
936 +
615 937 if ( null !== $params ) {
616 938 $data = array_merge( $data, $params );
617 939 }
618 940
@@ -622,11 +944,30 @@
622 944 return null;
623 945 }
624 946
625 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 + /**
626 967 * Method fetch_urls_authed()
627 968 *
628 - * Fetch authorized URLs.
969 + * Fetches data from child sites if authenticated.
629 970 *
630 971 * @param object $websites Websites information.
631 972 * @param string $what Action to perform.
632 973 * @param array $params Request parameters.
@@ -649,9 +990,12 @@
649 990 if ( ! is_array( $params ) ) {
650 991 $params = array();
651 992 }
652 993
653 - $chunkSize = apply_filters( 'mainwp_fetch_urls_chunk_size', 10 );
994 + $sleep_int = (int) get_option( 'mainwp_chunksleepinterval', 5 );
995 + $chunkSize = (int) get_option( 'mainwp_chunksitesnumber', 10 );
996 +
997 + $chunkSize = apply_filters( 'mainwp_fetch_urls_chunk_size', $chunkSize );
654 998 if ( count( $websites ) > $chunkSize ) {
655 999 $total = count( $websites );
656 1000 $loops = ceil( $total / $chunkSize );
657 1001 for ( $i = 0; $i < $loops; $i++ ) {
@@ -656,9 +1000,9 @@
656 1000 $loops = ceil( $total / $chunkSize );
657 1001 for ( $i = 0; $i < $loops; $i++ ) {
658 1002 $newSites = array_slice( $websites, $i * $chunkSize, $chunkSize, true );
659 1003 static::fetch_urls_authed( $newSites, $what, $params, $handler, $output, $whatPage, $others );
660 - sleep( 5 );
1004 + sleep( $sleep_int );
661 1005 }
662 1006
663 1007 return false;
664 1008 }
@@ -665,9 +1009,26 @@
665 1009
666 1010 $agent = 'Mozilla/5.0 (compatible; MainWP/' . MainWP_System::$version . '; +http://mainwp.com)';
667 1011 $mh = curl_multi_init();
668 1012
669 - $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 + }
670 1031
671 1032 $disabled_functions = ini_get( 'disable_functions' );
672 1033 $handleToWebsite = array();
673 1034 $requestUrls = array();
@@ -701,13 +1062,16 @@
701 1062 } else {
702 1063 $url .= 'admin-ajax.php';
703 1064 }
704 1065
1066 + $http_user = null;
1067 + $http_pass = null;
1068 +
705 1069 if ( property_exists( $website, 'http_user' ) ) {
706 - $http_user = $website->http_user;
1070 + $http_user = MainWP_Credential_Storage::decrypt_credential( $website->http_user );
707 1071 }
708 1072 if ( property_exists( $website, 'http_pass' ) ) {
709 - $http_pass = $website->http_pass;
1073 + $http_pass = MainWP_Credential_Storage::decrypt_credential( $website->http_pass );
710 1074 }
711 1075
712 1076 if ( isset( $params ) && isset( $params['new_post'] ) ) {
713 1077
@@ -770,9 +1134,10 @@
770 1134 // to fix.
771 1135 if ( defined( 'LOGGED_IN_SALT' ) && defined( 'NONCE_SALT' ) ) {
772 1136 $cookie_salt = sha1( sha1( 'mainwp' . LOGGED_IN_SALT . $website->id ) . NONCE_SALT . 'WP_Cookie' ); // NOSONAR - safe for salt file name.
773 1137 } else {
774 - $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' );
775 1140 }
776 1141 $cookieFile = $cookieDir . '/' . $cookie_salt;
777 1142 if ( ! file_exists( $cookieFile ) ) {
778 1143 @file_put_contents( $cookieFile, '' );
@@ -793,8 +1158,9 @@
793 1158 $postdata = static::get_post_data_authed( $website, $what, $params );
794 1159 curl_setopt( $ch, CURLOPT_POSTFIELDS, $postdata );
795 1160 curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, 10 );
796 1161 curl_setopt( $ch, CURLOPT_USERAGENT, $agent );
1162 + curl_setopt( $ch, CURLOPT_REFERER, get_option( 'siteurl' ) );
797 1163 curl_setopt( $ch, CURLOPT_ENCODING, 'none' );
798 1164 if ( ! empty( $http_user ) && ! empty( $http_pass ) ) {
799 1165 $http_pass = stripslashes( $http_pass );
800 1166 curl_setopt( $ch, CURLOPT_USERPWD, "$http_user:$http_pass" );
@@ -853,10 +1219,12 @@
853 1219 }
854 1220 }
855 1221
856 1222 if ( empty( $disabled_functions ) || ( false === stristr( $disabled_functions, 'curl_multi_exec' ) ) ) {
857 - $lastRun = 0;
1223 + $lastRun = 0;
1224 + $retry_added = false;
858 1225 do {
1226 + $retry_added = false;
859 1227 if ( 20 < time() - $lastRun ) {
860 1228 MainWP_System_Utility::set_time_limit( $timeout );
861 1229 $lastRun = time();
862 1230 }
@@ -861,21 +1229,38 @@
861 1229 $lastRun = time();
862 1230 }
863 1231
864 1232 curl_multi_exec( $mh, $running );
1233 + curl_multi_select( $mh );
865 1234 while ( $info = curl_multi_info_read( $mh ) ) {
866 1235 $data = curl_multi_getcontent( $info['handle'] );
867 1236 $contains = ( 0 < preg_match( '/<mainwp>(.*)<\/mainwp>/', $data, $results ) );
868 1237 curl_multi_remove_handle( $mh, $info['handle'] );
869 1238
870 - if ( ! $contains && isset( $requestUrls[ static::get_resource_id( $info['handle'] ) ] ) ) {
871 - curl_setopt( $info['handle'], CURLOPT_URL, $requestUrls[ static::get_resource_id( $info['handle'] ) ] );
872 - curl_multi_add_handle( $mh, $info['handle'] );
873 - unset( $requestUrls[ static::get_resource_id( $info['handle'] ) ] );
874 - ++$running;
875 - continue;
1239 + $rid = static::get_resource_id( $info['handle'] );
1240 + if ( ! $contains && isset( $requestUrls[ $rid ] ) ) {
1241 + curl_setopt( $info['handle'], CURLOPT_URL, $requestUrls[ $rid ] );
1242 + curl_setopt( $info['handle'], CURLOPT_FRESH_CONNECT, true );
1243 + curl_setopt( $info['handle'], CURLOPT_FORBID_REUSE, true );
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 + }
876 1256 }
877 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 +
878 1263 if ( null !== $handler ) {
879 1264 $site = &$handleToWebsite[ static::get_resource_id( $info['handle'] ) ];
880 1265 call_user_func_array( $handler, array( $data, $site, &$output, $params ) );
881 1266 }
@@ -880,17 +1265,17 @@
880 1265 call_user_func_array( $handler, array( $data, $site, &$output, $params ) );
881 1266 }
882 1267
883 1268 unset( $handleToWebsite[ static::get_resource_id( $info['handle'] ) ] );
884 - if ( 'resource' === gettype( $info['handle'] ) ) {
1269 + if ( static::is_valid_curl_handle( $info['handle'] ) ) {
885 1270 curl_close( $info['handle'] );
886 1271 }
887 1272 unset( $info['handle'] );
888 1273 }
889 1274 usleep( 10000 );
890 - } while ( $running > 0 );
1275 + } while ( $running > 0 || $retry_added );
891 1276
892 - if ( 'resource' === gettype( $mh ) ) {
1277 + if ( static::is_valid_curl_handle( $mh ) ) {
893 1278 curl_multi_close( $mh );
894 1279 }
895 1280 } else {
896 1281 foreach ( $requestHandles as $ch ) {
@@ -913,9 +1298,9 @@
913 1298 *
914 1299 * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD.
915 1300 * @return string The "Expect" header.
916 1301 */
917 - protected static function get_expect_header( $data ) {
1302 + public static function get_expect_header( $data ) {
918 1303 if ( ! is_array( $data ) ) {
919 1304 return strlen( (string) $data ) >= 1048576 ? '100-Continue' : '';
920 1305 }
921 1306
@@ -1049,8 +1434,9 @@
1049 1434 * @param bool $checkConstraints Whether or not to check constraints.
1050 1435 * @param bool $pForceFetch Whether or not to force the fetch.
1051 1436 * @param bool $pRetryFailed Whether or not to retry the fetch process.
1052 1437 * @param null $rawResponse Raw response.
1438 + * @param bool $verify_signature Wether verify signature data.
1053 1439 *
1054 1440 * @return mixed $information
1055 1441 *
1056 1442 * @uses \MainWP\Dashboard\MainWP_Monitoring_Handler::handle_check_website()
@@ -1063,9 +1449,10 @@
1063 1449 $params = null,
1064 1450 $checkConstraints = false,
1065 1451 $pForceFetch = false,
1066 1452 $pRetryFailed = true,
1067 - $rawResponse = null
1453 + $rawResponse = null,
1454 + $verify_signature = false
1068 1455 ) {
1069 1456 unset( $pForceFetch );
1070 1457
1071 1458 // to support demo data.
@@ -1072,8 +1459,42 @@
1072 1459 if ( MainWP_Demo_Handle::get_instance()->is_demo_website( $website ) ) {
1073 1460 return MainWP_Demo_Handle::get_instance()->handle_action_demo( $website, $what );
1074 1461 }
1075 1462
1463 + /**
1464 + * Filter to mock fetch_url_authed response before any HTTP/signing occurs.
1465 + *
1466 + * This filter fires early, before OpenSSL signing or HTTP requests, allowing
1467 + * tests to bypass child site communication entirely.
1468 + *
1469 + * SECURITY WARNING - TEST ONLY:
1470 + * This filter ONLY fires when ALL of the following conditions are met:
1471 + * 1. MAINWP_TESTING_MODE constant is defined and true
1472 + * 2. A PHPUnit test harness constant is present (WP_TESTS_DOMAIN, PHPUNIT_COMPOSER_INSTALL, or WP_TESTS_DIR)
1473 + *
1474 + * This triple-check prevents malicious code from defining MAINWP_TESTING_MODE
1475 + * in production to spoof child site responses.
1476 + *
1477 + * IMPORTANT: MAINWP_TESTING_MODE must ONLY be defined in the PHPUnit bootstrap
1478 + * file (tests/bootstrap.php). Defining it in production code, wp-config.php, or
1479 + * plugin files would create a security vulnerability allowing response spoofing.
1480 + *
1481 + * @since 5.4
1482 + *
1483 + * @param mixed $pre_result Return non-false to short-circuit and return this value.
1484 + * @param object $website Website object being communicated with.
1485 + * @param string $what Action being performed (e.g., 'plugin_action').
1486 + * @param array $params Request parameters.
1487 + * @return mixed Array to return early, false to proceed normally.
1488 + */
1489 + $is_phpunit_env = defined( 'WP_TESTS_DOMAIN' ) || defined( 'PHPUNIT_COMPOSER_INSTALL' ) || ( defined( 'WP_TESTS_DIR' ) && WP_TESTS_DIR );
1490 + if ( defined( 'MAINWP_TESTING_MODE' ) && MAINWP_TESTING_MODE && $is_phpunit_env ) {
1491 + $pre_result = apply_filters( 'mainwp_fetch_url_authed_pre', false, $website, $what, $params, $verify_signature );
1492 + if ( false !== $pre_result ) {
1493 + return $pre_result;
1494 + }
1495 + }
1496 +
1076 1497 if ( ! is_array( $params ) ) {
1077 1498 $params = array();
1078 1499 }
1079 1500
@@ -1081,10 +1502,8 @@
1081 1502 'force_use_ipv4' => $website->force_use_ipv4,
1082 1503 'upgrade' => ( 'upgradeplugintheme' === $what || 'upgrade' === $what || 'upgradetranslation' === $what ),
1083 1504 );
1084 1505
1085 - $request_update = MainWP_Premium_Update::maybe_request_premium_updates( $website, $what, $params );
1086 -
1087 1506 if ( isset( $rawResponse ) && $rawResponse ) {
1088 1507 $others['raw_response'] = 'yes';
1089 1508 }
1090 1509
@@ -1092,11 +1511,17 @@
1092 1511
1093 1512 $updating_website = false;
1094 1513 $type = '';
1095 1514 $list = '';
1096 - 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 ) {
1097 1519 $updating_website = true;
1098 - if ( 'upgradeplugintheme' === $what || 'upgradetranslation' === $what ) {
1520 + if ( $premium_update ) {
1521 + $type = $params['premium_type'];
1522 + $list = $params['list'];
1523 + } elseif ( 'upgradeplugintheme' === $what || 'upgradetranslation' === $what ) {
1099 1524 $type = $params['type'];
1100 1525 $list = $params['list'];
1101 1526 } else {
1102 1527 $type = 'wp';
@@ -1118,40 +1543,80 @@
1118 1543 */
1119 1544 do_action( 'mainwp_website_before_updated', $website, $type, $list );
1120 1545 }
1121 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 +
1122 1564 if ( 'renew' === $what ) {
1123 - $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 + );
1124 1572 } else {
1125 - $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 + );
1126 1583
1127 1584 }
1585 +
1128 1586 $others['function'] = $what;
1129 1587
1130 - $information = 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 );
1131 1589
1132 - if ( ! $request_update ) {
1133 - $information = static::fetch_url( $website, $website->url, $postdata, $checkConstraints, $website->verify_certificate, $pRetryFailed, $website->http_user, $website->http_pass, $website->ssl_version, $others );
1134 - /**
1135 - * Fires immediately after fetch url action.
1136 - *
1137 - * @param object $website website.
1138 - * @param array $information information result data.
1139 - * @param string $what action.
1140 - * @param array $params params input array.
1141 - * @param array $others others input array.
1142 - *
1143 - * @since 4.5.1.1
1144 - */
1145 - do_action( 'mainwp_fetch_url_authed', $website, $information, $what, $params, $others );
1146 - } else {
1147 - $slug = $params['list'];
1148 - $information['upgrades'] = array( $slug => 1 );
1590 + if ( ! empty( $output ) ) {
1591 + if ( ! is_array( $information ) ) {
1592 + $information = array();
1593 + }
1594 + $information['fetch_url_output'] = $output;
1149 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 );
1150 1608
1151 - if ( is_array( $information ) && isset( $information['sync'] ) && ! empty( $information['sync'] ) ) {
1152 - MainWP_Sync::sync_information_array( $website, $information['sync'] );
1153 - 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 + }
1154 1619 }
1155 1620
1156 1621 if ( $updating_website ) {
1157 1622 /**
@@ -1167,9 +1632,9 @@
1167 1632 * @since Unknown
1168 1633 */
1169 1634 do_action( 'mainwp_website_updated', $website, $type, $list, $information );
1170 1635 if ( 1 === (int) get_option( 'mainwp_check_http_response', 0 ) ) {
1171 - MainWP_Monitoring_Handler::handle_check_website( $website );
1636 + MainWP_Monitoring_Handler::handle_check_website( $website, true );
1172 1637 }
1173 1638 }
1174 1639
1175 1640 return $information;
@@ -1193,9 +1658,9 @@
1193 1658 * @param array $output Output values.
1194 1659 *
1195 1660 * @return mixed static::fetch_url() Fetch URL.
1196 1661 */
1197 - public static function fetch_url_not_authed( // NOSONAR - compatible.
1662 + public static function fetch_url_not_authed( // phpcs:ignore -- NOSONAR - compatible.
1198 1663 $url,
1199 1664 $admin,
1200 1665 $what,
1201 1666 $params = null,
@@ -1206,8 +1671,72 @@
1206 1671 $sslVersion = 0,
1207 1672 $others = array(),
1208 1673 &$output = array()
1209 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 +
1210 1739 unset( $pForceFetch );
1211 1740
1212 1741 if ( empty( $params ) ) {
1213 1742 $params = array();
@@ -1321,8 +1850,41 @@
1321 1850 $others = array(),
1322 1851 &$output = array()
1323 1852 ) {
1324 1853
1854 + /**
1855 + * Enables data to be returned prior to connecting to the site.
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 + *
1866 + * @since 5.5
1867 + *
1868 + * @param mixed false
1869 + * @param mixed $website
1870 + * @param mixed $url
1871 + * @param mixed $postdata
1872 + * @param mixed $checkConstraints
1873 + * @param mixed $verifyCertificate
1874 + * @param mixed $http_user
1875 + * @param mixed $http_pass
1876 + * @param mixed $sslVersion
1877 + * @param mixed $others
1878 + * @param mixed $output
1879 + */
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 + }
1885 + }
1886 +
1325 1887 $agent = 'Mozilla/5.0 (compatible; MainWP/' . MainWP_System::$version . '; +http://mainwp.com)';
1326 1888
1327 1889 if ( ! empty( $website ) ) {
1328 1890 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url_site', 'Request to [' . $url . '] [' . MainWP_Utility::value_to_string( $postdata, 1 ) . ']' );
@@ -1345,8 +1907,10 @@
1345 1907 $cookieDir = $dirs[0] . 'cookies';
1346 1908
1347 1909 static::init_cookiesdir( $cookieDir );
1348 1910
1911 + $fetch_track_id = MainWP_Execution_Helper::execute_call_track( 'start_point', $website, $postdata );
1912 +
1349 1913 $ch = curl_init();
1350 1914
1351 1915 $proxy = new \WP_HTTP_Proxy();
1352 1916 if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) {
@@ -1364,9 +1928,10 @@
1364 1928 // to fix.
1365 1929 if ( defined( 'LOGGED_IN_SALT' ) && defined( 'NONCE_SALT' ) ) {
1366 1930 $cookie_salt = sha1( sha1( 'mainwp' . LOGGED_IN_SALT . $website->id ) . NONCE_SALT . 'WP_Cookie' ); // NOSONAR - safe for salt file name.
1367 1931 } else {
1368 - $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' );
1369 1934 }
1370 1935 $cookieFile = $cookieDir . '/' . $cookie_salt;
1371 1936 if ( ! file_exists( $cookieFile ) ) {
1372 1937 @file_put_contents( $cookieFile, '' );
@@ -1415,23 +1980,29 @@
1415 1980 }
1416 1981
1417 1982 curl_setopt( $ch, CURLOPT_SSLVERSION, $sslVersion );
1418 1983
1984 + $http_version = apply_filters( 'mainwp_curl_http_version', false, $website ? $website->id : false, $url );
1985 + if ( false !== $http_version ) {
1986 + curl_setopt( $ch, CURLOPT_HTTP_VERSION, $http_version );
1987 + }
1988 +
1989 + $curlopt_resolve = false;
1990 +
1419 1991 if ( is_object( $website ) && property_exists( $website, 'id' ) ) {
1420 - $http_version = apply_filters( 'mainwp_curl_http_version', false, $website->id );
1421 - if ( false !== $http_version ) {
1422 - curl_setopt( $ch, CURLOPT_HTTP_VERSION, $http_version );
1423 - }
1424 1992 $curlopt_resolve = apply_filters( 'mainwp_curl_curlopt_resolve', false, $website->id, $website->url );
1425 - if ( is_array( $curlopt_resolve ) && ! empty( $curlopt_resolve ) ) {
1426 - curl_setopt( $ch, CURLOPT_RESOLVE, $curlopt_resolve );
1427 - curl_setopt( $ch, CURLOPT_DNS_USE_GLOBAL_CACHE, false );
1428 - }
1429 1993 }
1430 1994
1995 + if ( is_array( $curlopt_resolve ) && ! empty( $curlopt_resolve ) ) {
1996 + curl_setopt( $ch, CURLOPT_RESOLVE, $curlopt_resolve );
1997 + curl_setopt( $ch, CURLOPT_DNS_USE_GLOBAL_CACHE, false );
1998 + }
1999 +
1431 2000 $headers = array( 'X-Requested-With' => 'XMLHttpRequest' );
1432 2001 $headers['Expect'] = static::get_expect_header( $postdata );
1433 2002
2003 + $headers = apply_filters( 'mainwp_connect_http_request_headers', $headers, $website );
2004 +
1434 2005 if ( class_exists( '\WpOrg\Requests\Requests' ) ) {
1435 2006 $headers = \WpOrg\Requests\Requests::flatten( $headers );
1436 2007 } else {
1437 2008 $headers = \Requests::flatten( $headers );
@@ -1457,9 +2028,25 @@
1457 2028 if ( $force_use_ipv4 && defined( 'CURLOPT_IPRESOLVE' ) && defined( 'CURL_IPRESOLVE_V4' ) ) {
1458 2029 curl_setopt( $ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4 );
1459 2030 }
1460 2031
1461 - $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 +
1462 2049 curl_setopt( $ch, CURLOPT_TIMEOUT, $timeout );
1463 2050 MainWP_System_Utility::set_time_limit( $timeout );
1464 2051
1465 2052 MainWP_Utility::end_session();
@@ -1471,26 +2058,38 @@
1471 2058 $mh = @curl_multi_init();
1472 2059 @curl_multi_add_handle( $mh, $ch );
1473 2060
1474 2061 $lastRun = 0;
2062 + $running = null;
2063 +
1475 2064 do {
1476 2065 if ( 20 < time() - $lastRun ) {
1477 2066 MainWP_System_Utility::set_time_limit( $timeout );
1478 2067 $lastRun = time();
1479 2068 }
1480 - @curl_multi_exec( $mh, $running );
2069 +
2070 + do {
2071 + $mrc = curl_multi_exec( $mh, $running );
2072 + } while ( CURLM_CALL_MULTI_PERFORM === $mrc );
2073 +
2074 + $rc = curl_multi_select( $mh, 1.0 );
2075 + if ( -1 === $rc ) {
2076 + usleep( 100000 );
2077 + }
2078 +
1481 2079 while ( $info = @curl_multi_info_read( $mh ) ) {
1482 - $data = @curl_multi_getcontent( $info['handle'] );
1483 -
2080 + $data = @curl_multi_getcontent( $info['handle'] );
1484 2081 $http_status = @curl_getinfo( $info['handle'], CURLINFO_HTTP_CODE );
1485 2082 $err = @curl_error( $info['handle'] );
1486 2083 $real_url = @curl_getinfo( $info['handle'], CURLINFO_EFFECTIVE_URL );
1487 2084
1488 2085 @curl_multi_remove_handle( $mh, $info['handle'] );
2086 + curl_close( $info['handle'] );
1489 2087 }
1490 2088 usleep( 10000 );
1491 2089 } while ( $running > 0 );
1492 - if ( 'resource' === gettype( $mh ) ) {
2090 +
2091 + if ( static::is_valid_curl_handle( $mh ) ) {
1493 2092 @curl_multi_close( $mh );
1494 2093 }
1495 2094 } else {
1496 2095 $data = @curl_exec( $ch );
@@ -1496,13 +2095,16 @@
1496 2095 $data = @curl_exec( $ch );
1497 2096 $http_status = @curl_getinfo( $ch, CURLINFO_HTTP_CODE );
1498 2097 $err = @curl_error( $ch );
1499 2098 $real_url = @curl_getinfo( $ch, CURLINFO_EFFECTIVE_URL );
2099 + curl_close( $ch );
1500 2100 }
1501 2101
1502 2102 $host = wp_parse_url( $real_url, PHP_URL_HOST );
1503 2103 $ip = gethostbyname( $host );
1504 2104
2105 + MainWP_Execution_Helper::execute_call_track( 'end_point', $website, $postdata, $fetch_track_id, 'fetch site' );
2106 +
1505 2107 if ( null !== $website ) {
1506 2108 MainWP_DB_Common::instance()->insert_or_update_request_log( $website->id, $ip, null, microtime( true ) );
1507 2109 }
1508 2110
@@ -1507,9 +2109,20 @@
1507 2109 }
1508 2110
1509 2111 $raw_response = isset( $others['raw_response'] ) && 'yes' === $others['raw_response'] ? true : false;
1510 2112
1511 - $output['fetch_data'] = $data;
2113 + $hidden_data = '[hidden response data]';
2114 +
2115 + if ( ! apply_filters( 'mainwp_hide_raw_connection_response_data', true ) ) {
2116 + $hidden_data = $data;
2117 + }
2118 +
2119 + if ( ! is_array( $output ) ) {
2120 + $output = array();
2121 + }
2122 +
2123 + $output['fetch_data'] = $hidden_data;
2124 +
1512 2125 $output['http_status'] = (int) $http_status;
1513 2126
1514 2127 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url_site', 'http status: [' . $http_status . '] err: [' . $err . ']' );
1515 2128 if ( '400' === $http_status ) {
@@ -1519,39 +2132,85 @@
1519 2132 MainWP_Logger::instance()->log_execution_time( 'fetch_url_site :: [url=' . $url . ']' );
1520 2133
1521 2134 $thr_error = null;
1522 2135
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 );
2139 + }
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 +
1523 2145 if ( ( false === $data ) && empty( $http_status ) ) {
1524 2146 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url', '[' . $url . '] HTTP Error: [status=0][' . $err . ']' );
1525 - $thr_error = new MainWP_Exception( 'HTTPERROR', $err ); //phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
2147 + $thr_error = new MainWP_Exception( 'HTTPERROR', $err ); //phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
2148 + $output['error_category'] = 'http_error';
1526 2149 } elseif ( empty( $data ) && ! empty( $err ) ) {
1527 2150 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url', '[' . $url . '] HTTP Error: [status=' . $http_status . '][' . $err . ']' );
1528 - $thr_error = new MainWP_Exception( 'HTTPERROR', $err ); //phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
2151 + $thr_error = new MainWP_Exception( 'HTTPERROR', $err ); //phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
2152 + $output['error_category'] = 'http_error';
2153 + $output['error_code'] = 'http_request_failed';
2154 + $output['error_message'] = $err;
1529 2155 } elseif ( 0 < preg_match( '/<mainwp>(.*)<\/mainwp>/', $data, $results ) ) {
1530 - $result = $results[1];
1531 - $information = MainWP_System_Utility::get_child_response( base64_decode( $result ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
2156 + $output['connection_step'] = 'verify_credentials';
2157 + $result = $results[1];
2158 + $information = MainWP_System_Utility::get_child_response( base64_decode( $result ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
1532 2159 unset( $output['fetch_data'] ); // hide the data.
1533 2160 $pdt = is_string( $postdata ) ? $postdata : '';
1534 2161 $data_log = is_array( $postdata ) ? print_r( $postdata, true ) : $pdt; //phpcs:ignore -- good.
1535 2162 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url_site', '[' . $url . '] postdata [' . $data_log . '] information: [OK]' ); //phpcs:ignore -- ok.
2163 +
2164 + $error_code = is_array( $information ) && isset( $information['error_code'] ) ? sanitize_text_field( wp_unslash( $information['error_code'] ) ) : '';
2165 + if ( ! empty( $error_code ) ) {
2166 + $output['child_error_code'] = $error_code;
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 +
1536 2187 return $information;
1537 2188 } elseif ( 200 === (int) $http_status && ! empty( $err ) ) {
1538 - $thr_error = new MainWP_Exception( 'HTTPERROR', $err ); //phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
2189 + $thr_error = new MainWP_Exception( 'HTTPERROR', $err ); //phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
2190 + $output['error_category'] = 'http_error';
2191 + $output['error_code'] = 'http_request_failed';
2192 + $output['error_message'] = $err;
2193 + MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url_site', '[' . $url . '] error [' . $err . ']' ); //phpcs:ignore -- ok.
1539 2194 } elseif ( $raw_response ) {
1540 2195 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url_site', 'Response: [RAW]' );
1541 2196 return $data;
1542 2197 } else {
1543 - MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url', '[' . $url . '] Error: NOMAINWP [data=' . ( is_string( $data ) ? $data : 'OBJECT' ) . ']' );
2198 + MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url', '[' . $url . '] Error: NOMAINWP [data=' . $hidden_data . ']' );
1544 2199 $detect_wsidchk = is_string( $data ) ? strpos( $data, 'wsidchk' ) : false;
1545 2200 if ( false !== $detect_wsidchk ) {
1546 - $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 );
2201 + $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.';
2202 + $thr_error = new MainWP_Exception( 'ERROR:' . $err_msg, $url );
1547 2203 } else {
1548 2204 $thr_error = new MainWP_Exception( 'NOMAINWP', $url ); //phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
2205 + $err_msg = 'Connection Failed. Please ensure that the MainWP Child plugin is installed and activated on the child site.';
1549 2206 }
2207 + $output['error_category'] = 'child_plugin_missing';
2208 + $output['error_message'] = $err_msg;
1550 2209 }
1551 2210
1552 2211 if ( null !== $thr_error ) {
1553 - $thr_error->set_data( $data );
2212 + $thr_error->set_data( $hidden_data ); // to compatible.
1554 2213 throw $thr_error;
1555 2214 }
1556 2215 }
1557 2216
@@ -1639,9 +2298,9 @@
1639 2298 $lastRequest = MainWP_DB_Common::instance()->get_last_request_timestamp( $ip );
1640 2299 if ( $lastRequest > ( ( microtime( true ) ) - $minimumDelay ) ) {
1641 2300 static::release( $identifier );
1642 2301 $sleep = ( $minimumDelay - ( ( microtime( true ) ) - $lastRequest ) ) * 1000 * 1000;
1643 - $sleep = intval( $sleep );
2302 + $sleep = max( 0, intval( $sleep ) );
1644 2303 usleep( $sleep );
1645 2304 return true;
1646 2305 }
1647 2306 return false;
@@ -1700,9 +2359,9 @@
1700 2359 $wp_filesystem->delete( $file );
1701 2360 }
1702 2361
1703 2362 if ( ! $wp_filesystem->exists( dirname( $file ) ) ) {
1704 - $wp_filesystem->mkdir( dirname( $file ), 0777 );
2363 + $wp_filesystem->mkdir( dirname( $file ), 0750 ); // MWP-1558: tightened from 0777; downloaded files may contain backup data.
1705 2364 }
1706 2365
1707 2366 if ( ! $wp_filesystem->exists( dirname( $file ) ) ) {
1708 2367 throw new MainWP_Exception( esc_html__( 'MainWP plugin could not create directory in order to download the file.', 'mainwp' ) );
@@ -1747,9 +2406,9 @@
1747 2406 $http_pass = stripslashes( $http_pass );
1748 2407 curl_setopt( $ch, CURLOPT_USERPWD, "$http_user:$http_pass" );
1749 2408 }
1750 2409 curl_exec( $ch );
1751 - if ( 'resource' === gettype( $ch ) ) {
2410 + if ( static::is_valid_curl_handle( $ch ) ) {
1752 2411 curl_close( $ch );
1753 2412 }
1754 2413 fclose( $fp );
1755 2414 }
@@ -1779,9 +2438,9 @@
1779 2438
1780 2439 if ( $hasWPFileSystem && ! empty( $wp_filesystem ) ) {
1781 2440
1782 2441 if ( ! $wp_filesystem->is_dir( $cookieDir ) ) {
1783 - $wp_filesystem->mkdir( $cookieDir, 0777 );
2442 + $wp_filesystem->mkdir( $cookieDir, 0750 ); // MWP-1558: tightened from 0777; cookies/ holds child wp-admin session cookies.
1784 2443 }
1785 2444
1786 2445 if ( ! file_exists( $cookieDir . '/.htaccess' ) ) {
1787 2446 $file_htaccess = $cookieDir . '/.htaccess';
@@ -1794,9 +2453,9 @@
1794 2453 }
1795 2454 } else {
1796 2455
1797 2456 if ( ! file_exists( $cookieDir ) ) {
1798 - @mkdir( $cookieDir, 0777, true );
2457 + @mkdir( $cookieDir, 0750, true ); // MWP-1558: tightened from 0777; cookies/ holds child wp-admin session cookies.
1799 2458 }
1800 2459
1801 2460 if ( ! file_exists( $cookieDir . '/.htaccess' ) ) {
1802 2461 $file_htaccess = @fopen( $cookieDir . '/.htaccess', 'w+' );
@@ -1845,9 +2504,9 @@
1845 2504 curl_setopt( $ch, CURLOPT_ENCODING, 'none' );
1846 2505
1847 2506 $data = @curl_exec( $ch );
1848 2507 $httpCode = @curl_getinfo( $ch, CURLINFO_HTTP_CODE );
1849 - if ( 'resource' === gettype( $ch ) ) {
2508 + if ( static::is_valid_curl_handle( $ch ) ) {
1850 2509 curl_close( $ch );
1851 2510 }
1852 2511 if ( 200 === (int) $httpCode ) {
1853 2512 return $data;
@@ -1853,8 +2512,22 @@
1853 2512 return $data;
1854 2513 } else {
1855 2514 return false;
1856 2515 }
2516 + }
2517 +
2518 + /**
2519 + * Method is_valid_curl_handle
2520 + *
2521 + * @param mixed $ch cURL handle to validate.
2522 + * @return bool Valid curl handle.
2523 + */
2524 + public static function is_valid_curl_handle( $ch ) {
2525 + return is_resource( $ch )
2526 + || ( is_object( $ch )
2527 + && class_exists( 'CurlHandle', false )
2528 + && $ch instanceof \CurlHandle
2529 + );
1857 2530 }
1858 2531
1859 2532 /**
1860 2533 * Method get_favico_url()