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

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

2,186 lines 89.4 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 $found = false;
210 $dnsRecord = @dns_get_record( $host );
211 MainWP_Logger::instance()->debug( ' :: tryVisit :: [dnsRecord=' . MainWP_Utility::value_to_string( $dnsRecord, 1 ) . ']' );
212
213 if ( false !== $dnsRecord && is_array( $dnsRecord ) ) {
214 if ( ! isset( $dnsRecord['ip'] ) ) {
215 foreach ( $dnsRecord as $dnsRec ) {
216 if ( isset( $dnsRec['ip'] ) ) {
217 $ip = $dnsRec['ip'];
218 break;
219 }
220 }
221 } else {
222 $ip = $dnsRecord['ip'];
223 }
224
225 if ( ! isset( $dnsRecord['host'] ) ) {
226 foreach ( $dnsRecord as $dnsRec ) {
227 if ( $dnsRec['host'] === $host ) {
228 if ( 'CNAME' === $dnsRec['type'] ) {
229 $target = $dnsRec['target'];
230 }
231 $found = true;
232 break;
233 }
234 }
235 } else {
236 $found = ( $dnsRecord['host'] === $host );
237 if ( 'CNAME' === $dnsRecord['type'] ) {
238 $target = $dnsRecord['target'];
239 }
240 }
241 }
242
243 if ( false === $ip ) {
244 $ip = gethostbynamel( $host );
245 }
246 if ( ( false !== $target ) && ( $target !== $host ) ) {
247 $host .= ' (CNAME: ' . $target . ')';
248 }
249
250 $out = array(
251 'host' => $host,
252 'httpCode' => $http_status,
253 'httpCodeString' => MainWP_Utility::get_http_codes( $http_status ),
254 'httpVersion' => $http_version,
255 );
256
257 $hidden_data = '[hidden response data]';
258
259 if ( ( false === $ip || $ip === $host || ! static::validate_ip( $ip ) ) && apply_filters( 'mainwp_hide_raw_connection_response_data', true ) ) { // Failed to resolve hostname.
260 $data = $hidden_data;
261 }
262
263 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]' );
264
265 if ( false !== $ip ) {
266 $out['ip'] = $ip;
267 $found = true;
268 }
269
270 $out['error'] = ( '' === $err && false === $found ? 'Invalid host.' : $err );
271
272 return $out;
273 }
274
275
276 /**
277 * Method validate_ip().
278 *
279 * @param string $ip IP check.
280 * @return bool Check IP result.
281 */
282 public static function validate_ip( $ip ) {
283 // Validate the IP and check for private and reserved ranges.
284 if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
285 return true;
286 }
287 return false;
288 }
289
290 /**
291 * Method check_ignored_http_code()
292 *
293 * Check if http error code is being ignored.
294 *
295 * @param mixed $value http error code.
296 * @param object|false $website website.
297 *
298 * @return bolean True|False.
299 */
300 public static function check_ignored_http_code( $value, $website = false ) { // phpcs:ignore -- NOSONAR -complex method.
301 $value = (int) $value;
302 $site_id = is_object( $website ) && ! empty( $website->id ) ? $website->id : 0;
303 $global_settings = MainWP_Uptime_Monitoring_Handle::get_global_monitoring_settings();
304
305 $ignored_code = '';
306
307 if ( $site_id ) {
308
309 $primary_monitor = MainWP_DB_Uptime_Monitoring::instance()->get_monitor_by( $site_id, 'issub', 0 );
310 $global_settings = MainWP_Uptime_Monitoring_Handle::get_global_monitoring_settings();
311
312 $mo_active = 0;
313 if ( $primary_monitor ) {
314 $mo_active = MainWP_Uptime_Monitoring_Connect::get_apply_setting( 'active', (int) $primary_monitor->active, $global_settings, -1, 0 );
315 }
316
317 if ( $mo_active ) {
318 $ignored_code = MainWP_Uptime_Monitoring_Connect::instance()->get_up_codes( $primary_monitor, $global_settings );
319 } else {
320 $ignored_code = is_array( $global_settings ) && isset( $global_settings['up_status_codes'] ) ? $global_settings['up_status_codes'] : '';
321 }
322 } else {
323 $ignored_code = ! empty( $global_settings['up_status_codes'] ) ? $global_settings['up_status_codes'] : '';
324 }
325
326 if ( ! empty( $ignored_code ) ) {
327 $ignored_code = explode( ',', $ignored_code );
328 foreach ( $ignored_code as $code ) {
329 $code = trim( $code );
330 if ( (int) $value === (int) $code ) {
331 return true;
332 }
333 }
334 }
335 return false;
336 }
337
338 /**
339 * Method check website status.
340 *
341 * Check if the Website returns and http errors.
342 *
343 * @param object $website Child Site information.
344 * @param bool $chk_http_site Check site http response.
345 *
346 * @return mixed False|try visit result.
347 *
348 * @uses \MainWP\Dashboard\MainWP_Utility::is_domain_valid()
349 */
350 public static function check_website_status( $website, $chk_http_site = false ) { //phpcs:ignore -- NOSONAR - complexity.
351
352 if ( is_object( $website ) && isset( $website->id ) ) {
353 $primary_monitor = MainWP_DB_Uptime_Monitoring::instance()->get_monitor_by( $website->id, 'issub', 0 );
354 if ( $primary_monitor ) {
355 // return compatible uptime status here.
356 return MainWP_Uptime_Monitoring_Handle::check_website_uptime_monitoring_status(
357 $primary_monitor,
358 array(
359 'ignore_compatible_save' => 1,
360 'check_http_site' => $chk_http_site,
361 )
362 ); // to ignore save compatible uptime status.
363 }
364 }
365
366 $http_user = null;
367 $http_pass = null;
368 $sslVersion = null;
369 $verifyCertificate = null;
370 $forceUseIPv4 = null;
371 if ( is_object( $website ) && isset( $website->url ) ) {
372 $url = $website->url;
373 $verifyCertificate = isset( $website->verify_certificate ) ? (int) $website->verify_certificate : null;
374 $forceUseIPv4 = $website->force_use_ipv4;
375 // MWP-1548: decrypt at the boundary so HTTP Basic Auth gets
376 // the plaintext credentials. Legacy plaintext rows pass
377 // through unchanged via the helper's fallback.
378 $http_user = MainWP_Credential_Storage::decrypt_credential( $website->http_user );
379 $http_pass = MainWP_Credential_Storage::decrypt_credential( $website->http_pass );
380 $sslVersion = $website->ssl_version;
381 } else {
382 $url = $website;
383 }
384
385 if ( ! MainWP_Utility::is_domain_valid( $url ) ) {
386 return false;
387 }
388
389 $ssl_verifyhost = false;
390
391 if ( 1 === $verifyCertificate ) {
392 $ssl_verifyhost = true;
393 } elseif ( 2 === $verifyCertificate || null === $verifyCertificate ) {
394 if ( ( false === get_option( 'mainwp_sslVerifyCertificate' ) ) || ( 1 === get_option( 'mainwp_sslVerifyCertificate' ) ) ) {
395 $ssl_verifyhost = true;
396 }
397 }
398
399 $noBody = false;
400 return static::try_visit( $url, $ssl_verifyhost, $http_user, $http_pass, $sslVersion, $forceUseIPv4, $noBody );
401 }
402
403 /**
404 * Method get_post_data_authed()
405 *
406 * Get authorized $_POST data & build query.
407 *
408 * @param mixed $website Array of Child Site Info.
409 * @param mixed $what What we are posting.
410 * @param null $params Post parameters.
411 *
412 * @return mixed null|http_build_query()
413 */
414 public static function get_post_data_authed( &$website, $what, $params = null ) { //phpcs:ignore -- NOSONAR - complex method.
415 if ( $website && '' !== $what ) {
416 $data = array();
417 $data['user'] = $website->adminname;
418 $data['function'] = $what;
419 $data['nonce'] = wp_rand( 0, 9999 );
420 $data['mainwpver'] = MainWP_System::$version;
421
422 $params_filter = apply_filters( 'mainwp_pre_fetch_authed_data', false, $params, $what, $website );
423 if ( is_array( $params_filter ) && ! empty( $params_filter ) ) {
424 $data = array_merge( $data, $params_filter );
425 }
426
427 if ( null !== $params ) {
428 $data = array_merge( $data, $params );
429 }
430
431 $alg = false;
432 $sign_success = null;
433 $use_seclib = false;
434
435 $data = apply_filters( 'mainwp_get_post_data_authed', $data, $website, $what, $params );
436 if ( MainWP_Connect_Lib::is_use_fallback_sec_lib( $website ) ) {
437 $sign_success = MainWP_Connect_Lib::connect_sign( $what . $data['nonce'], $signature, base64_decode( $website->privkey ), $website->id ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
438 $use_seclib = true;
439 } elseif ( function_exists( 'openssl_verify' ) ) {
440 $alg = MainWP_System_Utility::get_connect_sign_algorithm( $website );
441 $sign_success = static::connect_sign( $what . $data['nonce'], $signature, base64_decode( $website->privkey ), $alg, $website->id ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
442 if ( false !== $alg ) {
443 $data['sign_algo'] = $alg;
444 }
445 }
446
447 if ( $use_seclib ) {
448 $data['verifylib'] = 1;
449 }
450
451 if ( null !== $sign_success && empty( $sign_success ) ) {
452 $sign_error = '';
453 while ( $msg = openssl_error_string() ) {
454 if ( is_string( $msg ) ) {
455 $sign_error .= $msg;
456 }
457 }
458 $pk_info = ! empty( $website->privkey ) ? substr( $website->privkey, 0, 10 ) : '';
459 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 );
460 }
461
462 $data['mainwpsignature'] = ! empty( $signature ) ? base64_encode( $signature ) : ''; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
463
464 /** This filter is documented in ../widgets/widget-mainwp-recent-posts.php */
465 $recent_number = apply_filters( 'mainwp_recent_posts_pages_number', 5 );
466 if ( 5 !== $recent_number ) {
467 $data['recent_number'] = $recent_number;
468 }
469
470 $scan_dir = apply_filters( 'mainwp_stats_scan_dir', false, $website );
471 if ( ! empty( $scan_dir ) ) {
472 $data['scan_dir'] = 1;
473 }
474
475 /**
476 * Current user global.
477 *
478 * @global string
479 */
480 global $current_user;
481
482 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 ) {
483 /**
484 * Filter: mainwp_alter_login_user
485 *
486 * Filters users accounts so it allows you user to jump to child site under alternative administrator account.
487 *
488 * @param int $website->id Child site ID.
489 * @param int $current_user->ID User ID.
490 *
491 * @since Unknown
492 */
493 $alter_user = apply_filters( 'mainwp_alter_login_user', false, $website->id, $current_user->ID );
494 if ( ! empty( $alter_user ) ) {
495 $data['alt_user'] = rawurlencode( $alter_user );
496 }
497 }
498
499 return http_build_query( $data, '', '&' );
500 }
501
502 return null;
503 }
504
505 /**
506 * Method get_renew_post_data_authed()
507 *
508 * Get authorized $_POST data & build query for renew connection action only.
509 *
510 * @param mixed $website Array of Child Site Info.
511 * @param mixed $what What we are posting.
512 *
513 * @return mixed null|http_build_query()
514 */
515 private static function get_renew_post_data_authed( &$website, $what ) { // phpcs:ignore -- NOSONAR - complex.
516
517 if ( $website && '' !== $what ) {
518 $compat_what = 'disconnect'; // to compatible, renew will call disconnect.
519 $data = array();
520 $data['user'] = $website->adminname;
521 $data['function'] = $compat_what;
522 $data['nonce'] = wp_rand( 0, 9999 );
523
524 $alg = false;
525 $sign_success = null;
526 $use_seclib = false;
527
528 if ( MainWP_Connect_Lib::is_use_fallback_sec_lib( $website ) ) {
529 // to disconnect.
530 $sign_success = MainWP_Connect_Lib::connect_sign( $compat_what . $data['nonce'], $signature, base64_decode( $website->privkey ), $website->id ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
531 $use_seclib = true;
532 } elseif ( function_exists( 'openssl_verify' ) ) {
533 $alg = MainWP_System_Utility::get_connect_sign_algorithm( $website );
534 $sign_success = static::connect_sign( $compat_what . $data['nonce'], $signature, base64_decode( $website->privkey ), $alg, $website->id ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for keys encoding.
535 if ( empty( $sign_success ) ) { // error from openssl, openssl_sign().
536 $alg = defined( 'OPENSSL_ALGO_SHA1' ) ? OPENSSL_ALGO_SHA1 : false; // to set default SHA1, to disconnect.
537 MainWP_Logger::instance()->debug_for_website( $website, 'get_renew_post_data_authed', '[' . $website->url . '] :: [openssl_sign:failed] :: Set sign_algo=SHA1' );
538 $sign_success = static::connect_sign( $compat_what . $data['nonce'], $signature, base64_decode( $website->privkey ), $alg, $website->id ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for keys encoding.
539 }
540
541 if ( false !== $alg ) {
542 $data['sign_algo'] = $alg;
543 }
544 }
545
546 if ( $use_seclib ) {
547 $data['verifylib'] = 1;
548 }
549
550 if ( null !== $sign_success && empty( $sign_success ) ) {
551 $sign_error = '';
552 while ( $msg = openssl_error_string() ) {
553 if ( is_string( $msg ) ) {
554 $sign_error .= $msg;
555 }
556 }
557 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 );
558 }
559
560 $data['mainwpsignature'] = ! empty( $signature ) ? base64_encode( $signature ) : ''; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
561
562 return http_build_query( $data, '', '&' );
563 }
564 return null;
565 }
566
567
568 /**
569 * Method get_get_data_authed()
570 *
571 * Get authorized $_GET data & build query.
572 *
573 * @param mixed $website Child Site data.
574 * @param mixed $paramValue OpenSSL parameter.
575 * @param string $paramName Parameter name.
576 * @param bool $asArray true|false Default is false.
577 * @param array $other_params other params.
578 *
579 * @return string $url
580 */
581 public static function get_get_data_authed( $website, $paramValue, $paramName = 'where', $asArray = false, $other_params = array() ) { //phpcs:ignore -- NOSONAR - complex method.
582 $params = array();
583 if ( $website && '' !== $paramValue ) {
584
585 $sign_success = null;
586 $alg = false;
587 $use_seclib = false;
588 $nonce = wp_rand( 0, 9999 );
589 if ( MainWP_Connect_Lib::is_use_fallback_sec_lib( $website ) ) {
590 $sign_success = MainWP_Connect_Lib::connect_sign( $paramValue . $nonce, $signature, base64_decode( $website->privkey ), $website->id ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
591 $use_seclib = true;
592 } elseif ( function_exists( 'openssl_verify' ) ) {
593 $alg = MainWP_System_Utility::get_connect_sign_algorithm( $website );
594 $sign_success = static::connect_sign( $paramValue . $nonce, $signature, base64_decode( $website->privkey ), $alg, $website->id ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
595 }
596
597 $signature = ! empty( $signature ) ? base64_encode( $signature ) : ''; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
598
599 if ( null !== $sign_success && empty( $sign_success ) ) {
600 $sign_error = '';
601 while ( $msg = openssl_error_string() ) {
602 if ( is_string( $msg ) ) {
603 $sign_error .= $msg;
604 }
605 }
606 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 );
607 }
608
609 $params = array(
610 'login_required' => 1,
611 'user' => rawurlencode( $website->adminname ),
612 'mainwpsignature' => rawurlencode( $signature ),
613 'nonce' => $nonce,
614 $paramName => rawurlencode( $paramValue ),
615 );
616
617 if ( is_array( $other_params ) ) {
618 foreach ( $other_params as $name => $value ) {
619 if ( is_string( $name ) && ! empty( $name ) && is_scalar( $value ) ) {
620 $params[ sanitize_text_field( wp_unslash( $name ) ) ] = rawurlencode( sanitize_text_field( wp_unslash( $value ) ) );
621 }
622 }
623 }
624
625 if ( ! empty( $params['login_required'] ) && ! empty( $params['where'] ) ) {
626 $open_params = apply_filters( 'mainwp_open_site_login_required_params', false, $params, $website );
627 if ( is_array( $open_params ) && ! empty( $open_params ) ) {
628 $where_params = '';
629 foreach ( $open_params as $key => $value ) {
630 $where_params .= rawurlencode( sanitize_text_field( wp_unslash( $key ) ) ) . '=' . rawurlencode( sanitize_text_field( wp_unslash( $value ) ) ) . '&';
631 }
632 if ( ! empty( $where_params ) ) {
633 $params['where_params'] = rawurlencode( rtrim( $where_params, '&' ) );
634 }
635 }
636 }
637
638 if ( false !== $alg ) {
639 $params['sign_algo'] = $alg;
640 }
641
642 if ( ! empty( $use_seclib ) ) {
643 $params['verifylib'] = 1;
644 }
645
646 /**
647 * Current user global.
648 *
649 * @global string
650 */
651 global $current_user;
652
653 if ( ( ( ! defined( 'DOING_CRON' ) || false === DOING_CRON ) && ( ! defined( 'WP_CLI' ) || false === WP_CLI ) ) && $current_user && $current_user->ID ) {
654 /** This filter is documented in ../class/class-mainwp-connect.php */
655 $alter_user = apply_filters( 'mainwp_alter_login_user', false, $website->id, $current_user->ID );
656 if ( ! empty( $alter_user ) ) {
657 $params['alt_user'] = rawurlencode( $alter_user );
658 }
659 }
660 }
661
662 if ( $asArray ) {
663 return $params;
664 }
665
666 $url = ( isset( $website->url ) && '' !== $website->url ? $website->url : $website->siteurl );
667 $url .= ( substr( $url, - 1 ) !== '/' ? '/' : '' );
668 $url .= '?';
669
670 foreach ( $params as $key => $value ) {
671 $url .= $key . '=' . $value . '&';
672 }
673 return rtrim( $url, '&' );
674 }
675
676 /**
677 * Method connect_sign()
678 *
679 * Sign connect.
680 *
681 * @param string $data Data sign.
682 * @param string $signature signature.
683 * @param string $privkey Private key.
684 * @param mixed $algorithm signature algorithm.
685 * @param int $site_id site id.
686 *
687 * @return bool Success or not.
688 */
689 public static function connect_sign( $data, &$signature, $privkey, $algorithm, $site_id ) {
690 $de_privkey = MainWP_Encrypt_Data_Lib::instance()->decrypt_privkey( $privkey, $site_id );
691
692 if ( empty( $de_privkey ) ) {
693 MainWP_Logger::instance()->debug( 'Error: Failed to decrypt the priv key.' );
694 }
695
696 if ( empty( $de_privkey ) ) {
697 $de_privkey = $privkey; // compatible.
698 }
699 if ( false === $algorithm ) {
700 return openssl_sign( $data, $signature, $de_privkey ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
701 } else {
702 return openssl_sign( $data, $signature, $de_privkey, $algorithm ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
703 }
704 }
705
706 /**
707 * Method get_post_data_not_authed()
708 *
709 * Get not authorized $_POST data.
710 *
711 * @param mixed $url Child site URL.
712 * @param mixed $admin Admin Username.
713 * @param mixed $what What function to perform.
714 * @param null $params Function parameters.
715 *
716 * @return mixed null|http_build_query()
717 */
718 public static function get_post_data_not_authed( $url, $admin, $what, $params = null ) {
719 if ( '' !== $url && '' !== $admin && '' !== $what ) {
720 $data = array();
721 $data['user'] = $admin;
722 $data['function'] = $what;
723 $data['mainwpver'] = MainWP_System::$version;
724
725 if ( null !== $params ) {
726 $data = array_merge( $data, $params );
727 }
728
729 return http_build_query( $data, '', '&' );
730 }
731
732 return null;
733 }
734
735 /**
736 * Method fetch_urls_authed()
737 *
738 * Fetches data from child sites if authenticated.
739 *
740 * @param object $websites Websites information.
741 * @param string $what Action to perform.
742 * @param array $params Request parameters.
743 * @param mixed $handler Request handler.
744 * @param mixed $output Request output.
745 * @param mixed $whatPage Request URL. Default /admin-ajax.php.
746 * @param array $others Request additional information.
747 *
748 * @return bool true|false
749 *
750 * @uses \MainWP\Dashboard\MainWP_System::$version
751 * @uses \MainWP\Dashboard\MainWP_System_Utility::get_mainwp_dir()
752 */
753 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.
754
755 if ( ! is_array( $websites ) || empty( $websites ) ) {
756 return false;
757 }
758
759 if ( ! is_array( $params ) ) {
760 $params = array();
761 }
762
763 $sleep_int = (int) get_option( 'mainwp_chunksleepinterval', 5 );
764 $chunkSize = (int) get_option( 'mainwp_chunksitesnumber', 10 );
765
766 $chunkSize = apply_filters( 'mainwp_fetch_urls_chunk_size', $chunkSize );
767 if ( count( $websites ) > $chunkSize ) {
768 $total = count( $websites );
769 $loops = ceil( $total / $chunkSize );
770 for ( $i = 0; $i < $loops; $i++ ) {
771 $newSites = array_slice( $websites, $i * $chunkSize, $chunkSize, true );
772 static::fetch_urls_authed( $newSites, $what, $params, $handler, $output, $whatPage, $others );
773 sleep( $sleep_int );
774 }
775
776 return false;
777 }
778
779 $agent = 'Mozilla/5.0 (compatible; MainWP/' . MainWP_System::$version . '; +http://mainwp.com)';
780 $mh = curl_multi_init();
781
782 $timeout = 20 * 60 * 60;
783
784 $disabled_functions = ini_get( 'disable_functions' );
785 $handleToWebsite = array();
786 $requestUrls = array();
787 $requestHandles = array();
788
789 $dirs = MainWP_System_Utility::get_mainwp_dir();
790 $cookieDir = $dirs[0] . 'cookies';
791
792 static::init_cookiesdir( $cookieDir );
793
794 $_org_params = null;
795
796 foreach ( $websites as $website ) {
797
798 if ( MainWP_Demo_Handle::get_instance()->is_demo_website( $website ) ) {
799 MainWP_Demo_Handle::get_instance()->handle_fetch_urls_demo( $data, $website, $output, $what, $params );
800 continue;
801 }
802
803 $url = $website->url;
804 if ( '/' !== substr( $url, - 1 ) ) {
805 $url .= '/';
806 }
807
808 if ( false === strpos( $url, 'wp-admin' ) ) {
809 $url .= 'wp-admin/';
810 }
811
812 if ( null !== $whatPage ) {
813 $url .= $whatPage;
814 } else {
815 $url .= 'admin-ajax.php';
816 }
817
818 $http_user = null;
819 $http_pass = null;
820
821 if ( property_exists( $website, 'http_user' ) ) {
822 $http_user = MainWP_Credential_Storage::decrypt_credential( $website->http_user );
823 }
824 if ( property_exists( $website, 'http_pass' ) ) {
825 $http_pass = MainWP_Credential_Storage::decrypt_credential( $website->http_pass );
826 }
827
828 if ( isset( $params ) && isset( $params['new_post'] ) ) {
829
830 if ( null === $_org_params ) {
831 $_org_params = $params;
832 }
833
834 /**
835 * Filter is being replaced with mainwp_pre_posting_posts.
836 *
837 * @deprecated
838 */
839 $params = apply_filters_deprecated(
840 'mainwp-pre-posting-posts',
841 array(
842 ( is_array( $params ) ? $params : array() ),
843 (object) array(
844 'id' => $website->id,
845 'url' => $website->url,
846 'name' => $website->name,
847 ),
848 ),
849 '4.0.7.2', // NOSONAR - not IP.
850 'mainwp_pre_posting_posts'
851 );
852
853 /**
854 * Filter: mainwp_pre_posting_posts
855 *
856 * Prepares parameters for the authenticated cURL post.
857 *
858 * @since 4.1
859 */
860 $params = apply_filters(
861 'mainwp_pre_posting_posts',
862 ( is_array( $params ) ? $params : array() ),
863 (object) array(
864 'id' => $website->id,
865 'url' => $website->url,
866 'name' => $website->name,
867 )
868 );
869 }
870
871 $ch = curl_init();
872
873 $proxy = new \WP_HTTP_Proxy();
874 if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) {
875 curl_setopt( $ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP );
876 curl_setopt( $ch, CURLOPT_PROXY, $proxy->host() );
877 curl_setopt( $ch, CURLOPT_PROXYPORT, $proxy->port() );
878
879 if ( $proxy->use_authentication() ) {
880 curl_setopt( $ch, CURLOPT_PROXYAUTH, CURLAUTH_ANY );
881 curl_setopt( $ch, CURLOPT_PROXYUSERPWD, $proxy->authentication() );
882 }
883 }
884
885 if ( ( null !== $website ) && ( ( property_exists( $website, 'wpe' ) && 1 !== $website->wpe ) || ( isset( $others['upgrade'] ) && ( true === $others['upgrade'] ) ) ) ) {
886 // to fix.
887 if ( defined( 'LOGGED_IN_SALT' ) && defined( 'NONCE_SALT' ) ) {
888 $cookie_salt = sha1( sha1( 'mainwp' . LOGGED_IN_SALT . $website->id ) . NONCE_SALT . 'WP_Cookie' ); // NOSONAR - safe for salt file name.
889 } else {
890 // MWP-1558: misconfigured WP installs (no salts) previously used unsalted SHA1, which is enumerable. Fall back to the per-install MainWP filename secret instead.
891 $cookie_salt = MainWP_System_Utility::get_private_filename( 'cookies', $website->id, 'WP_Cookie' );
892 }
893 $cookieFile = $cookieDir . '/' . $cookie_salt;
894 if ( ! file_exists( $cookieFile ) ) {
895 @file_put_contents( $cookieFile, '' );
896 }
897
898 if ( file_exists( $cookieFile ) ) {
899 @chmod( $cookieFile, 0644 ); // NOSONAR - correct file permissions, owner: rwe, group & others: r.
900 curl_setopt( $ch, CURLOPT_COOKIEJAR, $cookieFile );
901 curl_setopt( $ch, CURLOPT_COOKIEFILE, $cookieFile );
902 }
903 }
904
905 curl_setopt( $ch, CURLOPT_URL, $url );
906 curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
907 curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, true );
908 curl_setopt( $ch, CURLOPT_POST, true );
909
910 $postdata = static::get_post_data_authed( $website, $what, $params );
911 curl_setopt( $ch, CURLOPT_POSTFIELDS, $postdata );
912 curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, 10 );
913 curl_setopt( $ch, CURLOPT_USERAGENT, $agent );
914 curl_setopt( $ch, CURLOPT_REFERER, get_option( 'siteurl' ) );
915 curl_setopt( $ch, CURLOPT_ENCODING, 'none' );
916 if ( ! empty( $http_user ) && ! empty( $http_pass ) ) {
917 $http_pass = stripslashes( $http_pass );
918 curl_setopt( $ch, CURLOPT_USERPWD, "$http_user:$http_pass" );
919 }
920
921 $ssl_verifyhost = false;
922 $verifyCertificate = isset( $website->verify_certificate ) ? (int) $website->verify_certificate : null;
923 if ( null !== $verifyCertificate ) {
924 if ( 1 === $verifyCertificate ) {
925 $ssl_verifyhost = true;
926 } elseif ( 2 === $verifyCertificate ) {
927 if ( ( false === get_option( 'mainwp_sslVerifyCertificate' ) ) || ( 1 === (int) get_option( 'mainwp_sslVerifyCertificate' ) ) ) {
928 $ssl_verifyhost = true;
929 }
930 }
931 } elseif ( ( false === get_option( 'mainwp_sslVerifyCertificate' ) ) || ( 1 === (int) get_option( 'mainwp_sslVerifyCertificate' ) ) ) {
932 $ssl_verifyhost = true;
933 }
934
935 if ( $ssl_verifyhost ) {
936 curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, 2 );
937 curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true );
938 } else {
939 curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, false ); // NOSONAR.
940 curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false ); // NOSONAR.
941 }
942
943 curl_setopt( $ch, CURLOPT_SSLVERSION, $website->ssl_version );
944
945 if ( is_object( $website ) && property_exists( $website, 'id' ) ) {
946 $http_version = apply_filters( 'mainwp_curl_http_version', false, $website->id );
947 if ( false !== $http_version ) {
948 curl_setopt( $ch, CURLOPT_HTTP_VERSION, $http_version );
949 }
950
951 $curlopt_resolve = apply_filters( 'mainwp_curl_curlopt_resolve', false, $website->id, $website->url );
952 if ( is_array( $curlopt_resolve ) && ! empty( $curlopt_resolve ) ) {
953 curl_setopt( $ch, CURLOPT_RESOLVE, $curlopt_resolve );
954 curl_setopt( $ch, CURLOPT_DNS_USE_GLOBAL_CACHE, false );
955 }
956 }
957
958 curl_setopt( $ch, CURLOPT_TIMEOUT, $timeout );
959 MainWP_System_Utility::set_time_limit( $timeout );
960
961 if ( empty( $disabled_functions ) || ( false === stristr( $disabled_functions, 'curl_multi_exec' ) ) ) {
962 @curl_multi_add_handle( $mh, $ch );
963 }
964
965 $handleToWebsite[ static::get_resource_id( $ch ) ] = $website;
966 $requestUrls[ static::get_resource_id( $ch ) ] = $website->url;
967 $requestHandles[ static::get_resource_id( $ch ) ] = $ch;
968
969 if ( null !== $_org_params ) {
970 $params = $_org_params;
971 }
972 }
973
974 if ( empty( $disabled_functions ) || ( false === stristr( $disabled_functions, 'curl_multi_exec' ) ) ) {
975 $lastRun = 0;
976 do {
977 if ( 20 < time() - $lastRun ) {
978 MainWP_System_Utility::set_time_limit( $timeout );
979 $lastRun = time();
980 }
981
982 curl_multi_exec( $mh, $running );
983 curl_multi_select( $mh );
984 while ( $info = curl_multi_info_read( $mh ) ) {
985 $data = curl_multi_getcontent( $info['handle'] );
986 $contains = ( 0 < preg_match( '/<mainwp>(.*)<\/mainwp>/', $data, $results ) );
987 curl_multi_remove_handle( $mh, $info['handle'] );
988
989 $rid = static::get_resource_id( $info['handle'] );
990 if ( ! $contains && isset( $requestUrls[ $rid ] ) ) {
991 curl_setopt( $info['handle'], CURLOPT_URL, $requestUrls[ $rid ] );
992 curl_setopt( $info['handle'], CURLOPT_FRESH_CONNECT, true );
993 curl_setopt( $info['handle'], CURLOPT_FORBID_REUSE, true );
994 curl_multi_add_handle( $mh, $info['handle'] );
995 unset( $requestUrls[ $rid ] );
996 continue; // libcurl updates $running automatically.
997 }
998
999 if ( ! $contains ) {
1000 // Add useful debug log for unexpected response.
1001 $log_data = (string) $data;
1002 if ( is_string( $log_data ) && strlen( $log_data ) > 2000 ) {
1003 $log_data = substr( $log_data, 0, 2000 ) . '...[truncated]';
1004 }
1005 $log_url = isset( $requestUrls[ $rid ] ) ? $requestUrls[ $rid ] : '';
1006 if ( empty( $log_url ) ) {
1007 $log_url = ( isset( $handleToWebsite[ $rid ] ) && is_object( $handleToWebsite[ $rid ] ) && property_exists( $handleToWebsite[ $rid ], 'url' ) ) ? $handleToWebsite[ $rid ]->url : 'Unknown';
1008 }
1009 MainWP_Logger::instance()->debug( 'curl_multi_getcontent :: unexpected response :: [data=' . $log_data . '] :: [url=' . $log_url . ']' );
1010 }
1011
1012 if ( null !== $handler ) {
1013 $site = &$handleToWebsite[ static::get_resource_id( $info['handle'] ) ];
1014 call_user_func_array( $handler, array( $data, $site, &$output, $params ) );
1015 }
1016
1017 unset( $handleToWebsite[ static::get_resource_id( $info['handle'] ) ] );
1018 if ( static::is_valid_curl_handle( $info['handle'] ) ) {
1019 curl_close( $info['handle'] );
1020 }
1021 unset( $info['handle'] );
1022 }
1023 usleep( 10000 );
1024 } while ( $running > 0 );
1025
1026 if ( static::is_valid_curl_handle( $mh ) ) {
1027 curl_multi_close( $mh );
1028 }
1029 } else {
1030 foreach ( $requestHandles as $ch ) {
1031 $data = curl_exec( $ch );
1032
1033 if ( null !== $handler ) {
1034 $site = &$handleToWebsite[ static::get_resource_id( $ch ) ];
1035 call_user_func_array( $handler, array( $data, $site, &$output, $params ) );
1036 }
1037 }
1038 }
1039
1040 return true;
1041 }
1042
1043 /**
1044 * Credits WordPress org.
1045 *
1046 * Get the correct "Expect" header for the given request data.
1047 *
1048 * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD.
1049 * @return string The "Expect" header.
1050 */
1051 public static function get_expect_header( $data ) {
1052 if ( ! is_array( $data ) ) {
1053 return strlen( (string) $data ) >= 1048576 ? '100-Continue' : '';
1054 }
1055
1056 $bytesize = 0;
1057 $iterator = new \RecursiveIteratorIterator( new \RecursiveArrayIterator( $data ) );
1058
1059 foreach ( $iterator as $datum ) {
1060 $bytesize += strlen( (string) $datum );
1061
1062 if ( $bytesize >= 1048576 ) {
1063 return '100-Continue';
1064 }
1065 }
1066
1067 return '';
1068 }
1069
1070 /**
1071 * Method get_resource_id()
1072 *
1073 * Get resource id.
1074 *
1075 * @param mixed $res The given resource.
1076 *
1077 * @return $result Resource ID only.
1078 */
1079 public static function get_resource_id( $res ) {
1080 $result = false;
1081 if ( is_a( $res, 'CurlHandle' ) ) {
1082 $result = spl_object_hash( $res );
1083 } elseif ( is_resource( $res ) ) {
1084 $resourceString = (string) $res;
1085 $exploded = explode( '#', $resourceString );
1086 $result = array_pop( $exploded );
1087 }
1088 return $result;
1089 }
1090
1091 /**
1092 * Method get_lock_identifier().
1093 *
1094 * Get lock identifier.
1095 *
1096 * @param mixed $pLockName Provided Lock Name.
1097 *
1098 * @return mixed false|sem_get()|@fopen
1099 */
1100 public static function get_lock_identifier( $pLockName ) {
1101 if ( ( null === $pLockName ) || ( false === $pLockName ) ) {
1102 return false;
1103 }
1104
1105 if ( function_exists( 'sem_get' ) ) {
1106 return sem_get( $pLockName );
1107 } else {
1108 $fh = @fopen( sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'lock' . $pLockName . '.txt', 'w+' );
1109 if ( ! $fh ) {
1110 return false;
1111 }
1112
1113 return $fh;
1114 }
1115 }
1116
1117 /**
1118 * Method lock()
1119 *
1120 * Use sem_acquire or @flock to lock the $identifier.
1121 *
1122 * @param mixed $identifier Identifier.
1123 *
1124 * @return mixed false|sem_acquire()|@flock
1125 */
1126 public static function lock( $identifier ) {
1127 if ( ( null === $identifier ) || ( false === $identifier ) ) {
1128 return false;
1129 }
1130
1131 if ( function_exists( 'sem_acquire' ) ) {
1132 return sem_acquire( $identifier );
1133 } else {
1134 if ( ! is_resource( $identifier ) ) {
1135 return false; // to fix.
1136 }
1137 for ( $i = 0; $i < 3; $i++ ) {
1138 if ( @flock( $identifier, LOCK_EX ) ) {
1139 return $identifier;
1140 } else {
1141 sleep( 1 );
1142 }
1143 }
1144 return false;
1145 }
1146 }
1147
1148 /**
1149 * Method release()
1150 *
1151 * Use sem_release or @flock, @fclose to unlock $identifier.
1152 *
1153 * @param mixed $identifier Identifier.
1154 *
1155 * @return mixed false|sem_release()|@flock
1156 */
1157 public static function release( $identifier ) {
1158 if ( ( null === $identifier ) || ( false === $identifier ) ) {
1159 return false;
1160 }
1161
1162 if ( function_exists( 'sem_release' ) ) {
1163 return sem_release( $identifier );
1164 } else {
1165 if ( ! is_resource( $identifier ) ) {
1166 return false; // to fix.
1167 }
1168 @flock( $identifier, LOCK_UN );
1169 @fclose( $identifier );
1170 }
1171
1172 return false;
1173 }
1174
1175 /**
1176 * Method fetch_url_authed()
1177 *
1178 * Updates the child site via authenticated request.
1179 *
1180 * @param object $website Website information.
1181 * @param string $what Function to perform.
1182 * @param null $params Function parameters.
1183 * @param bool $checkConstraints Whether or not to check constraints.
1184 * @param bool $pForceFetch Whether or not to force the fetch.
1185 * @param bool $pRetryFailed Whether or not to retry the fetch process.
1186 * @param null $rawResponse Raw response.
1187 *
1188 * @return mixed $information
1189 *
1190 * @uses \MainWP\Dashboard\MainWP_Monitoring_Handler::handle_check_website()
1191 * @uses \MainWP\Dashboard\MainWP_Premium_Update::maybe_request_premium_updates()
1192 * @uses \MainWP\Dashboard\MainWP_Sync::sync_information_array()
1193 */
1194 public static function fetch_url_authed( // phpcs:ignore -- NOSONAR - complex.
1195 &$website,
1196 $what,
1197 $params = null,
1198 $checkConstraints = false,
1199 $pForceFetch = false,
1200 $pRetryFailed = true,
1201 $rawResponse = null
1202 ) {
1203 unset( $pForceFetch );
1204
1205 // to support demo data.
1206 if ( MainWP_Demo_Handle::get_instance()->is_demo_website( $website ) ) {
1207 return MainWP_Demo_Handle::get_instance()->handle_action_demo( $website, $what );
1208 }
1209
1210 /**
1211 * Filter to mock fetch_url_authed response before any HTTP/signing occurs.
1212 *
1213 * This filter fires early, before OpenSSL signing or HTTP requests, allowing
1214 * tests to bypass child site communication entirely.
1215 *
1216 * SECURITY WARNING - TEST ONLY:
1217 * This filter ONLY fires when ALL of the following conditions are met:
1218 * 1. MAINWP_TESTING_MODE constant is defined and true
1219 * 2. A PHPUnit test harness constant is present (WP_TESTS_DOMAIN, PHPUNIT_COMPOSER_INSTALL, or WP_TESTS_DIR)
1220 *
1221 * This triple-check prevents malicious code from defining MAINWP_TESTING_MODE
1222 * in production to spoof child site responses.
1223 *
1224 * IMPORTANT: MAINWP_TESTING_MODE must ONLY be defined in the PHPUnit bootstrap
1225 * file (tests/bootstrap.php). Defining it in production code, wp-config.php, or
1226 * plugin files would create a security vulnerability allowing response spoofing.
1227 *
1228 * @since 5.4
1229 *
1230 * @param mixed $pre_result Return non-false to short-circuit and return this value.
1231 * @param object $website Website object being communicated with.
1232 * @param string $what Action being performed (e.g., 'plugin_action').
1233 * @param array $params Request parameters.
1234 * @return mixed Array to return early, false to proceed normally.
1235 */
1236 $is_phpunit_env = defined( 'WP_TESTS_DOMAIN' ) || defined( 'PHPUNIT_COMPOSER_INSTALL' ) || ( defined( 'WP_TESTS_DIR' ) && WP_TESTS_DIR );
1237 if ( defined( 'MAINWP_TESTING_MODE' ) && MAINWP_TESTING_MODE && $is_phpunit_env ) {
1238 $pre_result = apply_filters( 'mainwp_fetch_url_authed_pre', false, $website, $what, $params );
1239 if ( false !== $pre_result ) {
1240 return $pre_result;
1241 }
1242 }
1243
1244 if ( ! is_array( $params ) ) {
1245 $params = array();
1246 }
1247
1248 $others = array(
1249 'force_use_ipv4' => $website->force_use_ipv4,
1250 'upgrade' => ( 'upgradeplugintheme' === $what || 'upgrade' === $what || 'upgradetranslation' === $what ),
1251 );
1252
1253 $request_update = MainWP_Premium_Update::maybe_request_premium_updates( $website, $what, $params );
1254
1255 if ( isset( $rawResponse ) && $rawResponse ) {
1256 $others['raw_response'] = 'yes';
1257 }
1258
1259 $params['optimize'] = ( ( 1 === (int) get_option( 'mainwp_optimize', 1 ) ) ? 1 : 0 );
1260
1261 $updating_website = false;
1262 $type = '';
1263 $list = '';
1264 if ( 'upgradeplugintheme' === $what || 'upgrade' === $what || 'upgradetranslation' === $what ) {
1265 $updating_website = true;
1266 if ( 'upgradeplugintheme' === $what || 'upgradetranslation' === $what ) {
1267 $type = $params['type'];
1268 $list = $params['list'];
1269 } else {
1270 $type = 'wp';
1271 $list = '';
1272 }
1273 }
1274
1275 if ( $updating_website ) {
1276 /**
1277 * Action: mainwp_website_before_updated
1278 *
1279 * Fires before the child site update process.
1280 *
1281 * @param object $website Object containing child site info.
1282 * @param string $type Type parameter.
1283 * @param string $list List parameter.
1284 *
1285 * @since Unknown
1286 */
1287 do_action( 'mainwp_website_before_updated', $website, $type, $list );
1288 }
1289
1290 if ( 'renew' === $what ) {
1291 $postdata = static::get_renew_post_data_authed( $website, $what );
1292 } else {
1293 $postdata = static::get_post_data_authed( $website, $what, $params );
1294
1295 }
1296 $others['function'] = $what;
1297
1298 $information = array();
1299 $output = array();
1300
1301 if ( ! $request_update ) {
1302 // MWP-1548: decrypt http_user / http_pass before they hit the
1303 // outbound HTTP Basic Auth header. Legacy plaintext rows pass
1304 // through unchanged.
1305 $http_user_plain = MainWP_Credential_Storage::decrypt_credential( $website->http_user );
1306 $http_pass_plain = MainWP_Credential_Storage::decrypt_credential( $website->http_pass );
1307 $information = static::fetch_url( $website, $website->url, $postdata, $checkConstraints, $website->verify_certificate, $pRetryFailed, $http_user_plain, $http_pass_plain, $website->ssl_version, $others, $output );
1308 if ( ! empty( $output ) ) {
1309 if ( ! is_array( $information ) ) {
1310 $information = array();
1311 }
1312 $information['fetch_url_output'] = $output;
1313 }
1314 /**
1315 * Fires immediately after fetch url action.
1316 *
1317 * @param object $website website.
1318 * @param array $information information result data.
1319 * @param string $what action.
1320 * @param array $params params input array.
1321 * @param array $others others input array.
1322 *
1323 * @since 4.5.1.1
1324 */
1325 do_action( 'mainwp_fetch_url_authed', $website, $information, $what, $params, $others );
1326 } else {
1327 $slug = $params['list'];
1328 $information['upgrades'] = array( $slug => 1 );
1329 }
1330
1331 if ( is_array( $information ) && isset( $information['sync'] ) && ! empty( $information['sync'] ) ) {
1332 MainWP_Sync::sync_information_array( $website, $information['sync'] );
1333 unset( $information['sync'] );
1334 }
1335
1336 if ( $updating_website ) {
1337 /**
1338 * Action: mainwp_website_updated
1339 *
1340 * Fires after the child site update process.
1341 *
1342 * @param object $website Object containing child site info.
1343 * @param string $type Type parameter.
1344 * @param string $list List parameter.
1345 * @param array $information Array containing the information fetched from the child site.
1346 *
1347 * @since Unknown
1348 */
1349 do_action( 'mainwp_website_updated', $website, $type, $list, $information );
1350 if ( 1 === (int) get_option( 'mainwp_check_http_response', 0 ) ) {
1351 MainWP_Monitoring_Handler::handle_check_website( $website, true );
1352 }
1353 }
1354
1355 return $information;
1356 }
1357
1358 /**
1359 * Method fetch_url_not_authed()
1360 *
1361 * Fetch not authorized URL.
1362 *
1363 * @param string $url URL to fetch from.
1364 * @param string $admin Admin name.
1365 * @param string $what Function to perform.
1366 * @param null $params Function parameters.
1367 * @param bool $pForceFetch true|false Whether or not to force the fetch.
1368 * @param null $verifyCertificate Verify the SSL Certificate.
1369 * @param null $http_user htaccess username.
1370 * @param null $http_pass htaccess password.
1371 * @param integer $sslVersion SSL version to check for.
1372 * @param array $others Other functions to perform.
1373 * @param array $output Output values.
1374 *
1375 * @return mixed static::fetch_url() Fetch URL.
1376 */
1377 public static function fetch_url_not_authed( // phpcs:ignore -- NOSONAR - compatible.
1378 $url,
1379 $admin,
1380 $what,
1381 $params = null,
1382 $pForceFetch = false,
1383 $verifyCertificate = null,
1384 $http_user = null,
1385 $http_pass = null,
1386 $sslVersion = 0,
1387 $others = array(),
1388 &$output = array()
1389 ) {
1390 unset( $pForceFetch );
1391
1392 if ( empty( $params ) ) {
1393 $params = array();
1394 }
1395
1396 $postdata = static::get_post_data_not_authed( $url, $admin, $what, $params );
1397 $website = null;
1398
1399 $others['function'] = $what;
1400 return static::fetch_url( $website, $url, $postdata, false, $verifyCertificate, true, $http_user, $http_pass, $sslVersion, $others, $output );
1401 }
1402
1403 /**
1404 * Method fetch_url()
1405 *
1406 * Fetch URL.
1407 *
1408 * @param object $website Child Site info.
1409 * @param string $url URL to fetch from.
1410 * @param mixed $postdata Post data to fetch.
1411 * @param bool $checkConstraints true|false Whether or not to check constraints.
1412 * @param null $verifyCertificate Verify SSL Certificate.
1413 * @param bool $pRetryFailed ture|false Whether or not the Retry has failed.
1414 * @param null $http_user htaccess username.
1415 * @param null $http_pass htaccess password.
1416 * @param integer $sslVersion SSL version.
1417 * @param array $others Other functions to perform.
1418 * @param array $output Output values.
1419 *
1420 * @throws \Exception Exception message.
1421 *
1422 * @return mixed static::fetch_url_site()
1423 */
1424 public static function fetch_url( // phpcs:ignore -- NOSONAR - complex.
1425 &$website,
1426 $url,
1427 $postdata,
1428 $checkConstraints = false,
1429 $verifyCertificate = null,
1430 $pRetryFailed = true,
1431 $http_user = null,
1432 $http_pass = null,
1433 $sslVersion = 0,
1434 $others = array(),
1435 &$output = array()
1436 ) {
1437
1438 $start = time();
1439
1440 try {
1441 $tmpUrl = $url;
1442 if ( '/' !== substr( $tmpUrl, - 1 ) ) {
1443 $tmpUrl .= '/';
1444 }
1445
1446 if ( false === strpos( $url, 'wp-admin' ) ) {
1447 $tmpUrl .= 'wp-admin/admin-ajax.php';
1448 }
1449
1450 return static::fetch_url_site( $website, $tmpUrl, $postdata, $checkConstraints, $verifyCertificate, $http_user, $http_pass, $sslVersion, $others, $output );
1451 } catch ( \Exception $e ) {
1452 if ( ! $pRetryFailed || ( 30 < ( time() - $start ) ) ) {
1453 throw $e;
1454 }
1455
1456 try {
1457 return static::fetch_url_site( $website, $url, $postdata, $checkConstraints, $verifyCertificate, $http_user, $http_pass, $sslVersion, $others, $output );
1458 } catch ( \Exception $ex ) {
1459 throw $e;
1460 }
1461 }
1462 }
1463
1464 /**
1465 * Method fetch_url_site()
1466 *
1467 * M Fetch URL.
1468 *
1469 * @param object $website Child Site info.
1470 * @param string $url URL to fetch from.
1471 * @param mixed $postdata Post data to fetch.
1472 * @param bool $checkConstraints true|false Whether or not to check constraints.
1473 * @param null $verifyCertificate Verify SSL Certificate.
1474 * @param null $http_user htaccess username.
1475 * @param null $http_pass htaccess password.
1476 * @param integer $sslVersion SSL version.
1477 * @param array $others Other functions to perform.
1478 * @param array $output Output values.
1479 *
1480 * @return mixed $data, $information.
1481 * @throws MainWP_Exception Exception message.
1482 *
1483 * @uses \MainWP\Dashboard\MainWP_DB_Common::insert_or_update_request_log()
1484 * @uses \MainWP\Dashboard\MainWP_Exception
1485 * @uses \MainWP\Dashboard\MainWP_Logger::debug_for_website()
1486 * @uses \MainWP\Dashboard\MainWP_System::$version
1487 * @uses \MainWP\Dashboard\MainWP_System_Utility::get_mainwp_dir()
1488 * @uses \MainWP\Dashboard\MainWP_System_Utility::get_child_response()
1489 * @uses \MainWP\Dashboard\MainWP_Utility::value_to_string()
1490 * @uses \MainWP\Dashboard\MainWP_Utility::end_session()
1491 */
1492 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.
1493 &$website,
1494 $url,
1495 $postdata,
1496 $checkConstraints = false,
1497 $verifyCertificate = null,
1498 $http_user = null,
1499 $http_pass = null,
1500 $sslVersion = 0,
1501 $others = array(),
1502 &$output = array()
1503 ) {
1504
1505 /**
1506 * Enables data to be returned prior to connecting to the site.
1507 *
1508 * Dev/test override only. Gated behind the MAINWP_DEV_FILTERS_ENABLED
1509 * constant so the filter does not dispatch in production. The filter
1510 * receives plaintext HTTP Basic Auth credentials and the full $website
1511 * DB row (including privkey); enabling it in production would expose
1512 * those values to any 3rd-party plugin hooking the filter.
1513 *
1514 * To enable in a dev/test environment, add to wp-config.php:
1515 * define( 'MAINWP_DEV_FILTERS_ENABLED', true );
1516 *
1517 * @since 5.5
1518 *
1519 * @param mixed false
1520 * @param mixed $website
1521 * @param mixed $url
1522 * @param mixed $postdata
1523 * @param mixed $checkConstraints
1524 * @param mixed $verifyCertificate
1525 * @param mixed $http_user
1526 * @param mixed $http_pass
1527 * @param mixed $sslVersion
1528 * @param mixed $others
1529 * @param mixed $output
1530 */
1531 if ( defined( 'MAINWP_DEV_FILTERS_ENABLED' ) && MAINWP_DEV_FILTERS_ENABLED ) {
1532 $dev_data = apply_filters( 'mainwp_dev_return_data_before_connect_site', false, $website, $url, $postdata, $checkConstraints, $verifyCertificate, $http_user, $http_pass, $sslVersion, $others, $output );
1533 if ( false !== $dev_data ) {
1534 return $dev_data;
1535 }
1536 }
1537
1538 $agent = 'Mozilla/5.0 (compatible; MainWP/' . MainWP_System::$version . '; +http://mainwp.com)';
1539
1540 if ( ! empty( $website ) ) {
1541 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url_site', 'Request to [' . $url . '] [' . MainWP_Utility::value_to_string( $postdata, 1 ) . ']' );
1542 }
1543
1544 $identifier = null;
1545 if ( $checkConstraints ) {
1546 static::check_constraints( $identifier, $website );
1547 }
1548
1549 if ( null !== $website ) {
1550 MainWP_DB_Common::instance()->insert_or_update_request_log( $website->id, null, microtime( true ), null );
1551 }
1552
1553 if ( null !== $identifier ) {
1554 static::release( $identifier );
1555 }
1556
1557 $dirs = MainWP_System_Utility::get_mainwp_dir();
1558 $cookieDir = $dirs[0] . 'cookies';
1559
1560 static::init_cookiesdir( $cookieDir );
1561
1562 $fetch_track_id = MainWP_Execution_Helper::execute_call_track( 'start_point', $website, $postdata );
1563
1564 $ch = curl_init();
1565
1566 $proxy = new \WP_HTTP_Proxy();
1567 if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) {
1568 curl_setopt( $ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP );
1569 curl_setopt( $ch, CURLOPT_PROXY, $proxy->host() );
1570 curl_setopt( $ch, CURLOPT_PROXYPORT, $proxy->port() );
1571
1572 if ( $proxy->use_authentication() ) {
1573 curl_setopt( $ch, CURLOPT_PROXYAUTH, CURLAUTH_ANY );
1574 curl_setopt( $ch, CURLOPT_PROXYUSERPWD, $proxy->authentication() );
1575 }
1576 }
1577
1578 if ( ( null !== $website ) && ( ( property_exists( $website, 'wpe' ) && 1 !== $website->wpe ) || ( isset( $others['upgrade'] ) && ( true === $others['upgrade'] ) ) ) ) {
1579 // to fix.
1580 if ( defined( 'LOGGED_IN_SALT' ) && defined( 'NONCE_SALT' ) ) {
1581 $cookie_salt = sha1( sha1( 'mainwp' . LOGGED_IN_SALT . $website->id ) . NONCE_SALT . 'WP_Cookie' ); // NOSONAR - safe for salt file name.
1582 } else {
1583 // MWP-1558: misconfigured WP installs (no salts) previously used unsalted SHA1, which is enumerable. Fall back to the per-install MainWP filename secret instead.
1584 $cookie_salt = MainWP_System_Utility::get_private_filename( 'cookies', $website->id, 'WP_Cookie' );
1585 }
1586 $cookieFile = $cookieDir . '/' . $cookie_salt;
1587 if ( ! file_exists( $cookieFile ) ) {
1588 @file_put_contents( $cookieFile, '' );
1589 }
1590
1591 if ( file_exists( $cookieFile ) ) {
1592 @chmod( $cookieFile, 0644 ); // NOSONAR - correct file permissions, owner: rwe, group & others: r.
1593 curl_setopt( $ch, CURLOPT_COOKIEJAR, $cookieFile );
1594 curl_setopt( $ch, CURLOPT_COOKIEFILE, $cookieFile );
1595 }
1596 }
1597
1598 curl_setopt( $ch, CURLOPT_URL, $url );
1599 curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
1600 curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, true );
1601 curl_setopt( $ch, CURLOPT_POST, true );
1602 curl_setopt( $ch, CURLOPT_POSTFIELDS, $postdata );
1603 curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, 10 );
1604 curl_setopt( $ch, CURLOPT_USERAGENT, $agent );
1605 curl_setopt( $ch, CURLOPT_ENCODING, 'none' );
1606
1607 if ( ! empty( $http_user ) && ! empty( $http_pass ) ) {
1608 $http_pass = stripslashes( $http_pass );
1609 curl_setopt( $ch, CURLOPT_USERPWD, "$http_user:$http_pass" );
1610 }
1611
1612 $ssl_verifyhost = false;
1613 if ( null !== $verifyCertificate ) {
1614 if ( 1 === (int) $verifyCertificate ) {
1615 $ssl_verifyhost = true;
1616 } elseif ( 2 === (int) $verifyCertificate ) {
1617 if ( ( false === get_option( 'mainwp_sslVerifyCertificate' ) ) || ( 1 === (int) get_option( 'mainwp_sslVerifyCertificate' ) ) ) {
1618 $ssl_verifyhost = true;
1619 }
1620 }
1621 } elseif ( ( false === get_option( 'mainwp_sslVerifyCertificate' ) ) || ( 1 === (int) get_option( 'mainwp_sslVerifyCertificate' ) ) ) {
1622 $ssl_verifyhost = true;
1623 }
1624
1625 if ( $ssl_verifyhost ) {
1626 curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, 2 );
1627 curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true );
1628 } else {
1629 curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, false ); // NOSONAR.
1630 curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false ); // NOSONAR.
1631 }
1632
1633 curl_setopt( $ch, CURLOPT_SSLVERSION, $sslVersion );
1634
1635 $http_version = apply_filters( 'mainwp_curl_http_version', false, $website ? $website->id : false, $url );
1636 if ( false !== $http_version ) {
1637 curl_setopt( $ch, CURLOPT_HTTP_VERSION, $http_version );
1638 }
1639
1640 $curlopt_resolve = false;
1641
1642 if ( is_object( $website ) && property_exists( $website, 'id' ) ) {
1643 $curlopt_resolve = apply_filters( 'mainwp_curl_curlopt_resolve', false, $website->id, $website->url );
1644 }
1645
1646 if ( is_array( $curlopt_resolve ) && ! empty( $curlopt_resolve ) ) {
1647 curl_setopt( $ch, CURLOPT_RESOLVE, $curlopt_resolve );
1648 curl_setopt( $ch, CURLOPT_DNS_USE_GLOBAL_CACHE, false );
1649 }
1650
1651 $headers = array( 'X-Requested-With' => 'XMLHttpRequest' );
1652 $headers['Expect'] = static::get_expect_header( $postdata );
1653
1654 $headers = apply_filters( 'mainwp_connect_http_request_headers', $headers, $website );
1655
1656 if ( class_exists( '\WpOrg\Requests\Requests' ) ) {
1657 $headers = \WpOrg\Requests\Requests::flatten( $headers );
1658 } else {
1659 $headers = \Requests::flatten( $headers );
1660 }
1661
1662 curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
1663 curl_setopt( $ch, CURLOPT_REFERER, get_option( 'siteurl' ) );
1664
1665 $force_use_ipv4 = false;
1666 $forceUseIPv4 = isset( $others['force_use_ipv4'] ) ? (int) $others['force_use_ipv4'] : null;
1667 if ( null !== $forceUseIPv4 ) {
1668 if ( 1 === $forceUseIPv4 ) {
1669 $force_use_ipv4 = true;
1670 } elseif ( 2 === $forceUseIPv4 ) {
1671 if ( 1 === (int) get_option( 'mainwp_forceUseIPv4' ) ) {
1672 $force_use_ipv4 = true;
1673 }
1674 }
1675 } elseif ( 1 === (int) get_option( 'mainwp_forceUseIPv4' ) ) {
1676 $force_use_ipv4 = true;
1677 }
1678
1679 if ( $force_use_ipv4 && defined( 'CURLOPT_IPRESOLVE' ) && defined( 'CURL_IPRESOLVE_V4' ) ) {
1680 curl_setopt( $ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4 );
1681 }
1682
1683 $timeout = 20 * 60 * 60;
1684 curl_setopt( $ch, CURLOPT_TIMEOUT, $timeout );
1685 MainWP_System_Utility::set_time_limit( $timeout );
1686
1687 MainWP_Utility::end_session();
1688
1689 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url_site', 'Executing handlers' );
1690
1691 $disabled_functions = ini_get( 'disable_functions' );
1692 if ( empty( $disabled_functions ) || ( false === stristr( $disabled_functions, 'curl_multi_exec' ) ) ) {
1693 $mh = @curl_multi_init();
1694 @curl_multi_add_handle( $mh, $ch );
1695
1696 $lastRun = 0;
1697 $running = null;
1698
1699 do {
1700 if ( 20 < time() - $lastRun ) {
1701 MainWP_System_Utility::set_time_limit( $timeout );
1702 $lastRun = time();
1703 }
1704
1705 do {
1706 $mrc = curl_multi_exec( $mh, $running );
1707 } while ( CURLM_CALL_MULTI_PERFORM === $mrc );
1708
1709 $rc = curl_multi_select( $mh, 1.0 );
1710 if ( -1 === $rc ) {
1711 usleep( 100000 );
1712 }
1713
1714 while ( $info = @curl_multi_info_read( $mh ) ) {
1715 $data = @curl_multi_getcontent( $info['handle'] );
1716 $http_status = @curl_getinfo( $info['handle'], CURLINFO_HTTP_CODE );
1717 $err = @curl_error( $info['handle'] );
1718 $real_url = @curl_getinfo( $info['handle'], CURLINFO_EFFECTIVE_URL );
1719
1720 @curl_multi_remove_handle( $mh, $info['handle'] );
1721 curl_close( $info['handle'] );
1722 }
1723 usleep( 10000 );
1724 } while ( $running > 0 );
1725
1726 if ( static::is_valid_curl_handle( $mh ) ) {
1727 @curl_multi_close( $mh );
1728 }
1729 } else {
1730 $data = @curl_exec( $ch );
1731 $http_status = @curl_getinfo( $ch, CURLINFO_HTTP_CODE );
1732 $err = @curl_error( $ch );
1733 $real_url = @curl_getinfo( $ch, CURLINFO_EFFECTIVE_URL );
1734 curl_close( $ch );
1735 }
1736
1737 $host = wp_parse_url( $real_url, PHP_URL_HOST );
1738 $ip = gethostbyname( $host );
1739
1740 MainWP_Execution_Helper::execute_call_track( 'end_point', $website, $postdata, $fetch_track_id, 'fetch site' );
1741
1742 if ( null !== $website ) {
1743 MainWP_DB_Common::instance()->insert_or_update_request_log( $website->id, $ip, null, microtime( true ) );
1744 }
1745
1746 $raw_response = isset( $others['raw_response'] ) && 'yes' === $others['raw_response'] ? true : false;
1747
1748 $hidden_data = '[hidden response data]';
1749
1750 if ( ! apply_filters( 'mainwp_hide_raw_connection_response_data', true ) ) {
1751 $hidden_data = $data;
1752 }
1753
1754 if ( ! is_array( $output ) ) {
1755 $output = array();
1756 }
1757
1758 $output['fetch_data'] = $hidden_data;
1759
1760 $output['http_status'] = (int) $http_status;
1761
1762 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url_site', 'http status: [' . $http_status . '] err: [' . $err . ']' );
1763 if ( '400' === $http_status ) {
1764 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url_site', 'post data: [' . MainWP_Utility::value_to_string( $postdata, 1 ) . ']' );
1765 }
1766
1767 MainWP_Logger::instance()->log_execution_time( 'fetch_url_site :: [url=' . $url . ']' );
1768
1769 $thr_error = null;
1770
1771 if ( isset( $others['function'] ) ) {
1772 $what = $others['function'];
1773 if ( in_array( $what, array( 'installplugintheme', 'upgradeplugintheme', 'upgradetranslation', 'upgrade', 'stats', 'renew', 'reconnect' ), true ) ) {
1774 MainWP_Cache_Helper::invalidate_cache_group( MainWP_Cache_Helper::CGR_UPDATES );
1775 MainWP_Cache_Warm_Helper::invalidate_pages_by_site_actions( $what );
1776 }
1777 }
1778
1779 if ( ( false === $data ) && empty( $http_status ) ) {
1780 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url', '[' . $url . '] HTTP Error: [status=0][' . $err . ']' );
1781 $thr_error = new MainWP_Exception( 'HTTPERROR', $err ); //phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
1782 $output['error_category'] = 'http_error';
1783 } elseif ( empty( $data ) && ! empty( $err ) ) {
1784 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url', '[' . $url . '] HTTP Error: [status=' . $http_status . '][' . $err . ']' );
1785 $thr_error = new MainWP_Exception( 'HTTPERROR', $err ); //phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
1786 $output['error_category'] = 'http_error';
1787 $output['error_code'] = 'http_request_failed';
1788 $output['error_message'] = $err;
1789 } elseif ( 0 < preg_match( '/<mainwp>(.*)<\/mainwp>/', $data, $results ) ) {
1790 $output['connection_step'] = 'verify_credentials';
1791 $result = $results[1];
1792 $information = MainWP_System_Utility::get_child_response( base64_decode( $result ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
1793 unset( $output['fetch_data'] ); // hide the data.
1794 $pdt = is_string( $postdata ) ? $postdata : '';
1795 $data_log = is_array( $postdata ) ? print_r( $postdata, true ) : $pdt; //phpcs:ignore -- good.
1796 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url_site', '[' . $url . '] postdata [' . $data_log . '] information: [OK]' ); //phpcs:ignore -- ok.
1797
1798 $error_code = is_array( $information ) && isset( $information['error_code'] ) ? sanitize_text_field( wp_unslash( $information['error_code'] ) ) : '';
1799 if ( ! empty( $error_code ) ) {
1800 $output['child_error_code'] = $error_code;
1801 }
1802 return $information;
1803 } elseif ( 200 === (int) $http_status && ! empty( $err ) ) {
1804 $thr_error = new MainWP_Exception( 'HTTPERROR', $err ); //phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
1805 $output['error_category'] = 'http_error';
1806 $output['error_code'] = 'http_request_failed';
1807 $output['error_message'] = $err;
1808 } elseif ( $raw_response ) {
1809 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url_site', 'Response: [RAW]' );
1810 return $data;
1811 } else {
1812 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url', '[' . $url . '] Error: NOMAINWP [data=' . $hidden_data . ']' );
1813 $detect_wsidchk = is_string( $data ) ? strpos( $data, 'wsidchk' ) : false;
1814 if ( false !== $detect_wsidchk ) {
1815 $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.';
1816 $thr_error = new MainWP_Exception( 'ERROR:' . $err_msg, $url );
1817 } else {
1818 $thr_error = new MainWP_Exception( 'NOMAINWP', $url ); //phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
1819 $err_msg = 'Connection Failed. Please ensure that the MainWP Child plugin is installed and activated on the child site.';
1820 }
1821 $output['error_category'] = 'child_plugin_missing';
1822 $output['error_message'] = $err_msg;
1823 }
1824
1825 if ( null !== $thr_error ) {
1826 $thr_error->set_data( $hidden_data ); // to compatible.
1827 throw $thr_error;
1828 }
1829 }
1830
1831 /**
1832 * Method check_constraints()
1833 *
1834 * Check connection delay constraints.
1835 *
1836 * @param mixed $identifier Lock identifier.
1837 * @param mixed $website Object child site.
1838 *
1839 * @uses \MainWP\Dashboard\MainWP_DB_Common::close_open_requests()
1840 * @uses \MainWP\Dashboard\MainWP_DB::get_wp_ip()
1841 * @uses \MainWP\Dashboard\MainWP_Utility::end_session()
1842 */
1843 private static function check_constraints( &$identifier, $website ) { // phpcs:ignore -- NOSONAR -Current complexity is the only way to achieve desired results, pull request solutions appreciated.
1844 $semLock = '103218';
1845 $identifier = static::get_lock_identifier( $semLock );
1846 $minimumDelay = ( ( false === get_option( 'mainwp_minimumDelay' ) ) ? 200 : get_option( 'mainwp_minimumDelay' ) );
1847 if ( 0 < $minimumDelay ) {
1848 $minimumDelay = $minimumDelay / 1000;
1849 }
1850 $minimumIPDelay = ( ( false === get_option( 'mainwp_minimumIPDelay' ) ) ? 1000 : get_option( 'mainwp_minimumIPDelay' ) );
1851 if ( 0 < $minimumIPDelay ) {
1852 $minimumIPDelay = $minimumIPDelay / 1000;
1853 }
1854
1855 MainWP_Utility::end_session();
1856 $delay = true;
1857 while ( $delay ) {
1858 static::lock( $identifier );
1859 if ( 0 < $minimumDelay && static::check_constraints_last_request( $identifier, $minimumDelay ) ) {
1860 continue;
1861 }
1862
1863 if ( 0 < $minimumIPDelay && null !== $website ) {
1864 $ip = MainWP_DB::instance()->get_wp_ip( $website->id );
1865 if ( null !== $ip && '' !== $ip && static::check_constraints_last_request( $identifier, $minimumIPDelay, $ip ) ) {
1866 continue;
1867 }
1868 }
1869 $delay = false;
1870 }
1871
1872 $maximumRequests = ( ( false === get_option( 'mainwp_maximumRequests' ) ) ? 4 : get_option( 'mainwp_maximumRequests' ) );
1873 $maximumIPRequests = ( ( false === get_option( 'mainwp_maximumIPRequests' ) ) ? 1 : get_option( 'mainwp_maximumIPRequests' ) );
1874
1875 $first = true;
1876 $delay = true;
1877 while ( $delay ) {
1878 if ( ! $first ) {
1879 static::lock( $identifier );
1880 } else {
1881 $first = false;
1882 }
1883
1884 MainWP_DB_Common::instance()->close_open_requests();
1885
1886 if ( 0 < $maximumRequests && static::check_constraints_open_requests( $identifier, $maximumRequests ) ) {
1887 continue;
1888 }
1889
1890 if ( 0 < $maximumIPRequests && null !== $website ) {
1891 $ip = MainWP_DB::instance()->get_wp_ip( $website->id );
1892 if ( null !== $ip && '' !== $ip && static::check_constraints_open_requests( $identifier, $maximumIPRequests, $ip ) ) {
1893 continue;
1894 }
1895 }
1896 $delay = false;
1897 }
1898 }
1899
1900 /**
1901 * Method check_constraints_last_request().
1902 *
1903 * Check constraints for last requests.
1904 *
1905 * @param mixed $identifier connect identifier.
1906 * @param int $minimumDelay minimum delay.
1907 * @param string|null $ip ip address.
1908 *
1909 * @uses \MainWP\Dashboard\MainWP_DB_Common::get_last_request_timestamp()
1910 */
1911 private static function check_constraints_last_request( $identifier, $minimumDelay, $ip = null ) {
1912 $lastRequest = MainWP_DB_Common::instance()->get_last_request_timestamp( $ip );
1913 if ( $lastRequest > ( ( microtime( true ) ) - $minimumDelay ) ) {
1914 static::release( $identifier );
1915 $sleep = ( $minimumDelay - ( ( microtime( true ) ) - $lastRequest ) ) * 1000 * 1000;
1916 $sleep = max( 0, intval( $sleep ) );
1917 usleep( $sleep );
1918 return true;
1919 }
1920 return false;
1921 }
1922
1923 /**
1924 * Method check_constraints_open_requests().
1925 *
1926 * Check constraints for open requests.
1927 *
1928 * @param mixed $identifier connect identifier.
1929 * @param int $maximumRequests maximum requests.
1930 * @param string|null $ip ip address.
1931 *
1932 * @uses \MainWP\Dashboard\MainWP_DB_Common::get_nrof_open_requests()
1933 */
1934 private static function check_constraints_open_requests( $identifier, $maximumRequests, $ip = null ) {
1935 $nrOfOpenRequests = MainWP_DB_Common::instance()->get_nrof_open_requests( $ip );
1936 if ( $nrOfOpenRequests >= $maximumRequests ) {
1937 static::release( $identifier );
1938 usleep( 200000 );
1939 return true;
1940 }
1941 return false;
1942 }
1943
1944 /**
1945 * Method download_to_file()
1946 *
1947 * Download to file.
1948 *
1949 * @param mixed $url Download URL.
1950 * @param mixed $file File to download to.
1951 * @param bool $size Size of file.
1952 * @param null $http_user htaccess username.
1953 * @param null $http_pass htaccess password.
1954 *
1955 * @throws MainWP_Exception Exception message.
1956 *
1957 * @uses \MainWP\Dashboard\MainWP_Exception
1958 * @uses \MainWP\Dashboard\MainWP_System::$version
1959 * @uses \MainWP\Dashboard\MainWP_System_Utility::get_wp_file_system()
1960 */
1961 public static function download_to_file( $url, $file, $size = false, $http_user = null, $http_pass = null ) { // phpcs:ignore -- NOSONAR - complex.
1962
1963 $hasWPFileSystem = MainWP_System_Utility::get_wp_file_system();
1964
1965 /**
1966 * WordPress files system object.
1967 *
1968 * @global object
1969 */
1970 global $wp_filesystem;
1971
1972 if ( $wp_filesystem->exists( $file ) && ( ( false === $size ) || ( $wp_filesystem->size( $file ) > $size ) ) ) {
1973 $wp_filesystem->delete( $file );
1974 }
1975
1976 if ( ! $wp_filesystem->exists( dirname( $file ) ) ) {
1977 $wp_filesystem->mkdir( dirname( $file ), 0750 ); // MWP-1558: tightened from 0777; downloaded files may contain backup data.
1978 }
1979
1980 if ( ! $wp_filesystem->exists( dirname( $file ) ) ) {
1981 throw new MainWP_Exception( esc_html__( 'MainWP plugin could not create directory in order to download the file.', 'mainwp' ) );
1982 }
1983
1984 if ( $hasWPFileSystem && ! empty( $wp_filesystem ) ) {
1985 if ( ! $wp_filesystem->is_writable( @dirname( $file ) ) ) {
1986 throw new MainWP_Exception( esc_html__( 'MainWP upload directory is not writable.', 'mainwp' ) );
1987 }
1988 } elseif ( ! is_writable( @dirname( $file ) ) ) { //phpcs:ignore -- ok.
1989 throw new MainWP_Exception( esc_html__( 'MainWP upload directory is not writable.', 'mainwp' ) );
1990 }
1991
1992 $fp = fopen( $file, 'a' );
1993 $agent = 'Mozilla/5.0 (compatible; MainWP/' . MainWP_System::$version . '; +http://mainwp.com)';
1994 if ( false !== $size && $wp_filesystem->exists( $file ) ) {
1995 $size = $wp_filesystem->size( $file );
1996 $url .= '&foffset=' . $size;
1997 }
1998 $ch = curl_init( str_replace( ' ', '%20', $url ) );
1999
2000 $proxy = new \WP_HTTP_Proxy();
2001 if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) {
2002 curl_setopt( $ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP );
2003 curl_setopt( $ch, CURLOPT_PROXY, $proxy->host() );
2004 curl_setopt( $ch, CURLOPT_PROXYPORT, $proxy->port() );
2005
2006 if ( $proxy->use_authentication() ) {
2007 curl_setopt( $ch, CURLOPT_PROXYAUTH, CURLAUTH_ANY );
2008 curl_setopt( $ch, CURLOPT_PROXYUSERPWD, $proxy->authentication() );
2009 }
2010 }
2011 curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
2012 curl_setopt( $ch, CURLOPT_HEADER, false );
2013 curl_setopt( $ch, CURLOPT_FILE, $fp );
2014
2015 curl_setopt( $ch, CURLOPT_USERAGENT, $agent );
2016 curl_setopt( $ch, CURLOPT_ENCODING, 'none' );
2017
2018 curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, true );
2019 if ( ! empty( $http_user ) && ! empty( $http_pass ) ) {
2020 $http_pass = stripslashes( $http_pass );
2021 curl_setopt( $ch, CURLOPT_USERPWD, "$http_user:$http_pass" );
2022 }
2023 curl_exec( $ch );
2024 if ( static::is_valid_curl_handle( $ch ) ) {
2025 curl_close( $ch );
2026 }
2027 fclose( $fp );
2028 }
2029
2030 /**
2031 * Method init_coockiesdir()
2032 *
2033 * Check for cookies directory and create it if it doesn't already exist,
2034 * set the file permissions and update htaccess.
2035 *
2036 * @param mixed $cookieDir Cookies directory.
2037 *
2038 * @return void
2039 *
2040 * @uses \MainWP\Dashboard\MainWP_System_Utility::get_wp_file_system()
2041 */
2042 public static function init_cookiesdir( $cookieDir ) {
2043
2044 $hasWPFileSystem = MainWP_System_Utility::get_wp_file_system();
2045
2046 /**
2047 * WordPress files system object.
2048 *
2049 * @global object
2050 */
2051 global $wp_filesystem;
2052
2053 if ( $hasWPFileSystem && ! empty( $wp_filesystem ) ) {
2054
2055 if ( ! $wp_filesystem->is_dir( $cookieDir ) ) {
2056 $wp_filesystem->mkdir( $cookieDir, 0750 ); // MWP-1558: tightened from 0777; cookies/ holds child wp-admin session cookies.
2057 }
2058
2059 if ( ! file_exists( $cookieDir . '/.htaccess' ) ) {
2060 $file_htaccess = $cookieDir . '/.htaccess';
2061 $wp_filesystem->put_contents( $file_htaccess, 'deny from all' );
2062 }
2063
2064 if ( ! file_exists( $cookieDir . '/index.php' ) ) {
2065 $file_index = $cookieDir . '/index.php';
2066 $wp_filesystem->touch( $file_index );
2067 }
2068 } else {
2069
2070 if ( ! file_exists( $cookieDir ) ) {
2071 @mkdir( $cookieDir, 0750, true ); // MWP-1558: tightened from 0777; cookies/ holds child wp-admin session cookies.
2072 }
2073
2074 if ( ! file_exists( $cookieDir . '/.htaccess' ) ) {
2075 $file_htaccess = @fopen( $cookieDir . '/.htaccess', 'w+' );
2076 @fwrite( $file_htaccess, 'deny from all' );
2077 @fclose( $file_htaccess );
2078 }
2079
2080 if ( ! file_exists( $cookieDir . '/index.php' ) ) {
2081 $file_index = @fopen( $cookieDir . '/index.php', 'w+' );
2082 @fclose( $file_index );
2083 }
2084 }
2085 }
2086
2087 /**
2088 * Method get_file_content()
2089 *
2090 * Get contents of file.
2091 *
2092 * @param mixed $url File Location.
2093 *
2094 * @return mixed false|$data
2095 *
2096 * @uses \MainWP\Dashboard\MainWP_System::$version
2097 */
2098 public static function get_file_content( $url ) {
2099 $agent = 'Mozilla/5.0 (compatible; MainWP/' . MainWP_System::$version . '; +http://mainwp.com)';
2100 $ch = curl_init();
2101
2102 $proxy = new \WP_HTTP_Proxy();
2103 if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) {
2104 curl_setopt( $ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP );
2105 curl_setopt( $ch, CURLOPT_PROXY, $proxy->host() );
2106 curl_setopt( $ch, CURLOPT_PROXYPORT, $proxy->port() );
2107
2108 if ( $proxy->use_authentication() ) {
2109 curl_setopt( $ch, CURLOPT_PROXYAUTH, CURLAUTH_ANY );
2110 curl_setopt( $ch, CURLOPT_PROXYUSERPWD, $proxy->authentication() );
2111 }
2112 }
2113
2114 curl_setopt( $ch, CURLOPT_HEADER, 0 );
2115 curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
2116 curl_setopt( $ch, CURLOPT_URL, $url );
2117 curl_setopt( $ch, CURLOPT_USERAGENT, $agent );
2118 curl_setopt( $ch, CURLOPT_ENCODING, 'none' );
2119
2120 $data = @curl_exec( $ch );
2121 $httpCode = @curl_getinfo( $ch, CURLINFO_HTTP_CODE );
2122 if ( static::is_valid_curl_handle( $ch ) ) {
2123 curl_close( $ch );
2124 }
2125 if ( 200 === (int) $httpCode ) {
2126 return $data;
2127 } else {
2128 return false;
2129 }
2130 }
2131
2132 /**
2133 * Method is_valid_curl_handle
2134 *
2135 * @param mixed $ch cURL handle to validate.
2136 * @return bool Valid curl handle.
2137 */
2138 public static function is_valid_curl_handle( $ch ) {
2139 return is_resource( $ch )
2140 || ( is_object( $ch )
2141 && class_exists( 'CurlHandle', false )
2142 && $ch instanceof \CurlHandle
2143 );
2144 }
2145
2146 /**
2147 * Method get_favico_url()
2148 *
2149 * Get Child Site favicon URL.
2150 *
2151 * @param mixed $website Child Site info.
2152 *
2153 * @return mixed $faviurl Favicon URL.
2154 *
2155 * @uses \MainWP\Dashboard\MainWP_DB::get_website_option()
2156 * @uses \MainWP\Dashboard\MainWP_System_Utility::get_icons_dir()
2157 * @uses \MainWP\Dashboard\MainWP_Utility::remove_http_prefix()
2158 */
2159 public static function get_favico_url( $website ) {
2160 $favi = MainWP_DB::instance()->get_website_option( $website, 'favi_icon', '' );
2161 $faviurl = '';
2162
2163 if ( ! empty( $favi ) ) {
2164 if ( false !== strpos( $favi, 'favi-' . intval( $website->id ) . '-' ) ) {
2165 $dirs = MainWP_System_Utility::get_icons_dir();
2166 if ( file_exists( $dirs[0] . $favi ) ) {
2167 $faviurl = $dirs[1] . $favi;
2168 } else {
2169 $faviurl = '';
2170 }
2171 } elseif ( ( 0 === strpos( $favi, '//' ) ) || ( 0 === strpos( $favi, 'http' ) ) ) {
2172 $faviurl = $favi;
2173 } else {
2174 $faviurl = $website->url . $favi;
2175 $faviurl = MainWP_Utility::remove_http_prefix( $faviurl );
2176 }
2177 }
2178
2179 if ( empty( $faviurl ) ) {
2180 $faviurl = false;
2181 }
2182
2183 return $faviurl;
2184 }
2185 }
2186