PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / 6.1.4
MainWP Dashboard: Self-hosted WordPress Management for Agencies v6.1.4
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.4, at class/class-mainwp-connect.php

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