PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / 6.2.2
MainWP Dashboard: Self-hosted WordPress Management for Agencies v6.2.2
6.2.2 6.2.1 6.2 6.1.8 6.1.7 6.1.6 6.1.5 6.1.4 6.1.3 6.1.2 6.1.1 6.1 6.0.12 6.0.11 4.6.0.1 5.0 5.0.1 5.0.2 5.0.3 5.0.3.1 5.0.3.2 5.1 5.1.1 5.2 5.2.1 All 155 releases
mainwp / class / class-mainwp-connect.php

class-mainwp-connect.php in MainWP Dashboard: Self-hosted WordPress Management for Agencies 6.2.2, at class/class-mainwp-connect.php

2,572 lines 109.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * MainWP Connect
4 *
5 * MainWP Connect functions.
6 *
7 * @package MainWP/Dashboard
8 */
9
10 namespace MainWP\Dashboard;
11
12 // Exit if accessed directly.
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit;
15 }
16
17 /**
18 * Class MainWP_Connect
19 *
20 * @package MainWP\Dashboard
21 */
22 class MainWP_Connect { // phpcs:ignore Generic.Classes.OpeningBraceSameLine.ContentAfterBrace -- NOSONAR.
23
24 // phpcs:disable WordPress.DB.RestrictedFunctions, Generic.Metrics.CyclomaticComplexity, WordPress.WP.AlternativeFunctions, WordPress.PHP.NoSilencedErrors -- Using cURL functions.
25
26 /**
27 * Method get_class_name()
28 *
29 * Get Class Name.
30 *
31 * @return object Class name.
32 */
33 public static function get_class_name() {
34 return __CLASS__;
35 }
36
37 /**
38 * Method try visit.
39 *
40 * Try connecting to Child Site via cURL.
41 *
42 * @param string $url Child Site URL.
43 * @param bool $ssl_verifyhost Option to check SSL Certificate. Default = null.
44 * @param string $http_user HTTPAuth Username. Default = null.
45 * @param string $http_pass HTTPAuth Password. Default = null.
46 * @param int $sslVersion Child Site SSL Version.
47 * @param bool $forceUseIPv4 Option to force IP4. Default = null.
48 * @param bool $no_body Option to set CURLOPT_NOBODY option. Default = false.
49 *
50 * @return array $out. 'host IP, Returned HTTP Code, Error Message, http Status error message.
51 *
52 * @uses \MainWP\Dashboard\MainWP_Logger::debug()
53 * @uses \MainWP\Dashboard\MainWP_System::$version
54 * @uses \MainWP\Dashboard\MainWP_Utility::value_to_string()
55 * @uses \MainWP\Dashboard\MainWP_Utility::get_http_codes()
56 */
57 public static function try_visit( $url, $ssl_verifyhost = null, $http_user = null, $http_pass = null, $sslVersion = 0, $forceUseIPv4 = null, $no_body = false ) { // phpcs:ignore -- NOSONAR -Current complexity is the only way to achieve desired results, pull request solutions appreciated.
58
59 $agent = 'Mozilla/5.0 (compatible; MainWP/' . MainWP_System::$version . '; +http://mainwp.com)';
60 $postdata = array( 'test' => 'yes' );
61
62 $ch = curl_init();
63
64 $proxy = new \WP_HTTP_Proxy();
65 if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) {
66 curl_setopt( $ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP );
67 curl_setopt( $ch, CURLOPT_PROXY, $proxy->host() );
68 curl_setopt( $ch, CURLOPT_PROXYPORT, $proxy->port() );
69
70 if ( $proxy->use_authentication() ) {
71 curl_setopt( $ch, CURLOPT_PROXYAUTH, CURLAUTH_ANY );
72 curl_setopt( $ch, CURLOPT_PROXYUSERPWD, $proxy->authentication() );
73 }
74 }
75
76 curl_setopt( $ch, CURLOPT_URL, $url );
77 if ( $no_body ) {
78 curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, 'HEAD' ); // HTTP request is 'HEAD', but sometime return 4xx - error code.
79 }
80
81 $follow_loc = apply_filters( 'mainwp_try_visit_follow_location', false ); // to support for case compatible.
82
83 curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
84 curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, $follow_loc ? true : false );
85 curl_setopt( $ch, CURLOPT_POST, true );
86 curl_setopt( $ch, CURLOPT_POSTFIELDS, $postdata );
87 curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, 10 );
88 curl_setopt( $ch, CURLOPT_USERAGENT, $agent );
89 curl_setopt( $ch, CURLOPT_ENCODING, 'none' );
90
91 if ( ! empty( $http_user ) && ! empty( $http_pass ) ) {
92 $http_pass = stripslashes( $http_pass );
93 curl_setopt( $ch, CURLOPT_USERPWD, "$http_user:$http_pass" );
94 }
95
96 if ( $ssl_verifyhost ) {
97 curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, 2 );
98 curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true );
99 } else {
100 curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, false ); // NOSONAR.
101 curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false ); // NOSONAR.
102 }
103
104 curl_setopt( $ch, CURLOPT_SSLVERSION, $sslVersion );
105
106 $http_version = apply_filters( 'mainwp_curl_http_version', false, false, $url );
107 if ( false !== $http_version ) {
108 curl_setopt( $ch, CURLOPT_HTTP_VERSION, $http_version );
109 }
110
111 $curlopt_resolve = apply_filters( 'mainwp_curl_curlopt_resolve', false, false, $url );
112 if ( is_array( $curlopt_resolve ) && ! empty( $curlopt_resolve ) ) {
113 curl_setopt( $ch, CURLOPT_RESOLVE, $curlopt_resolve );
114 curl_setopt( $ch, CURLOPT_DNS_USE_GLOBAL_CACHE, false );
115 }
116
117 $headers = array( 'X-Requested-With' => 'XMLHttpRequest' );
118 $headers['Expect'] = static::get_expect_header( $postdata );
119 $headers = apply_filters( 'mainwp_connect_http_request_headers', $headers, false );
120
121 if ( class_exists( '\WpOrg\Requests\Requests' ) ) {
122 $headers = \WpOrg\Requests\Requests::flatten( $headers );
123 } else {
124 $headers = \Requests::flatten( $headers );
125 }
126
127 curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
128 curl_setopt( $ch, CURLOPT_REFERER, get_option( 'siteurl' ) );
129
130 $force_use_ipv4 = false;
131 if ( null !== $forceUseIPv4 ) {
132 if ( 1 === $forceUseIPv4 ) {
133 $force_use_ipv4 = true;
134 } elseif ( 2 === $forceUseIPv4 ) {
135 if ( 1 === (int) get_option( 'mainwp_forceUseIPv4' ) ) {
136 $force_use_ipv4 = true;
137 }
138 }
139 } elseif ( 1 === (int) get_option( 'mainwp_forceUseIPv4' ) ) {
140 $force_use_ipv4 = true;
141 }
142
143 if ( $force_use_ipv4 && defined( 'CURLOPT_IPRESOLVE' ) && defined( 'CURL_IPRESOLVE_V4' ) ) {
144 curl_setopt( $ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4 );
145 }
146
147 MainWP_Logger::instance()->debug( ' :: trying Visit :: [url=' . $url . ']' );
148
149 $http_version = false;
150
151 $disabled_functions = ini_get( 'disable_functions' );
152 if ( empty( $disabled_functions ) || ( stristr( $disabled_functions, 'curl_multi_exec' ) === false ) ) {
153 MainWP_Logger::instance()->debug( ' :: trying Visit :: curl_multi_exec => enabled.' );
154 $mh = curl_multi_init();
155 @curl_multi_add_handle( $mh, $ch );
156
157 do {
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
169 while ( $info = curl_multi_info_read( $mh ) ) {
170 $data = curl_multi_getcontent( $info['handle'] );
171 $err = curl_error( $info['handle'] );
172 $http_status = curl_getinfo( $info['handle'], CURLINFO_HTTP_CODE );
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
178 curl_multi_remove_handle( $mh, $info['handle'] );
179 curl_close( $info['handle'] );
180 }
181 usleep( 10000 );
182
183 } while ( $running > 0 );
184
185 if ( static::is_valid_curl_handle( $mh ) ) {
186 curl_multi_close( $mh );
187 }
188 } else {
189 $data = curl_exec( $ch );
190 $err = curl_error( $ch );
191 $http_status = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
192 $realurl = curl_getinfo( $ch, CURLINFO_EFFECTIVE_URL );
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 ) ) {
199 curl_close( $ch );
200 }
201 }
202
203 MainWP_Logger::instance()->log_execution_time( 'tryVisit :: [url=' . $url . '] [http_status=' . $http_status . ']' );
204
205 $host = wp_parse_url( ( empty( $realurl ) ? $url : $realurl ), PHP_URL_HOST );
206 $ip = false;
207 $target = false;
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.
214 $found = false;
215 $dnsRecord = @dns_get_record( $host, DNS_A | DNS_AAAA | DNS_CNAME );
216 MainWP_Logger::instance()->debug( ' :: tryVisit :: [dnsRecord=' . MainWP_Utility::value_to_string( $dnsRecord, 1 ) . ']' );
217
218 if ( false !== $dnsRecord && is_array( $dnsRecord ) ) {
219 if ( ! isset( $dnsRecord['ip'] ) ) {
220 foreach ( $dnsRecord as $dnsRec ) {
221 if ( isset( $dnsRec['ip'] ) ) {
222 $ip = $dnsRec['ip'];
223 break;
224 }
225 }
226 } else {
227 $ip = $dnsRecord['ip'];
228 }
229
230 if ( ! isset( $dnsRecord['host'] ) ) {
231 foreach ( $dnsRecord as $dnsRec ) {
232 if ( $dnsRec['host'] === $host ) {
233 if ( 'CNAME' === $dnsRec['type'] ) {
234 $target = $dnsRec['target'];
235 }
236 $found = true;
237 break;
238 }
239 }
240 } else {
241 $found = ( $dnsRecord['host'] === $host );
242 if ( 'CNAME' === $dnsRecord['type'] ) {
243 $target = $dnsRecord['target'];
244 }
245 }
246 }
247
248 if ( false === $ip ) {
249 $ip = gethostbynamel( $host );
250 }
251 if ( ( false !== $target ) && ( $target !== $host ) ) {
252 $host .= ' (CNAME: ' . $target . ')';
253 }
254
255 $out = array(
256 'host' => $host,
257 'httpCode' => $http_status,
258 'httpCodeString' => MainWP_Utility::get_http_codes( $http_status ),
259 'httpVersion' => $http_version,
260 );
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
270 if ( false !== $ip ) {
271 $out['ip'] = $ip;
272 $found = true;
273 }
274
275 $out['error'] = ( '' === $err && false === $found ? 'Invalid host.' : $err );
276
277 return $out;
278 }
279
280
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 /**
296 * Method check_ignored_http_code()
297 *
298 * Check if http error code is being ignored.
299 *
300 * @param mixed $value http error code.
301 * @param object|false $website website.
302 *
303 * @return bolean True|False.
304 */
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'] : '';
329 }
330
331 if ( ! empty( $ignored_code ) ) {
332 $ignored_code = explode( ',', $ignored_code );
333 foreach ( $ignored_code as $code ) {
334 $code = trim( $code );
335 if ( (int) $value === (int) $code ) {
336 return true;
337 }
338 }
339 }
340 return false;
341 }
342
343 /**
344 * Method check website status.
345 *
346 * Check if the Website returns and http errors.
347 *
348 * @param object $website Child Site information.
349 * @param bool $chk_http_site Check site http response.
350 *
351 * @return mixed False|try visit result.
352 *
353 * @uses \MainWP\Dashboard\MainWP_Utility::is_domain_valid()
354 */
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
371 $http_user = null;
372 $http_pass = null;
373 $sslVersion = null;
374 $verifyCertificate = null;
375 $forceUseIPv4 = null;
376 if ( is_object( $website ) && isset( $website->url ) ) {
377 $url = $website->url;
378 $verifyCertificate = isset( $website->verify_certificate ) ? (int) $website->verify_certificate : null;
379 $forceUseIPv4 = $website->force_use_ipv4;
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;
386 } else {
387 $url = $website;
388 }
389
390 if ( ! MainWP_Utility::is_domain_valid( $url ) ) {
391 return false;
392 }
393
394 $ssl_verifyhost = false;
395
396 if ( 1 === $verifyCertificate ) {
397 $ssl_verifyhost = true;
398 } elseif ( 2 === $verifyCertificate || null === $verifyCertificate ) {
399 if ( ( false === get_option( 'mainwp_sslVerifyCertificate' ) ) || ( 1 === get_option( 'mainwp_sslVerifyCertificate' ) ) ) {
400 $ssl_verifyhost = true;
401 }
402 }
403
404 $noBody = false;
405 return static::try_visit( $url, $ssl_verifyhost, $http_user, $http_pass, $sslVersion, $forceUseIPv4, $noBody );
406 }
407
408 /**
409 * Method get_post_data_authed()
410 *
411 * Get authorized $_POST data & build query.
412 *
413 * @param mixed $website Array of Child Site Info.
414 * @param mixed $what What we are posting.
415 * @param null $params Post parameters.
416 * @param array $others Other data.
417 *
418 * @return mixed null|http_build_query()
419 */
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
430 if ( $website && '' !== $what ) {
431 // The nonce must never repeat. The Child stores every legacy signature it accepts, forever,
432 // and refuses a repeat as "This request has already been used"; signing is deterministic,
433 // so the nonce is the only thing that makes two requests for the same action differ.
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;
439
440 $params_filter = apply_filters( 'mainwp_pre_fetch_authed_data', false, $params, $what, $website, $verify_signature );
441 if ( is_array( $params_filter ) && ! empty( $params_filter ) ) {
442 $data = array_merge( $data, $params_filter );
443 }
444
445 if ( null !== $params ) {
446 $data = array_merge( $data, $params );
447 }
448
449 $alg = false;
450 $sign_success = null;
451 $use_seclib = false;
452
453 $data = apply_filters( 'mainwp_get_post_data_authed', $data, $website, $what, $params, $verify_signature );
454
455 $alt_user = '';
456
457 /**
458 * Current user global.
459 *
460 * @global string
461 */
462 global $current_user;
463
464 if ( ( ! defined( 'DOING_CRON' ) || false === DOING_CRON ) && ( ! defined( 'WP_CLI' ) || false === WP_CLI ) && is_object( $current_user ) && property_exists( $current_user, 'ID' ) && $current_user->ID ) {
465 /**
466 * Filter: mainwp_alter_login_user
467 *
468 * Filters users accounts so it allows you user to jump to child site under alternative administrator account.
469 *
470 * @param int $website->id Child site ID.
471 * @param int $current_user->ID User ID.
472 *
473 * @since Unknown
474 */
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 }
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';
508 }
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 }
592 return http_build_query( $data, '', '&' );
593 }
594
595 return null;
596 }
597
598 /**
599 * Method get_renew_post_data_authed()
600 *
601 * Get authorized $_POST data & build query for renew connection action only.
602 *
603 * @param mixed $website Array of Child Site Info.
604 * @param mixed $what What we are posting.
605 * @param array $others Other data.
606 *
607 * @return mixed null|http_build_query()
608 */
609 private static function get_renew_post_data_authed( &$website, $what, $others = array() ) { // phpcs:ignore -- NOSONAR - complex.
610
611 if ( ! is_array( $others ) ) {
612 $others = array();
613 }
614
615 $verify_signature = ! empty( $others['verify_signature'] ) ? true : false;
616
617 if ( $website && '' !== $what ) {
618 $compat_what = 'disconnect'; // to compatible, renew will call disconnect.
619 $data = array();
620 $data['user'] = $website->adminname;
621 $data['function'] = $compat_what;
622 $data['nonce'] = bin2hex( random_bytes( 16 ) );
623
624 $sign_value = $compat_what . $data['nonce']; // compatible format.
625
626 $alg = false;
627 $sign_success = null;
628 $use_seclib = false;
629
630 if ( MainWP_Connect_Lib::is_use_fallback_sec_lib( $website ) ) {
631 // to disconnect.
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.
633 $use_seclib = true;
634 } elseif ( function_exists( 'openssl_verify' ) ) {
635 $alg = MainWP_System_Utility::get_connect_sign_algorithm( $website );
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.
637 if ( empty( $sign_success ) ) { // error from openssl, openssl_sign().
638 $alg = defined( 'OPENSSL_ALGO_SHA1' ) ? OPENSSL_ALGO_SHA1 : false; // to set default SHA1, to disconnect.
639 MainWP_Logger::instance()->debug_for_website( $website, 'get_renew_post_data_authed', '[' . $website->url . '] :: [openssl_sign:failed] :: Set sign_algo=SHA1' );
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.
641 }
642
643 if ( false !== $alg ) {
644 $data['sign_algo'] = $alg;
645 }
646 }
647
648 $data['mainwpsignature'] = ! empty( $signature ) ? base64_encode( $signature ) : ''; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
649
650 if ( null !== $sign_success && empty( $sign_success ) ) {
651 $sign_error = '';
652 while ( $msg = openssl_error_string() ) {
653 if ( is_string( $msg ) ) {
654 $sign_error .= $msg;
655 }
656 }
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 );
658 }
659
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 );
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
707 return http_build_query( $data, '', '&' );
708 }
709 return null;
710 }
711
712
713 /**
714 * Method get_get_data_authed()
715 *
716 * Get authorized $_GET data & build query.
717 *
718 * @param mixed $website Child Site data.
719 * @param mixed $paramValue OpenSSL parameter.
720 * @param string $paramName Parameter name.
721 * @param bool $asArray true|false Default is false.
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.
727 *
728 * @return string $url
729 */
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.
731 $params = array();
732 if ( $website && '' !== $paramValue ) {
733
734 $sign_success = null;
735 $alg = false;
736 $use_seclib = false;
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 );
750 }
751
752 $verify_signature = is_array( $other_params ) && ! empty( $other_params['verify_signature'] );
753
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 }
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 );
777 }
778 } else {
779 $signature = base64_encode( 'useadvancedmainwpsignature' ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
780 }
781
782 $params = array(
783 'login_required' => 1,
784 'user' => rawurlencode( $website->adminname ),
785 'mainwpsignature' => rawurlencode( $signature ),
786 'nonce' => $nonce,
787 $paramName => rawurlencode( $paramValue ),
788 );
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
844 if ( is_array( $other_params ) ) {
845 foreach ( $other_params as $name => $value ) {
846 if ( is_string( $name ) && ! empty( $name ) && is_scalar( $value ) ) {
847 $params[ sanitize_text_field( wp_unslash( $name ) ) ] = rawurlencode( sanitize_text_field( wp_unslash( $value ) ) );
848 }
849 }
850 }
851
852 if ( ! empty( $params['login_required'] ) && ! empty( $params['where'] ) ) {
853 $open_params = apply_filters( 'mainwp_open_site_login_required_params', false, $params, $website );
854 if ( is_array( $open_params ) && ! empty( $open_params ) ) {
855 $where_params = '';
856 foreach ( $open_params as $key => $value ) {
857 $where_params .= rawurlencode( sanitize_text_field( wp_unslash( $key ) ) ) . '=' . rawurlencode( sanitize_text_field( wp_unslash( $value ) ) ) . '&';
858 }
859 if ( ! empty( $where_params ) ) {
860 $params['where_params'] = rawurlencode( rtrim( $where_params, '&' ) );
861 }
862 }
863 }
864
865 if ( ! empty( $use_seclib ) ) {
866 $params['verifylib'] = 1;
867 }
868 }
869
870 if ( $asArray ) {
871 return $params;
872 }
873
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 }
879 $url .= ( substr( $url, - 1 ) !== '/' ? '/' : '' );
880 $url .= '?';
881
882 foreach ( $params as $key => $value ) {
883 $url .= $key . '=' . $value . '&';
884 }
885 return rtrim( $url, '&' );
886 }
887
888 /**
889 * Method connect_sign()
890 *
891 * Sign connect.
892 *
893 * @param string $data Data sign.
894 * @param string $signature signature.
895 * @param string $privkey Private key.
896 * @param mixed $algorithm signature algorithm.
897 * @param int $site_id site id.
898 *
899 * @return bool Success or not.
900 */
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 }
911 if ( false === $algorithm ) {
912 return openssl_sign( $data, $signature, $de_privkey ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
913 } else {
914 return openssl_sign( $data, $signature, $de_privkey, $algorithm ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
915 }
916 }
917
918 /**
919 * Method get_post_data_not_authed()
920 *
921 * Get not authorized $_POST data.
922 *
923 * @param mixed $url Child site URL.
924 * @param mixed $admin Admin Username.
925 * @param mixed $what What function to perform.
926 * @param null $params Function parameters.
927 *
928 * @return mixed null|http_build_query()
929 */
930 public static function get_post_data_not_authed( $url, $admin, $what, $params = null ) {
931 if ( '' !== $url && '' !== $admin && '' !== $what ) {
932 $data = array();
933 $data['user'] = $admin;
934 $data['function'] = $what;
935 $data['mainwpver'] = MainWP_System::$version;
936
937 if ( null !== $params ) {
938 $data = array_merge( $data, $params );
939 }
940
941 return http_build_query( $data, '', '&' );
942 }
943
944 return null;
945 }
946
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 /**
967 * Method fetch_urls_authed()
968 *
969 * Fetches data from child sites if authenticated.
970 *
971 * @param object $websites Websites information.
972 * @param string $what Action to perform.
973 * @param array $params Request parameters.
974 * @param mixed $handler Request handler.
975 * @param mixed $output Request output.
976 * @param mixed $whatPage Request URL. Default /admin-ajax.php.
977 * @param array $others Request additional information.
978 *
979 * @return bool true|false
980 *
981 * @uses \MainWP\Dashboard\MainWP_System::$version
982 * @uses \MainWP\Dashboard\MainWP_System_Utility::get_mainwp_dir()
983 */
984 public static function fetch_urls_authed( &$websites, $what, $params, $handler, &$output, $whatPage = null, $others = array() ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity -- NOSONAR - complex function. Current complexity is the only way to achieve desired results, pull request solutions appreciated.
985
986 if ( ! is_array( $websites ) || empty( $websites ) ) {
987 return false;
988 }
989
990 if ( ! is_array( $params ) ) {
991 $params = array();
992 }
993
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 );
998 if ( count( $websites ) > $chunkSize ) {
999 $total = count( $websites );
1000 $loops = ceil( $total / $chunkSize );
1001 for ( $i = 0; $i < $loops; $i++ ) {
1002 $newSites = array_slice( $websites, $i * $chunkSize, $chunkSize, true );
1003 static::fetch_urls_authed( $newSites, $what, $params, $handler, $output, $whatPage, $others );
1004 sleep( $sleep_int );
1005 }
1006
1007 return false;
1008 }
1009
1010 $agent = 'Mozilla/5.0 (compatible; MainWP/' . MainWP_System::$version . '; +http://mainwp.com)';
1011 $mh = curl_multi_init();
1012
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 }
1031
1032 $disabled_functions = ini_get( 'disable_functions' );
1033 $handleToWebsite = array();
1034 $requestUrls = array();
1035 $requestHandles = array();
1036
1037 $dirs = MainWP_System_Utility::get_mainwp_dir();
1038 $cookieDir = $dirs[0] . 'cookies';
1039
1040 static::init_cookiesdir( $cookieDir );
1041
1042 $_org_params = null;
1043
1044 foreach ( $websites as $website ) {
1045
1046 if ( MainWP_Demo_Handle::get_instance()->is_demo_website( $website ) ) {
1047 MainWP_Demo_Handle::get_instance()->handle_fetch_urls_demo( $data, $website, $output, $what, $params );
1048 continue;
1049 }
1050
1051 $url = $website->url;
1052 if ( '/' !== substr( $url, - 1 ) ) {
1053 $url .= '/';
1054 }
1055
1056 if ( false === strpos( $url, 'wp-admin' ) ) {
1057 $url .= 'wp-admin/';
1058 }
1059
1060 if ( null !== $whatPage ) {
1061 $url .= $whatPage;
1062 } else {
1063 $url .= 'admin-ajax.php';
1064 }
1065
1066 $http_user = null;
1067 $http_pass = null;
1068
1069 if ( property_exists( $website, 'http_user' ) ) {
1070 $http_user = MainWP_Credential_Storage::decrypt_credential( $website->http_user );
1071 }
1072 if ( property_exists( $website, 'http_pass' ) ) {
1073 $http_pass = MainWP_Credential_Storage::decrypt_credential( $website->http_pass );
1074 }
1075
1076 if ( isset( $params ) && isset( $params['new_post'] ) ) {
1077
1078 if ( null === $_org_params ) {
1079 $_org_params = $params;
1080 }
1081
1082 /**
1083 * Filter is being replaced with mainwp_pre_posting_posts.
1084 *
1085 * @deprecated
1086 */
1087 $params = apply_filters_deprecated(
1088 'mainwp-pre-posting-posts',
1089 array(
1090 ( is_array( $params ) ? $params : array() ),
1091 (object) array(
1092 'id' => $website->id,
1093 'url' => $website->url,
1094 'name' => $website->name,
1095 ),
1096 ),
1097 '4.0.7.2', // NOSONAR - not IP.
1098 'mainwp_pre_posting_posts'
1099 );
1100
1101 /**
1102 * Filter: mainwp_pre_posting_posts
1103 *
1104 * Prepares parameters for the authenticated cURL post.
1105 *
1106 * @since 4.1
1107 */
1108 $params = apply_filters(
1109 'mainwp_pre_posting_posts',
1110 ( is_array( $params ) ? $params : array() ),
1111 (object) array(
1112 'id' => $website->id,
1113 'url' => $website->url,
1114 'name' => $website->name,
1115 )
1116 );
1117 }
1118
1119 $ch = curl_init();
1120
1121 $proxy = new \WP_HTTP_Proxy();
1122 if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) {
1123 curl_setopt( $ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP );
1124 curl_setopt( $ch, CURLOPT_PROXY, $proxy->host() );
1125 curl_setopt( $ch, CURLOPT_PROXYPORT, $proxy->port() );
1126
1127 if ( $proxy->use_authentication() ) {
1128 curl_setopt( $ch, CURLOPT_PROXYAUTH, CURLAUTH_ANY );
1129 curl_setopt( $ch, CURLOPT_PROXYUSERPWD, $proxy->authentication() );
1130 }
1131 }
1132
1133 if ( ( null !== $website ) && ( ( property_exists( $website, 'wpe' ) && 1 !== $website->wpe ) || ( isset( $others['upgrade'] ) && ( true === $others['upgrade'] ) ) ) ) {
1134 // to fix.
1135 if ( defined( 'LOGGED_IN_SALT' ) && defined( 'NONCE_SALT' ) ) {
1136 $cookie_salt = sha1( sha1( 'mainwp' . LOGGED_IN_SALT . $website->id ) . NONCE_SALT . 'WP_Cookie' ); // NOSONAR - safe for salt file name.
1137 } else {
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' );
1140 }
1141 $cookieFile = $cookieDir . '/' . $cookie_salt;
1142 if ( ! file_exists( $cookieFile ) ) {
1143 @file_put_contents( $cookieFile, '' );
1144 }
1145
1146 if ( file_exists( $cookieFile ) ) {
1147 @chmod( $cookieFile, 0644 ); // NOSONAR - correct file permissions, owner: rwe, group & others: r.
1148 curl_setopt( $ch, CURLOPT_COOKIEJAR, $cookieFile );
1149 curl_setopt( $ch, CURLOPT_COOKIEFILE, $cookieFile );
1150 }
1151 }
1152
1153 curl_setopt( $ch, CURLOPT_URL, $url );
1154 curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
1155 curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, true );
1156 curl_setopt( $ch, CURLOPT_POST, true );
1157
1158 $postdata = static::get_post_data_authed( $website, $what, $params );
1159 curl_setopt( $ch, CURLOPT_POSTFIELDS, $postdata );
1160 curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, 10 );
1161 curl_setopt( $ch, CURLOPT_USERAGENT, $agent );
1162 curl_setopt( $ch, CURLOPT_REFERER, get_option( 'siteurl' ) );
1163 curl_setopt( $ch, CURLOPT_ENCODING, 'none' );
1164 if ( ! empty( $http_user ) && ! empty( $http_pass ) ) {
1165 $http_pass = stripslashes( $http_pass );
1166 curl_setopt( $ch, CURLOPT_USERPWD, "$http_user:$http_pass" );
1167 }
1168
1169 $ssl_verifyhost = false;
1170 $verifyCertificate = isset( $website->verify_certificate ) ? (int) $website->verify_certificate : null;
1171 if ( null !== $verifyCertificate ) {
1172 if ( 1 === $verifyCertificate ) {
1173 $ssl_verifyhost = true;
1174 } elseif ( 2 === $verifyCertificate ) {
1175 if ( ( false === get_option( 'mainwp_sslVerifyCertificate' ) ) || ( 1 === (int) get_option( 'mainwp_sslVerifyCertificate' ) ) ) {
1176 $ssl_verifyhost = true;
1177 }
1178 }
1179 } elseif ( ( false === get_option( 'mainwp_sslVerifyCertificate' ) ) || ( 1 === (int) get_option( 'mainwp_sslVerifyCertificate' ) ) ) {
1180 $ssl_verifyhost = true;
1181 }
1182
1183 if ( $ssl_verifyhost ) {
1184 curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, 2 );
1185 curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true );
1186 } else {
1187 curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, false ); // NOSONAR.
1188 curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false ); // NOSONAR.
1189 }
1190
1191 curl_setopt( $ch, CURLOPT_SSLVERSION, $website->ssl_version );
1192
1193 if ( is_object( $website ) && property_exists( $website, 'id' ) ) {
1194 $http_version = apply_filters( 'mainwp_curl_http_version', false, $website->id );
1195 if ( false !== $http_version ) {
1196 curl_setopt( $ch, CURLOPT_HTTP_VERSION, $http_version );
1197 }
1198
1199 $curlopt_resolve = apply_filters( 'mainwp_curl_curlopt_resolve', false, $website->id, $website->url );
1200 if ( is_array( $curlopt_resolve ) && ! empty( $curlopt_resolve ) ) {
1201 curl_setopt( $ch, CURLOPT_RESOLVE, $curlopt_resolve );
1202 curl_setopt( $ch, CURLOPT_DNS_USE_GLOBAL_CACHE, false );
1203 }
1204 }
1205
1206 curl_setopt( $ch, CURLOPT_TIMEOUT, $timeout );
1207 MainWP_System_Utility::set_time_limit( $timeout );
1208
1209 if ( empty( $disabled_functions ) || ( false === stristr( $disabled_functions, 'curl_multi_exec' ) ) ) {
1210 @curl_multi_add_handle( $mh, $ch );
1211 }
1212
1213 $handleToWebsite[ static::get_resource_id( $ch ) ] = $website;
1214 $requestUrls[ static::get_resource_id( $ch ) ] = $website->url;
1215 $requestHandles[ static::get_resource_id( $ch ) ] = $ch;
1216
1217 if ( null !== $_org_params ) {
1218 $params = $_org_params;
1219 }
1220 }
1221
1222 if ( empty( $disabled_functions ) || ( false === stristr( $disabled_functions, 'curl_multi_exec' ) ) ) {
1223 $lastRun = 0;
1224 $retry_added = false;
1225 do {
1226 $retry_added = false;
1227 if ( 20 < time() - $lastRun ) {
1228 MainWP_System_Utility::set_time_limit( $timeout );
1229 $lastRun = time();
1230 }
1231
1232 curl_multi_exec( $mh, $running );
1233 curl_multi_select( $mh );
1234 while ( $info = curl_multi_info_read( $mh ) ) {
1235 $data = curl_multi_getcontent( $info['handle'] );
1236 $contains = ( 0 < preg_match( '/<mainwp>(.*)<\/mainwp>/', $data, $results ) );
1237 curl_multi_remove_handle( $mh, $info['handle'] );
1238
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 }
1256 }
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
1263 if ( null !== $handler ) {
1264 $site = &$handleToWebsite[ static::get_resource_id( $info['handle'] ) ];
1265 call_user_func_array( $handler, array( $data, $site, &$output, $params ) );
1266 }
1267
1268 unset( $handleToWebsite[ static::get_resource_id( $info['handle'] ) ] );
1269 if ( static::is_valid_curl_handle( $info['handle'] ) ) {
1270 curl_close( $info['handle'] );
1271 }
1272 unset( $info['handle'] );
1273 }
1274 usleep( 10000 );
1275 } while ( $running > 0 || $retry_added );
1276
1277 if ( static::is_valid_curl_handle( $mh ) ) {
1278 curl_multi_close( $mh );
1279 }
1280 } else {
1281 foreach ( $requestHandles as $ch ) {
1282 $data = curl_exec( $ch );
1283
1284 if ( null !== $handler ) {
1285 $site = &$handleToWebsite[ static::get_resource_id( $ch ) ];
1286 call_user_func_array( $handler, array( $data, $site, &$output, $params ) );
1287 }
1288 }
1289 }
1290
1291 return true;
1292 }
1293
1294 /**
1295 * Credits WordPress org.
1296 *
1297 * Get the correct "Expect" header for the given request data.
1298 *
1299 * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD.
1300 * @return string The "Expect" header.
1301 */
1302 public static function get_expect_header( $data ) {
1303 if ( ! is_array( $data ) ) {
1304 return strlen( (string) $data ) >= 1048576 ? '100-Continue' : '';
1305 }
1306
1307 $bytesize = 0;
1308 $iterator = new \RecursiveIteratorIterator( new \RecursiveArrayIterator( $data ) );
1309
1310 foreach ( $iterator as $datum ) {
1311 $bytesize += strlen( (string) $datum );
1312
1313 if ( $bytesize >= 1048576 ) {
1314 return '100-Continue';
1315 }
1316 }
1317
1318 return '';
1319 }
1320
1321 /**
1322 * Method get_resource_id()
1323 *
1324 * Get resource id.
1325 *
1326 * @param mixed $res The given resource.
1327 *
1328 * @return $result Resource ID only.
1329 */
1330 public static function get_resource_id( $res ) {
1331 $result = false;
1332 if ( is_a( $res, 'CurlHandle' ) ) {
1333 $result = spl_object_hash( $res );
1334 } elseif ( is_resource( $res ) ) {
1335 $resourceString = (string) $res;
1336 $exploded = explode( '#', $resourceString );
1337 $result = array_pop( $exploded );
1338 }
1339 return $result;
1340 }
1341
1342 /**
1343 * Method get_lock_identifier().
1344 *
1345 * Get lock identifier.
1346 *
1347 * @param mixed $pLockName Provided Lock Name.
1348 *
1349 * @return mixed false|sem_get()|@fopen
1350 */
1351 public static function get_lock_identifier( $pLockName ) {
1352 if ( ( null === $pLockName ) || ( false === $pLockName ) ) {
1353 return false;
1354 }
1355
1356 if ( function_exists( 'sem_get' ) ) {
1357 return sem_get( $pLockName );
1358 } else {
1359 $fh = @fopen( sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'lock' . $pLockName . '.txt', 'w+' );
1360 if ( ! $fh ) {
1361 return false;
1362 }
1363
1364 return $fh;
1365 }
1366 }
1367
1368 /**
1369 * Method lock()
1370 *
1371 * Use sem_acquire or @flock to lock the $identifier.
1372 *
1373 * @param mixed $identifier Identifier.
1374 *
1375 * @return mixed false|sem_acquire()|@flock
1376 */
1377 public static function lock( $identifier ) {
1378 if ( ( null === $identifier ) || ( false === $identifier ) ) {
1379 return false;
1380 }
1381
1382 if ( function_exists( 'sem_acquire' ) ) {
1383 return sem_acquire( $identifier );
1384 } else {
1385 if ( ! is_resource( $identifier ) ) {
1386 return false; // to fix.
1387 }
1388 for ( $i = 0; $i < 3; $i++ ) {
1389 if ( @flock( $identifier, LOCK_EX ) ) {
1390 return $identifier;
1391 } else {
1392 sleep( 1 );
1393 }
1394 }
1395 return false;
1396 }
1397 }
1398
1399 /**
1400 * Method release()
1401 *
1402 * Use sem_release or @flock, @fclose to unlock $identifier.
1403 *
1404 * @param mixed $identifier Identifier.
1405 *
1406 * @return mixed false|sem_release()|@flock
1407 */
1408 public static function release( $identifier ) {
1409 if ( ( null === $identifier ) || ( false === $identifier ) ) {
1410 return false;
1411 }
1412
1413 if ( function_exists( 'sem_release' ) ) {
1414 return sem_release( $identifier );
1415 } else {
1416 if ( ! is_resource( $identifier ) ) {
1417 return false; // to fix.
1418 }
1419 @flock( $identifier, LOCK_UN );
1420 @fclose( $identifier );
1421 }
1422
1423 return false;
1424 }
1425
1426 /**
1427 * Method fetch_url_authed()
1428 *
1429 * Updates the child site via authenticated request.
1430 *
1431 * @param object $website Website information.
1432 * @param string $what Function to perform.
1433 * @param null $params Function parameters.
1434 * @param bool $checkConstraints Whether or not to check constraints.
1435 * @param bool $pForceFetch Whether or not to force the fetch.
1436 * @param bool $pRetryFailed Whether or not to retry the fetch process.
1437 * @param null $rawResponse Raw response.
1438 * @param bool $verify_signature Wether verify signature data.
1439 *
1440 * @return mixed $information
1441 *
1442 * @uses \MainWP\Dashboard\MainWP_Monitoring_Handler::handle_check_website()
1443 * @uses \MainWP\Dashboard\MainWP_Premium_Update::maybe_request_premium_updates()
1444 * @uses \MainWP\Dashboard\MainWP_Sync::sync_information_array()
1445 */
1446 public static function fetch_url_authed( // phpcs:ignore -- NOSONAR - complex.
1447 &$website,
1448 $what,
1449 $params = null,
1450 $checkConstraints = false,
1451 $pForceFetch = false,
1452 $pRetryFailed = true,
1453 $rawResponse = null,
1454 $verify_signature = false
1455 ) {
1456 unset( $pForceFetch );
1457
1458 // to support demo data.
1459 if ( MainWP_Demo_Handle::get_instance()->is_demo_website( $website ) ) {
1460 return MainWP_Demo_Handle::get_instance()->handle_action_demo( $website, $what );
1461 }
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
1497 if ( ! is_array( $params ) ) {
1498 $params = array();
1499 }
1500
1501 $others = array(
1502 'force_use_ipv4' => $website->force_use_ipv4,
1503 'upgrade' => ( 'upgradeplugintheme' === $what || 'upgrade' === $what || 'upgradetranslation' === $what ),
1504 );
1505
1506 if ( isset( $rawResponse ) && $rawResponse ) {
1507 $others['raw_response'] = 'yes';
1508 }
1509
1510 $params['optimize'] = ( ( 1 === (int) get_option( 'mainwp_optimize', 1 ) ) ? 1 : 0 );
1511
1512 $updating_website = false;
1513 $type = '';
1514 $list = '';
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 ) {
1519 $updating_website = true;
1520 if ( $premium_update ) {
1521 $type = $params['premium_type'];
1522 $list = $params['list'];
1523 } elseif ( 'upgradeplugintheme' === $what || 'upgradetranslation' === $what ) {
1524 $type = $params['type'];
1525 $list = $params['list'];
1526 } else {
1527 $type = 'wp';
1528 $list = '';
1529 }
1530 }
1531
1532 if ( $updating_website ) {
1533 /**
1534 * Action: mainwp_website_before_updated
1535 *
1536 * Fires before the child site update process.
1537 *
1538 * @param object $website Object containing child site info.
1539 * @param string $type Type parameter.
1540 * @param string $list List parameter.
1541 *
1542 * @since Unknown
1543 */
1544 do_action( 'mainwp_website_before_updated', $website, $type, $list );
1545 }
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
1564 if ( 'renew' === $what ) {
1565 $postdata = static::get_renew_post_data_authed(
1566 $website,
1567 $what,
1568 array(
1569 'verify_signature' => $verify_signature,
1570 )
1571 );
1572 } else {
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 );
1583
1584 }
1585
1586 $others['function'] = $what;
1587
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 );
1589
1590 if ( ! empty( $output ) ) {
1591 if ( ! is_array( $information ) ) {
1592 $information = array();
1593 }
1594 $information['fetch_url_output'] = $output;
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 );
1608
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 }
1619 }
1620
1621 if ( $updating_website ) {
1622 /**
1623 * Action: mainwp_website_updated
1624 *
1625 * Fires after the child site update process.
1626 *
1627 * @param object $website Object containing child site info.
1628 * @param string $type Type parameter.
1629 * @param string $list List parameter.
1630 * @param array $information Array containing the information fetched from the child site.
1631 *
1632 * @since Unknown
1633 */
1634 do_action( 'mainwp_website_updated', $website, $type, $list, $information );
1635 if ( 1 === (int) get_option( 'mainwp_check_http_response', 0 ) ) {
1636 MainWP_Monitoring_Handler::handle_check_website( $website, true );
1637 }
1638 }
1639
1640 return $information;
1641 }
1642
1643 /**
1644 * Method fetch_url_not_authed()
1645 *
1646 * Fetch not authorized URL.
1647 *
1648 * @param string $url URL to fetch from.
1649 * @param string $admin Admin name.
1650 * @param string $what Function to perform.
1651 * @param null $params Function parameters.
1652 * @param bool $pForceFetch true|false Whether or not to force the fetch.
1653 * @param null $verifyCertificate Verify the SSL Certificate.
1654 * @param null $http_user htaccess username.
1655 * @param null $http_pass htaccess password.
1656 * @param integer $sslVersion SSL version to check for.
1657 * @param array $others Other functions to perform.
1658 * @param array $output Output values.
1659 *
1660 * @return mixed static::fetch_url() Fetch URL.
1661 */
1662 public static function fetch_url_not_authed( // phpcs:ignore -- NOSONAR - compatible.
1663 $url,
1664 $admin,
1665 $what,
1666 $params = null,
1667 $pForceFetch = false,
1668 $verifyCertificate = null,
1669 $http_user = null,
1670 $http_pass = null,
1671 $sslVersion = 0,
1672 $others = array(),
1673 &$output = array()
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
1739 unset( $pForceFetch );
1740
1741 if ( empty( $params ) ) {
1742 $params = array();
1743 }
1744
1745 $postdata = static::get_post_data_not_authed( $url, $admin, $what, $params );
1746 $website = null;
1747
1748 $others['function'] = $what;
1749 return static::fetch_url( $website, $url, $postdata, false, $verifyCertificate, true, $http_user, $http_pass, $sslVersion, $others, $output );
1750 }
1751
1752 /**
1753 * Method fetch_url()
1754 *
1755 * Fetch URL.
1756 *
1757 * @param object $website Child Site info.
1758 * @param string $url URL to fetch from.
1759 * @param mixed $postdata Post data to fetch.
1760 * @param bool $checkConstraints true|false Whether or not to check constraints.
1761 * @param null $verifyCertificate Verify SSL Certificate.
1762 * @param bool $pRetryFailed ture|false Whether or not the Retry has failed.
1763 * @param null $http_user htaccess username.
1764 * @param null $http_pass htaccess password.
1765 * @param integer $sslVersion SSL version.
1766 * @param array $others Other functions to perform.
1767 * @param array $output Output values.
1768 *
1769 * @throws \Exception Exception message.
1770 *
1771 * @return mixed static::fetch_url_site()
1772 */
1773 public static function fetch_url( // phpcs:ignore -- NOSONAR - complex.
1774 &$website,
1775 $url,
1776 $postdata,
1777 $checkConstraints = false,
1778 $verifyCertificate = null,
1779 $pRetryFailed = true,
1780 $http_user = null,
1781 $http_pass = null,
1782 $sslVersion = 0,
1783 $others = array(),
1784 &$output = array()
1785 ) {
1786
1787 $start = time();
1788
1789 try {
1790 $tmpUrl = $url;
1791 if ( '/' !== substr( $tmpUrl, - 1 ) ) {
1792 $tmpUrl .= '/';
1793 }
1794
1795 if ( false === strpos( $url, 'wp-admin' ) ) {
1796 $tmpUrl .= 'wp-admin/admin-ajax.php';
1797 }
1798
1799 return static::fetch_url_site( $website, $tmpUrl, $postdata, $checkConstraints, $verifyCertificate, $http_user, $http_pass, $sslVersion, $others, $output );
1800 } catch ( \Exception $e ) {
1801 if ( ! $pRetryFailed || ( 30 < ( time() - $start ) ) ) {
1802 throw $e;
1803 }
1804
1805 try {
1806 return static::fetch_url_site( $website, $url, $postdata, $checkConstraints, $verifyCertificate, $http_user, $http_pass, $sslVersion, $others, $output );
1807 } catch ( \Exception $ex ) {
1808 throw $e;
1809 }
1810 }
1811 }
1812
1813 /**
1814 * Method fetch_url_site()
1815 *
1816 * M Fetch URL.
1817 *
1818 * @param object $website Child Site info.
1819 * @param string $url URL to fetch from.
1820 * @param mixed $postdata Post data to fetch.
1821 * @param bool $checkConstraints true|false Whether or not to check constraints.
1822 * @param null $verifyCertificate Verify SSL Certificate.
1823 * @param null $http_user htaccess username.
1824 * @param null $http_pass htaccess password.
1825 * @param integer $sslVersion SSL version.
1826 * @param array $others Other functions to perform.
1827 * @param array $output Output values.
1828 *
1829 * @return mixed $data, $information.
1830 * @throws MainWP_Exception Exception message.
1831 *
1832 * @uses \MainWP\Dashboard\MainWP_DB_Common::insert_or_update_request_log()
1833 * @uses \MainWP\Dashboard\MainWP_Exception
1834 * @uses \MainWP\Dashboard\MainWP_Logger::debug_for_website()
1835 * @uses \MainWP\Dashboard\MainWP_System::$version
1836 * @uses \MainWP\Dashboard\MainWP_System_Utility::get_mainwp_dir()
1837 * @uses \MainWP\Dashboard\MainWP_System_Utility::get_child_response()
1838 * @uses \MainWP\Dashboard\MainWP_Utility::value_to_string()
1839 * @uses \MainWP\Dashboard\MainWP_Utility::end_session()
1840 */
1841 public static function fetch_url_site( // phpcs:ignore -- NOSONAR - complex method. Current complexity is the only way to achieve desired results, pull request solutions appreciated.
1842 &$website,
1843 $url,
1844 $postdata,
1845 $checkConstraints = false,
1846 $verifyCertificate = null,
1847 $http_user = null,
1848 $http_pass = null,
1849 $sslVersion = 0,
1850 $others = array(),
1851 &$output = array()
1852 ) {
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
1887 $agent = 'Mozilla/5.0 (compatible; MainWP/' . MainWP_System::$version . '; +http://mainwp.com)';
1888
1889 if ( ! empty( $website ) ) {
1890 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url_site', 'Request to [' . $url . '] [' . MainWP_Utility::value_to_string( $postdata, 1 ) . ']' );
1891 }
1892
1893 $identifier = null;
1894 if ( $checkConstraints ) {
1895 static::check_constraints( $identifier, $website );
1896 }
1897
1898 if ( null !== $website ) {
1899 MainWP_DB_Common::instance()->insert_or_update_request_log( $website->id, null, microtime( true ), null );
1900 }
1901
1902 if ( null !== $identifier ) {
1903 static::release( $identifier );
1904 }
1905
1906 $dirs = MainWP_System_Utility::get_mainwp_dir();
1907 $cookieDir = $dirs[0] . 'cookies';
1908
1909 static::init_cookiesdir( $cookieDir );
1910
1911 $fetch_track_id = MainWP_Execution_Helper::execute_call_track( 'start_point', $website, $postdata );
1912
1913 $ch = curl_init();
1914
1915 $proxy = new \WP_HTTP_Proxy();
1916 if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) {
1917 curl_setopt( $ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP );
1918 curl_setopt( $ch, CURLOPT_PROXY, $proxy->host() );
1919 curl_setopt( $ch, CURLOPT_PROXYPORT, $proxy->port() );
1920
1921 if ( $proxy->use_authentication() ) {
1922 curl_setopt( $ch, CURLOPT_PROXYAUTH, CURLAUTH_ANY );
1923 curl_setopt( $ch, CURLOPT_PROXYUSERPWD, $proxy->authentication() );
1924 }
1925 }
1926
1927 if ( ( null !== $website ) && ( ( property_exists( $website, 'wpe' ) && 1 !== $website->wpe ) || ( isset( $others['upgrade'] ) && ( true === $others['upgrade'] ) ) ) ) {
1928 // to fix.
1929 if ( defined( 'LOGGED_IN_SALT' ) && defined( 'NONCE_SALT' ) ) {
1930 $cookie_salt = sha1( sha1( 'mainwp' . LOGGED_IN_SALT . $website->id ) . NONCE_SALT . 'WP_Cookie' ); // NOSONAR - safe for salt file name.
1931 } else {
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' );
1934 }
1935 $cookieFile = $cookieDir . '/' . $cookie_salt;
1936 if ( ! file_exists( $cookieFile ) ) {
1937 @file_put_contents( $cookieFile, '' );
1938 }
1939
1940 if ( file_exists( $cookieFile ) ) {
1941 @chmod( $cookieFile, 0644 ); // NOSONAR - correct file permissions, owner: rwe, group & others: r.
1942 curl_setopt( $ch, CURLOPT_COOKIEJAR, $cookieFile );
1943 curl_setopt( $ch, CURLOPT_COOKIEFILE, $cookieFile );
1944 }
1945 }
1946
1947 curl_setopt( $ch, CURLOPT_URL, $url );
1948 curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
1949 curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, true );
1950 curl_setopt( $ch, CURLOPT_POST, true );
1951 curl_setopt( $ch, CURLOPT_POSTFIELDS, $postdata );
1952 curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, 10 );
1953 curl_setopt( $ch, CURLOPT_USERAGENT, $agent );
1954 curl_setopt( $ch, CURLOPT_ENCODING, 'none' );
1955
1956 if ( ! empty( $http_user ) && ! empty( $http_pass ) ) {
1957 $http_pass = stripslashes( $http_pass );
1958 curl_setopt( $ch, CURLOPT_USERPWD, "$http_user:$http_pass" );
1959 }
1960
1961 $ssl_verifyhost = false;
1962 if ( null !== $verifyCertificate ) {
1963 if ( 1 === (int) $verifyCertificate ) {
1964 $ssl_verifyhost = true;
1965 } elseif ( 2 === (int) $verifyCertificate ) {
1966 if ( ( false === get_option( 'mainwp_sslVerifyCertificate' ) ) || ( 1 === (int) get_option( 'mainwp_sslVerifyCertificate' ) ) ) {
1967 $ssl_verifyhost = true;
1968 }
1969 }
1970 } elseif ( ( false === get_option( 'mainwp_sslVerifyCertificate' ) ) || ( 1 === (int) get_option( 'mainwp_sslVerifyCertificate' ) ) ) {
1971 $ssl_verifyhost = true;
1972 }
1973
1974 if ( $ssl_verifyhost ) {
1975 curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, 2 );
1976 curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true );
1977 } else {
1978 curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, false ); // NOSONAR.
1979 curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false ); // NOSONAR.
1980 }
1981
1982 curl_setopt( $ch, CURLOPT_SSLVERSION, $sslVersion );
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
1991 if ( is_object( $website ) && property_exists( $website, 'id' ) ) {
1992 $curlopt_resolve = apply_filters( 'mainwp_curl_curlopt_resolve', false, $website->id, $website->url );
1993 }
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
2000 $headers = array( 'X-Requested-With' => 'XMLHttpRequest' );
2001 $headers['Expect'] = static::get_expect_header( $postdata );
2002
2003 $headers = apply_filters( 'mainwp_connect_http_request_headers', $headers, $website );
2004
2005 if ( class_exists( '\WpOrg\Requests\Requests' ) ) {
2006 $headers = \WpOrg\Requests\Requests::flatten( $headers );
2007 } else {
2008 $headers = \Requests::flatten( $headers );
2009 }
2010
2011 curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
2012 curl_setopt( $ch, CURLOPT_REFERER, get_option( 'siteurl' ) );
2013
2014 $force_use_ipv4 = false;
2015 $forceUseIPv4 = isset( $others['force_use_ipv4'] ) ? (int) $others['force_use_ipv4'] : null;
2016 if ( null !== $forceUseIPv4 ) {
2017 if ( 1 === $forceUseIPv4 ) {
2018 $force_use_ipv4 = true;
2019 } elseif ( 2 === $forceUseIPv4 ) {
2020 if ( 1 === (int) get_option( 'mainwp_forceUseIPv4' ) ) {
2021 $force_use_ipv4 = true;
2022 }
2023 }
2024 } elseif ( 1 === (int) get_option( 'mainwp_forceUseIPv4' ) ) {
2025 $force_use_ipv4 = true;
2026 }
2027
2028 if ( $force_use_ipv4 && defined( 'CURLOPT_IPRESOLVE' ) && defined( 'CURL_IPRESOLVE_V4' ) ) {
2029 curl_setopt( $ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4 );
2030 }
2031
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
2049 curl_setopt( $ch, CURLOPT_TIMEOUT, $timeout );
2050 MainWP_System_Utility::set_time_limit( $timeout );
2051
2052 MainWP_Utility::end_session();
2053
2054 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url_site', 'Executing handlers' );
2055
2056 $disabled_functions = ini_get( 'disable_functions' );
2057 if ( empty( $disabled_functions ) || ( false === stristr( $disabled_functions, 'curl_multi_exec' ) ) ) {
2058 $mh = @curl_multi_init();
2059 @curl_multi_add_handle( $mh, $ch );
2060
2061 $lastRun = 0;
2062 $running = null;
2063
2064 do {
2065 if ( 20 < time() - $lastRun ) {
2066 MainWP_System_Utility::set_time_limit( $timeout );
2067 $lastRun = time();
2068 }
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
2079 while ( $info = @curl_multi_info_read( $mh ) ) {
2080 $data = @curl_multi_getcontent( $info['handle'] );
2081 $http_status = @curl_getinfo( $info['handle'], CURLINFO_HTTP_CODE );
2082 $err = @curl_error( $info['handle'] );
2083 $real_url = @curl_getinfo( $info['handle'], CURLINFO_EFFECTIVE_URL );
2084
2085 @curl_multi_remove_handle( $mh, $info['handle'] );
2086 curl_close( $info['handle'] );
2087 }
2088 usleep( 10000 );
2089 } while ( $running > 0 );
2090
2091 if ( static::is_valid_curl_handle( $mh ) ) {
2092 @curl_multi_close( $mh );
2093 }
2094 } else {
2095 $data = @curl_exec( $ch );
2096 $http_status = @curl_getinfo( $ch, CURLINFO_HTTP_CODE );
2097 $err = @curl_error( $ch );
2098 $real_url = @curl_getinfo( $ch, CURLINFO_EFFECTIVE_URL );
2099 curl_close( $ch );
2100 }
2101
2102 $host = wp_parse_url( $real_url, PHP_URL_HOST );
2103 $ip = gethostbyname( $host );
2104
2105 MainWP_Execution_Helper::execute_call_track( 'end_point', $website, $postdata, $fetch_track_id, 'fetch site' );
2106
2107 if ( null !== $website ) {
2108 MainWP_DB_Common::instance()->insert_or_update_request_log( $website->id, $ip, null, microtime( true ) );
2109 }
2110
2111 $raw_response = isset( $others['raw_response'] ) && 'yes' === $others['raw_response'] ? true : false;
2112
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
2125 $output['http_status'] = (int) $http_status;
2126
2127 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url_site', 'http status: [' . $http_status . '] err: [' . $err . ']' );
2128 if ( '400' === $http_status ) {
2129 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url_site', 'post data: [' . MainWP_Utility::value_to_string( $postdata, 1 ) . ']' );
2130 }
2131
2132 MainWP_Logger::instance()->log_execution_time( 'fetch_url_site :: [url=' . $url . ']' );
2133
2134 $thr_error = null;
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
2145 if ( ( false === $data ) && empty( $http_status ) ) {
2146 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url', '[' . $url . '] HTTP Error: [status=0][' . $err . ']' );
2147 $thr_error = new MainWP_Exception( 'HTTPERROR', $err ); //phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
2148 $output['error_category'] = 'http_error';
2149 } elseif ( empty( $data ) && ! empty( $err ) ) {
2150 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url', '[' . $url . '] HTTP Error: [status=' . $http_status . '][' . $err . ']' );
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;
2155 } elseif ( 0 < preg_match( '/<mainwp>(.*)<\/mainwp>/', $data, $results ) ) {
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.
2159 unset( $output['fetch_data'] ); // hide the data.
2160 $pdt = is_string( $postdata ) ? $postdata : '';
2161 $data_log = is_array( $postdata ) ? print_r( $postdata, true ) : $pdt; //phpcs:ignore -- good.
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
2187 return $information;
2188 } elseif ( 200 === (int) $http_status && ! empty( $err ) ) {
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.
2194 } elseif ( $raw_response ) {
2195 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url_site', 'Response: [RAW]' );
2196 return $data;
2197 } else {
2198 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url', '[' . $url . '] Error: NOMAINWP [data=' . $hidden_data . ']' );
2199 $detect_wsidchk = is_string( $data ) ? strpos( $data, 'wsidchk' ) : false;
2200 if ( false !== $detect_wsidchk ) {
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 );
2203 } else {
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.';
2206 }
2207 $output['error_category'] = 'child_plugin_missing';
2208 $output['error_message'] = $err_msg;
2209 }
2210
2211 if ( null !== $thr_error ) {
2212 $thr_error->set_data( $hidden_data ); // to compatible.
2213 throw $thr_error;
2214 }
2215 }
2216
2217 /**
2218 * Method check_constraints()
2219 *
2220 * Check connection delay constraints.
2221 *
2222 * @param mixed $identifier Lock identifier.
2223 * @param mixed $website Object child site.
2224 *
2225 * @uses \MainWP\Dashboard\MainWP_DB_Common::close_open_requests()
2226 * @uses \MainWP\Dashboard\MainWP_DB::get_wp_ip()
2227 * @uses \MainWP\Dashboard\MainWP_Utility::end_session()
2228 */
2229 private static function check_constraints( &$identifier, $website ) { // phpcs:ignore -- NOSONAR -Current complexity is the only way to achieve desired results, pull request solutions appreciated.
2230 $semLock = '103218';
2231 $identifier = static::get_lock_identifier( $semLock );
2232 $minimumDelay = ( ( false === get_option( 'mainwp_minimumDelay' ) ) ? 200 : get_option( 'mainwp_minimumDelay' ) );
2233 if ( 0 < $minimumDelay ) {
2234 $minimumDelay = $minimumDelay / 1000;
2235 }
2236 $minimumIPDelay = ( ( false === get_option( 'mainwp_minimumIPDelay' ) ) ? 1000 : get_option( 'mainwp_minimumIPDelay' ) );
2237 if ( 0 < $minimumIPDelay ) {
2238 $minimumIPDelay = $minimumIPDelay / 1000;
2239 }
2240
2241 MainWP_Utility::end_session();
2242 $delay = true;
2243 while ( $delay ) {
2244 static::lock( $identifier );
2245 if ( 0 < $minimumDelay && static::check_constraints_last_request( $identifier, $minimumDelay ) ) {
2246 continue;
2247 }
2248
2249 if ( 0 < $minimumIPDelay && null !== $website ) {
2250 $ip = MainWP_DB::instance()->get_wp_ip( $website->id );
2251 if ( null !== $ip && '' !== $ip && static::check_constraints_last_request( $identifier, $minimumIPDelay, $ip ) ) {
2252 continue;
2253 }
2254 }
2255 $delay = false;
2256 }
2257
2258 $maximumRequests = ( ( false === get_option( 'mainwp_maximumRequests' ) ) ? 4 : get_option( 'mainwp_maximumRequests' ) );
2259 $maximumIPRequests = ( ( false === get_option( 'mainwp_maximumIPRequests' ) ) ? 1 : get_option( 'mainwp_maximumIPRequests' ) );
2260
2261 $first = true;
2262 $delay = true;
2263 while ( $delay ) {
2264 if ( ! $first ) {
2265 static::lock( $identifier );
2266 } else {
2267 $first = false;
2268 }
2269
2270 MainWP_DB_Common::instance()->close_open_requests();
2271
2272 if ( 0 < $maximumRequests && static::check_constraints_open_requests( $identifier, $maximumRequests ) ) {
2273 continue;
2274 }
2275
2276 if ( 0 < $maximumIPRequests && null !== $website ) {
2277 $ip = MainWP_DB::instance()->get_wp_ip( $website->id );
2278 if ( null !== $ip && '' !== $ip && static::check_constraints_open_requests( $identifier, $maximumIPRequests, $ip ) ) {
2279 continue;
2280 }
2281 }
2282 $delay = false;
2283 }
2284 }
2285
2286 /**
2287 * Method check_constraints_last_request().
2288 *
2289 * Check constraints for last requests.
2290 *
2291 * @param mixed $identifier connect identifier.
2292 * @param int $minimumDelay minimum delay.
2293 * @param string|null $ip ip address.
2294 *
2295 * @uses \MainWP\Dashboard\MainWP_DB_Common::get_last_request_timestamp()
2296 */
2297 private static function check_constraints_last_request( $identifier, $minimumDelay, $ip = null ) {
2298 $lastRequest = MainWP_DB_Common::instance()->get_last_request_timestamp( $ip );
2299 if ( $lastRequest > ( ( microtime( true ) ) - $minimumDelay ) ) {
2300 static::release( $identifier );
2301 $sleep = ( $minimumDelay - ( ( microtime( true ) ) - $lastRequest ) ) * 1000 * 1000;
2302 $sleep = max( 0, intval( $sleep ) );
2303 usleep( $sleep );
2304 return true;
2305 }
2306 return false;
2307 }
2308
2309 /**
2310 * Method check_constraints_open_requests().
2311 *
2312 * Check constraints for open requests.
2313 *
2314 * @param mixed $identifier connect identifier.
2315 * @param int $maximumRequests maximum requests.
2316 * @param string|null $ip ip address.
2317 *
2318 * @uses \MainWP\Dashboard\MainWP_DB_Common::get_nrof_open_requests()
2319 */
2320 private static function check_constraints_open_requests( $identifier, $maximumRequests, $ip = null ) {
2321 $nrOfOpenRequests = MainWP_DB_Common::instance()->get_nrof_open_requests( $ip );
2322 if ( $nrOfOpenRequests >= $maximumRequests ) {
2323 static::release( $identifier );
2324 usleep( 200000 );
2325 return true;
2326 }
2327 return false;
2328 }
2329
2330 /**
2331 * Method download_to_file()
2332 *
2333 * Download to file.
2334 *
2335 * @param mixed $url Download URL.
2336 * @param mixed $file File to download to.
2337 * @param bool $size Size of file.
2338 * @param null $http_user htaccess username.
2339 * @param null $http_pass htaccess password.
2340 *
2341 * @throws MainWP_Exception Exception message.
2342 *
2343 * @uses \MainWP\Dashboard\MainWP_Exception
2344 * @uses \MainWP\Dashboard\MainWP_System::$version
2345 * @uses \MainWP\Dashboard\MainWP_System_Utility::get_wp_file_system()
2346 */
2347 public static function download_to_file( $url, $file, $size = false, $http_user = null, $http_pass = null ) { // phpcs:ignore -- NOSONAR - complex.
2348
2349 $hasWPFileSystem = MainWP_System_Utility::get_wp_file_system();
2350
2351 /**
2352 * WordPress files system object.
2353 *
2354 * @global object
2355 */
2356 global $wp_filesystem;
2357
2358 if ( $wp_filesystem->exists( $file ) && ( ( false === $size ) || ( $wp_filesystem->size( $file ) > $size ) ) ) {
2359 $wp_filesystem->delete( $file );
2360 }
2361
2362 if ( ! $wp_filesystem->exists( dirname( $file ) ) ) {
2363 $wp_filesystem->mkdir( dirname( $file ), 0750 ); // MWP-1558: tightened from 0777; downloaded files may contain backup data.
2364 }
2365
2366 if ( ! $wp_filesystem->exists( dirname( $file ) ) ) {
2367 throw new MainWP_Exception( esc_html__( 'MainWP plugin could not create directory in order to download the file.', 'mainwp' ) );
2368 }
2369
2370 if ( $hasWPFileSystem && ! empty( $wp_filesystem ) ) {
2371 if ( ! $wp_filesystem->is_writable( @dirname( $file ) ) ) {
2372 throw new MainWP_Exception( esc_html__( 'MainWP upload directory is not writable.', 'mainwp' ) );
2373 }
2374 } elseif ( ! is_writable( @dirname( $file ) ) ) { //phpcs:ignore -- ok.
2375 throw new MainWP_Exception( esc_html__( 'MainWP upload directory is not writable.', 'mainwp' ) );
2376 }
2377
2378 $fp = fopen( $file, 'a' );
2379 $agent = 'Mozilla/5.0 (compatible; MainWP/' . MainWP_System::$version . '; +http://mainwp.com)';
2380 if ( false !== $size && $wp_filesystem->exists( $file ) ) {
2381 $size = $wp_filesystem->size( $file );
2382 $url .= '&foffset=' . $size;
2383 }
2384 $ch = curl_init( str_replace( ' ', '%20', $url ) );
2385
2386 $proxy = new \WP_HTTP_Proxy();
2387 if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) {
2388 curl_setopt( $ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP );
2389 curl_setopt( $ch, CURLOPT_PROXY, $proxy->host() );
2390 curl_setopt( $ch, CURLOPT_PROXYPORT, $proxy->port() );
2391
2392 if ( $proxy->use_authentication() ) {
2393 curl_setopt( $ch, CURLOPT_PROXYAUTH, CURLAUTH_ANY );
2394 curl_setopt( $ch, CURLOPT_PROXYUSERPWD, $proxy->authentication() );
2395 }
2396 }
2397 curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
2398 curl_setopt( $ch, CURLOPT_HEADER, false );
2399 curl_setopt( $ch, CURLOPT_FILE, $fp );
2400
2401 curl_setopt( $ch, CURLOPT_USERAGENT, $agent );
2402 curl_setopt( $ch, CURLOPT_ENCODING, 'none' );
2403
2404 curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, true );
2405 if ( ! empty( $http_user ) && ! empty( $http_pass ) ) {
2406 $http_pass = stripslashes( $http_pass );
2407 curl_setopt( $ch, CURLOPT_USERPWD, "$http_user:$http_pass" );
2408 }
2409 curl_exec( $ch );
2410 if ( static::is_valid_curl_handle( $ch ) ) {
2411 curl_close( $ch );
2412 }
2413 fclose( $fp );
2414 }
2415
2416 /**
2417 * Method init_coockiesdir()
2418 *
2419 * Check for cookies directory and create it if it doesn't already exist,
2420 * set the file permissions and update htaccess.
2421 *
2422 * @param mixed $cookieDir Cookies directory.
2423 *
2424 * @return void
2425 *
2426 * @uses \MainWP\Dashboard\MainWP_System_Utility::get_wp_file_system()
2427 */
2428 public static function init_cookiesdir( $cookieDir ) {
2429
2430 $hasWPFileSystem = MainWP_System_Utility::get_wp_file_system();
2431
2432 /**
2433 * WordPress files system object.
2434 *
2435 * @global object
2436 */
2437 global $wp_filesystem;
2438
2439 if ( $hasWPFileSystem && ! empty( $wp_filesystem ) ) {
2440
2441 if ( ! $wp_filesystem->is_dir( $cookieDir ) ) {
2442 $wp_filesystem->mkdir( $cookieDir, 0750 ); // MWP-1558: tightened from 0777; cookies/ holds child wp-admin session cookies.
2443 }
2444
2445 if ( ! file_exists( $cookieDir . '/.htaccess' ) ) {
2446 $file_htaccess = $cookieDir . '/.htaccess';
2447 $wp_filesystem->put_contents( $file_htaccess, 'deny from all' );
2448 }
2449
2450 if ( ! file_exists( $cookieDir . '/index.php' ) ) {
2451 $file_index = $cookieDir . '/index.php';
2452 $wp_filesystem->touch( $file_index );
2453 }
2454 } else {
2455
2456 if ( ! file_exists( $cookieDir ) ) {
2457 @mkdir( $cookieDir, 0750, true ); // MWP-1558: tightened from 0777; cookies/ holds child wp-admin session cookies.
2458 }
2459
2460 if ( ! file_exists( $cookieDir . '/.htaccess' ) ) {
2461 $file_htaccess = @fopen( $cookieDir . '/.htaccess', 'w+' );
2462 @fwrite( $file_htaccess, 'deny from all' );
2463 @fclose( $file_htaccess );
2464 }
2465
2466 if ( ! file_exists( $cookieDir . '/index.php' ) ) {
2467 $file_index = @fopen( $cookieDir . '/index.php', 'w+' );
2468 @fclose( $file_index );
2469 }
2470 }
2471 }
2472
2473 /**
2474 * Method get_file_content()
2475 *
2476 * Get contents of file.
2477 *
2478 * @param mixed $url File Location.
2479 *
2480 * @return mixed false|$data
2481 *
2482 * @uses \MainWP\Dashboard\MainWP_System::$version
2483 */
2484 public static function get_file_content( $url ) {
2485 $agent = 'Mozilla/5.0 (compatible; MainWP/' . MainWP_System::$version . '; +http://mainwp.com)';
2486 $ch = curl_init();
2487
2488 $proxy = new \WP_HTTP_Proxy();
2489 if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) {
2490 curl_setopt( $ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP );
2491 curl_setopt( $ch, CURLOPT_PROXY, $proxy->host() );
2492 curl_setopt( $ch, CURLOPT_PROXYPORT, $proxy->port() );
2493
2494 if ( $proxy->use_authentication() ) {
2495 curl_setopt( $ch, CURLOPT_PROXYAUTH, CURLAUTH_ANY );
2496 curl_setopt( $ch, CURLOPT_PROXYUSERPWD, $proxy->authentication() );
2497 }
2498 }
2499
2500 curl_setopt( $ch, CURLOPT_HEADER, 0 );
2501 curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
2502 curl_setopt( $ch, CURLOPT_URL, $url );
2503 curl_setopt( $ch, CURLOPT_USERAGENT, $agent );
2504 curl_setopt( $ch, CURLOPT_ENCODING, 'none' );
2505
2506 $data = @curl_exec( $ch );
2507 $httpCode = @curl_getinfo( $ch, CURLINFO_HTTP_CODE );
2508 if ( static::is_valid_curl_handle( $ch ) ) {
2509 curl_close( $ch );
2510 }
2511 if ( 200 === (int) $httpCode ) {
2512 return $data;
2513 } else {
2514 return false;
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 );
2530 }
2531
2532 /**
2533 * Method get_favico_url()
2534 *
2535 * Get Child Site favicon URL.
2536 *
2537 * @param mixed $website Child Site info.
2538 *
2539 * @return mixed $faviurl Favicon URL.
2540 *
2541 * @uses \MainWP\Dashboard\MainWP_DB::get_website_option()
2542 * @uses \MainWP\Dashboard\MainWP_System_Utility::get_icons_dir()
2543 * @uses \MainWP\Dashboard\MainWP_Utility::remove_http_prefix()
2544 */
2545 public static function get_favico_url( $website ) {
2546 $favi = MainWP_DB::instance()->get_website_option( $website, 'favi_icon', '' );
2547 $faviurl = '';
2548
2549 if ( ! empty( $favi ) ) {
2550 if ( false !== strpos( $favi, 'favi-' . intval( $website->id ) . '-' ) ) {
2551 $dirs = MainWP_System_Utility::get_icons_dir();
2552 if ( file_exists( $dirs[0] . $favi ) ) {
2553 $faviurl = $dirs[1] . $favi;
2554 } else {
2555 $faviurl = '';
2556 }
2557 } elseif ( ( 0 === strpos( $favi, '//' ) ) || ( 0 === strpos( $favi, 'http' ) ) ) {
2558 $faviurl = $favi;
2559 } else {
2560 $faviurl = $website->url . $favi;
2561 $faviurl = MainWP_Utility::remove_http_prefix( $faviurl );
2562 }
2563 }
2564
2565 if ( empty( $faviurl ) ) {
2566 $faviurl = false;
2567 }
2568
2569 return $faviurl;
2570 }
2571 }
2572