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

2,209 lines 90.5 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 $retry_added = false;
982 do {
983 $retry_added = false;
984 if ( 20 < time() - $lastRun ) {
985 MainWP_System_Utility::set_time_limit( $timeout );
986 $lastRun = time();
987 }
988
989 curl_multi_exec( $mh, $running );
990 curl_multi_select( $mh );
991 while ( $info = curl_multi_info_read( $mh ) ) {
992 $data = curl_multi_getcontent( $info['handle'] );
993 $contains = ( 0 < preg_match( '/<mainwp>(.*)<\/mainwp>/', $data, $results ) );
994 curl_multi_remove_handle( $mh, $info['handle'] );
995
996 $rid = static::get_resource_id( $info['handle'] );
997 if ( ! $contains && isset( $requestUrls[ $rid ] ) ) {
998 curl_setopt( $info['handle'], CURLOPT_URL, $requestUrls[ $rid ] );
999 curl_setopt( $info['handle'], CURLOPT_FRESH_CONNECT, true );
1000 curl_setopt( $info['handle'], CURLOPT_FORBID_REUSE, true );
1001 $add_retry = curl_multi_add_handle( $mh, $info['handle'] );
1002 if ( CURLM_OK === $add_retry ) {
1003 $mrc = curl_multi_exec( $mh, $running );
1004
1005 if ( CURLM_OK === $mrc ) {
1006 $retry_added = true;
1007 unset( $requestUrls[ $rid ] );
1008 continue; // libcurl updates $running automatically.
1009 }
1010
1011 curl_multi_remove_handle( $mh, $info['handle'] );
1012 }
1013 }
1014
1015 if ( ! $contains ) {
1016 // Add useful debug log for unexpected response.
1017 $log_data = (string) $data;
1018 if ( is_string( $log_data ) && strlen( $log_data ) > 2000 ) {
1019 $log_data = substr( $log_data, 0, 2000 ) . '...[truncated]';
1020 }
1021 $log_url = isset( $requestUrls[ $rid ] ) ? $requestUrls[ $rid ] : '';
1022 if ( empty( $log_url ) ) {
1023 $log_url = ( isset( $handleToWebsite[ $rid ] ) && is_object( $handleToWebsite[ $rid ] ) && property_exists( $handleToWebsite[ $rid ], 'url' ) ) ? $handleToWebsite[ $rid ]->url : 'Unknown';
1024 }
1025 MainWP_Logger::instance()->debug( 'curl_multi_getcontent :: unexpected response :: [data=' . $log_data . '] :: [url=' . $log_url . ']' );
1026 }
1027
1028 if ( null !== $handler ) {
1029 $site = &$handleToWebsite[ static::get_resource_id( $info['handle'] ) ];
1030 call_user_func_array( $handler, array( $data, $site, &$output, $params ) );
1031 }
1032
1033 unset( $handleToWebsite[ static::get_resource_id( $info['handle'] ) ] );
1034 if ( static::is_valid_curl_handle( $info['handle'] ) ) {
1035 curl_close( $info['handle'] );
1036 }
1037 unset( $info['handle'] );
1038 }
1039 usleep( 10000 );
1040 } while ( $running > 0 || $retry_added );
1041
1042 if ( static::is_valid_curl_handle( $mh ) ) {
1043 curl_multi_close( $mh );
1044 }
1045 } else {
1046 foreach ( $requestHandles as $ch ) {
1047 $data = curl_exec( $ch );
1048
1049 if ( null !== $handler ) {
1050 $site = &$handleToWebsite[ static::get_resource_id( $ch ) ];
1051 call_user_func_array( $handler, array( $data, $site, &$output, $params ) );
1052 }
1053 }
1054 }
1055
1056 return true;
1057 }
1058
1059 /**
1060 * Credits WordPress org.
1061 *
1062 * Get the correct "Expect" header for the given request data.
1063 *
1064 * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD.
1065 * @return string The "Expect" header.
1066 */
1067 public static function get_expect_header( $data ) {
1068 if ( ! is_array( $data ) ) {
1069 return strlen( (string) $data ) >= 1048576 ? '100-Continue' : '';
1070 }
1071
1072 $bytesize = 0;
1073 $iterator = new \RecursiveIteratorIterator( new \RecursiveArrayIterator( $data ) );
1074
1075 foreach ( $iterator as $datum ) {
1076 $bytesize += strlen( (string) $datum );
1077
1078 if ( $bytesize >= 1048576 ) {
1079 return '100-Continue';
1080 }
1081 }
1082
1083 return '';
1084 }
1085
1086 /**
1087 * Method get_resource_id()
1088 *
1089 * Get resource id.
1090 *
1091 * @param mixed $res The given resource.
1092 *
1093 * @return $result Resource ID only.
1094 */
1095 public static function get_resource_id( $res ) {
1096 $result = false;
1097 if ( is_a( $res, 'CurlHandle' ) ) {
1098 $result = spl_object_hash( $res );
1099 } elseif ( is_resource( $res ) ) {
1100 $resourceString = (string) $res;
1101 $exploded = explode( '#', $resourceString );
1102 $result = array_pop( $exploded );
1103 }
1104 return $result;
1105 }
1106
1107 /**
1108 * Method get_lock_identifier().
1109 *
1110 * Get lock identifier.
1111 *
1112 * @param mixed $pLockName Provided Lock Name.
1113 *
1114 * @return mixed false|sem_get()|@fopen
1115 */
1116 public static function get_lock_identifier( $pLockName ) {
1117 if ( ( null === $pLockName ) || ( false === $pLockName ) ) {
1118 return false;
1119 }
1120
1121 if ( function_exists( 'sem_get' ) ) {
1122 return sem_get( $pLockName );
1123 } else {
1124 $fh = @fopen( sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'lock' . $pLockName . '.txt', 'w+' );
1125 if ( ! $fh ) {
1126 return false;
1127 }
1128
1129 return $fh;
1130 }
1131 }
1132
1133 /**
1134 * Method lock()
1135 *
1136 * Use sem_acquire or @flock to lock the $identifier.
1137 *
1138 * @param mixed $identifier Identifier.
1139 *
1140 * @return mixed false|sem_acquire()|@flock
1141 */
1142 public static function lock( $identifier ) {
1143 if ( ( null === $identifier ) || ( false === $identifier ) ) {
1144 return false;
1145 }
1146
1147 if ( function_exists( 'sem_acquire' ) ) {
1148 return sem_acquire( $identifier );
1149 } else {
1150 if ( ! is_resource( $identifier ) ) {
1151 return false; // to fix.
1152 }
1153 for ( $i = 0; $i < 3; $i++ ) {
1154 if ( @flock( $identifier, LOCK_EX ) ) {
1155 return $identifier;
1156 } else {
1157 sleep( 1 );
1158 }
1159 }
1160 return false;
1161 }
1162 }
1163
1164 /**
1165 * Method release()
1166 *
1167 * Use sem_release or @flock, @fclose to unlock $identifier.
1168 *
1169 * @param mixed $identifier Identifier.
1170 *
1171 * @return mixed false|sem_release()|@flock
1172 */
1173 public static function release( $identifier ) {
1174 if ( ( null === $identifier ) || ( false === $identifier ) ) {
1175 return false;
1176 }
1177
1178 if ( function_exists( 'sem_release' ) ) {
1179 return sem_release( $identifier );
1180 } else {
1181 if ( ! is_resource( $identifier ) ) {
1182 return false; // to fix.
1183 }
1184 @flock( $identifier, LOCK_UN );
1185 @fclose( $identifier );
1186 }
1187
1188 return false;
1189 }
1190
1191 /**
1192 * Method fetch_url_authed()
1193 *
1194 * Updates the child site via authenticated request.
1195 *
1196 * @param object $website Website information.
1197 * @param string $what Function to perform.
1198 * @param null $params Function parameters.
1199 * @param bool $checkConstraints Whether or not to check constraints.
1200 * @param bool $pForceFetch Whether or not to force the fetch.
1201 * @param bool $pRetryFailed Whether or not to retry the fetch process.
1202 * @param null $rawResponse Raw response.
1203 *
1204 * @return mixed $information
1205 *
1206 * @uses \MainWP\Dashboard\MainWP_Monitoring_Handler::handle_check_website()
1207 * @uses \MainWP\Dashboard\MainWP_Premium_Update::maybe_request_premium_updates()
1208 * @uses \MainWP\Dashboard\MainWP_Sync::sync_information_array()
1209 */
1210 public static function fetch_url_authed( // phpcs:ignore -- NOSONAR - complex.
1211 &$website,
1212 $what,
1213 $params = null,
1214 $checkConstraints = false,
1215 $pForceFetch = false,
1216 $pRetryFailed = true,
1217 $rawResponse = null
1218 ) {
1219 unset( $pForceFetch );
1220
1221 // to support demo data.
1222 if ( MainWP_Demo_Handle::get_instance()->is_demo_website( $website ) ) {
1223 return MainWP_Demo_Handle::get_instance()->handle_action_demo( $website, $what );
1224 }
1225
1226 /**
1227 * Filter to mock fetch_url_authed response before any HTTP/signing occurs.
1228 *
1229 * This filter fires early, before OpenSSL signing or HTTP requests, allowing
1230 * tests to bypass child site communication entirely.
1231 *
1232 * SECURITY WARNING - TEST ONLY:
1233 * This filter ONLY fires when ALL of the following conditions are met:
1234 * 1. MAINWP_TESTING_MODE constant is defined and true
1235 * 2. A PHPUnit test harness constant is present (WP_TESTS_DOMAIN, PHPUNIT_COMPOSER_INSTALL, or WP_TESTS_DIR)
1236 *
1237 * This triple-check prevents malicious code from defining MAINWP_TESTING_MODE
1238 * in production to spoof child site responses.
1239 *
1240 * IMPORTANT: MAINWP_TESTING_MODE must ONLY be defined in the PHPUnit bootstrap
1241 * file (tests/bootstrap.php). Defining it in production code, wp-config.php, or
1242 * plugin files would create a security vulnerability allowing response spoofing.
1243 *
1244 * @since 5.4
1245 *
1246 * @param mixed $pre_result Return non-false to short-circuit and return this value.
1247 * @param object $website Website object being communicated with.
1248 * @param string $what Action being performed (e.g., 'plugin_action').
1249 * @param array $params Request parameters.
1250 * @return mixed Array to return early, false to proceed normally.
1251 */
1252 $is_phpunit_env = defined( 'WP_TESTS_DOMAIN' ) || defined( 'PHPUNIT_COMPOSER_INSTALL' ) || ( defined( 'WP_TESTS_DIR' ) && WP_TESTS_DIR );
1253 if ( defined( 'MAINWP_TESTING_MODE' ) && MAINWP_TESTING_MODE && $is_phpunit_env ) {
1254 $pre_result = apply_filters( 'mainwp_fetch_url_authed_pre', false, $website, $what, $params );
1255 if ( false !== $pre_result ) {
1256 return $pre_result;
1257 }
1258 }
1259
1260 if ( ! is_array( $params ) ) {
1261 $params = array();
1262 }
1263
1264 $others = array(
1265 'force_use_ipv4' => $website->force_use_ipv4,
1266 'upgrade' => ( 'upgradeplugintheme' === $what || 'upgrade' === $what || 'upgradetranslation' === $what ),
1267 );
1268
1269 $request_update = MainWP_Premium_Update::maybe_request_premium_updates( $website, $what, $params );
1270
1271 if ( isset( $rawResponse ) && $rawResponse ) {
1272 $others['raw_response'] = 'yes';
1273 }
1274
1275 $params['optimize'] = ( ( 1 === (int) get_option( 'mainwp_optimize', 1 ) ) ? 1 : 0 );
1276
1277 $updating_website = false;
1278 $type = '';
1279 $list = '';
1280 if ( 'upgradeplugintheme' === $what || 'upgrade' === $what || 'upgradetranslation' === $what ) {
1281 $updating_website = true;
1282 if ( 'upgradeplugintheme' === $what || 'upgradetranslation' === $what ) {
1283 $type = $params['type'];
1284 $list = $params['list'];
1285 } else {
1286 $type = 'wp';
1287 $list = '';
1288 }
1289 }
1290
1291 if ( $updating_website ) {
1292 /**
1293 * Action: mainwp_website_before_updated
1294 *
1295 * Fires before the child site update process.
1296 *
1297 * @param object $website Object containing child site info.
1298 * @param string $type Type parameter.
1299 * @param string $list List parameter.
1300 *
1301 * @since Unknown
1302 */
1303 do_action( 'mainwp_website_before_updated', $website, $type, $list );
1304 }
1305
1306 if ( 'renew' === $what ) {
1307 $postdata = static::get_renew_post_data_authed( $website, $what );
1308 } else {
1309 $postdata = static::get_post_data_authed( $website, $what, $params );
1310
1311 }
1312 $others['function'] = $what;
1313
1314 $information = array();
1315 $output = array();
1316
1317 if ( ! $request_update ) {
1318 // MWP-1548: decrypt http_user / http_pass before they hit the
1319 // outbound HTTP Basic Auth header. Legacy plaintext rows pass
1320 // through unchanged.
1321 $http_user_plain = MainWP_Credential_Storage::decrypt_credential( $website->http_user );
1322 $http_pass_plain = MainWP_Credential_Storage::decrypt_credential( $website->http_pass );
1323 $information = static::fetch_url( $website, $website->url, $postdata, $checkConstraints, $website->verify_certificate, $pRetryFailed, $http_user_plain, $http_pass_plain, $website->ssl_version, $others, $output );
1324 if ( ! empty( $output ) ) {
1325 if ( ! is_array( $information ) ) {
1326 $information = array();
1327 }
1328 $information['fetch_url_output'] = $output;
1329 }
1330 /**
1331 * Fires immediately after fetch url action.
1332 *
1333 * @param object $website website.
1334 * @param array $information information result data.
1335 * @param string $what action.
1336 * @param array $params params input array.
1337 * @param array $others others input array.
1338 *
1339 * @since 4.5.1.1
1340 */
1341 do_action( 'mainwp_fetch_url_authed', $website, $information, $what, $params, $others );
1342 } else {
1343 $slug = $params['list'];
1344 $information['upgrades'] = array( $slug => 1 );
1345 }
1346
1347 if ( is_array( $information ) && isset( $information['sync'] ) && ! empty( $information['sync'] ) ) {
1348 MainWP_Sync::sync_information_array( $website, $information['sync'] );
1349 unset( $information['sync'] );
1350 }
1351
1352 if ( $updating_website ) {
1353 /**
1354 * Action: mainwp_website_updated
1355 *
1356 * Fires after the child site update process.
1357 *
1358 * @param object $website Object containing child site info.
1359 * @param string $type Type parameter.
1360 * @param string $list List parameter.
1361 * @param array $information Array containing the information fetched from the child site.
1362 *
1363 * @since Unknown
1364 */
1365 do_action( 'mainwp_website_updated', $website, $type, $list, $information );
1366 if ( 1 === (int) get_option( 'mainwp_check_http_response', 0 ) ) {
1367 MainWP_Monitoring_Handler::handle_check_website( $website, true );
1368 }
1369 }
1370
1371 return $information;
1372 }
1373
1374 /**
1375 * Method fetch_url_not_authed()
1376 *
1377 * Fetch not authorized URL.
1378 *
1379 * @param string $url URL to fetch from.
1380 * @param string $admin Admin name.
1381 * @param string $what Function to perform.
1382 * @param null $params Function parameters.
1383 * @param bool $pForceFetch true|false Whether or not to force the fetch.
1384 * @param null $verifyCertificate Verify the SSL Certificate.
1385 * @param null $http_user htaccess username.
1386 * @param null $http_pass htaccess password.
1387 * @param integer $sslVersion SSL version to check for.
1388 * @param array $others Other functions to perform.
1389 * @param array $output Output values.
1390 *
1391 * @return mixed static::fetch_url() Fetch URL.
1392 */
1393 public static function fetch_url_not_authed( // phpcs:ignore -- NOSONAR - compatible.
1394 $url,
1395 $admin,
1396 $what,
1397 $params = null,
1398 $pForceFetch = false,
1399 $verifyCertificate = null,
1400 $http_user = null,
1401 $http_pass = null,
1402 $sslVersion = 0,
1403 $others = array(),
1404 &$output = array()
1405 ) {
1406 unset( $pForceFetch );
1407
1408 if ( empty( $params ) ) {
1409 $params = array();
1410 }
1411
1412 $postdata = static::get_post_data_not_authed( $url, $admin, $what, $params );
1413 $website = null;
1414
1415 $others['function'] = $what;
1416 return static::fetch_url( $website, $url, $postdata, false, $verifyCertificate, true, $http_user, $http_pass, $sslVersion, $others, $output );
1417 }
1418
1419 /**
1420 * Method fetch_url()
1421 *
1422 * Fetch URL.
1423 *
1424 * @param object $website Child Site info.
1425 * @param string $url URL to fetch from.
1426 * @param mixed $postdata Post data to fetch.
1427 * @param bool $checkConstraints true|false Whether or not to check constraints.
1428 * @param null $verifyCertificate Verify SSL Certificate.
1429 * @param bool $pRetryFailed ture|false Whether or not the Retry has failed.
1430 * @param null $http_user htaccess username.
1431 * @param null $http_pass htaccess password.
1432 * @param integer $sslVersion SSL version.
1433 * @param array $others Other functions to perform.
1434 * @param array $output Output values.
1435 *
1436 * @throws \Exception Exception message.
1437 *
1438 * @return mixed static::fetch_url_site()
1439 */
1440 public static function fetch_url( // phpcs:ignore -- NOSONAR - complex.
1441 &$website,
1442 $url,
1443 $postdata,
1444 $checkConstraints = false,
1445 $verifyCertificate = null,
1446 $pRetryFailed = true,
1447 $http_user = null,
1448 $http_pass = null,
1449 $sslVersion = 0,
1450 $others = array(),
1451 &$output = array()
1452 ) {
1453
1454 $start = time();
1455
1456 try {
1457 $tmpUrl = $url;
1458 if ( '/' !== substr( $tmpUrl, - 1 ) ) {
1459 $tmpUrl .= '/';
1460 }
1461
1462 if ( false === strpos( $url, 'wp-admin' ) ) {
1463 $tmpUrl .= 'wp-admin/admin-ajax.php';
1464 }
1465
1466 return static::fetch_url_site( $website, $tmpUrl, $postdata, $checkConstraints, $verifyCertificate, $http_user, $http_pass, $sslVersion, $others, $output );
1467 } catch ( \Exception $e ) {
1468 if ( ! $pRetryFailed || ( 30 < ( time() - $start ) ) ) {
1469 throw $e;
1470 }
1471
1472 try {
1473 return static::fetch_url_site( $website, $url, $postdata, $checkConstraints, $verifyCertificate, $http_user, $http_pass, $sslVersion, $others, $output );
1474 } catch ( \Exception $ex ) {
1475 throw $e;
1476 }
1477 }
1478 }
1479
1480 /**
1481 * Method fetch_url_site()
1482 *
1483 * M Fetch URL.
1484 *
1485 * @param object $website Child Site info.
1486 * @param string $url URL to fetch from.
1487 * @param mixed $postdata Post data to fetch.
1488 * @param bool $checkConstraints true|false Whether or not to check constraints.
1489 * @param null $verifyCertificate Verify SSL Certificate.
1490 * @param null $http_user htaccess username.
1491 * @param null $http_pass htaccess password.
1492 * @param integer $sslVersion SSL version.
1493 * @param array $others Other functions to perform.
1494 * @param array $output Output values.
1495 *
1496 * @return mixed $data, $information.
1497 * @throws MainWP_Exception Exception message.
1498 *
1499 * @uses \MainWP\Dashboard\MainWP_DB_Common::insert_or_update_request_log()
1500 * @uses \MainWP\Dashboard\MainWP_Exception
1501 * @uses \MainWP\Dashboard\MainWP_Logger::debug_for_website()
1502 * @uses \MainWP\Dashboard\MainWP_System::$version
1503 * @uses \MainWP\Dashboard\MainWP_System_Utility::get_mainwp_dir()
1504 * @uses \MainWP\Dashboard\MainWP_System_Utility::get_child_response()
1505 * @uses \MainWP\Dashboard\MainWP_Utility::value_to_string()
1506 * @uses \MainWP\Dashboard\MainWP_Utility::end_session()
1507 */
1508 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.
1509 &$website,
1510 $url,
1511 $postdata,
1512 $checkConstraints = false,
1513 $verifyCertificate = null,
1514 $http_user = null,
1515 $http_pass = null,
1516 $sslVersion = 0,
1517 $others = array(),
1518 &$output = array()
1519 ) {
1520
1521 /**
1522 * Enables data to be returned prior to connecting to the site.
1523 *
1524 * Dev/test override only. Gated behind the MAINWP_DEV_FILTERS_ENABLED
1525 * constant so the filter does not dispatch in production. The filter
1526 * receives plaintext HTTP Basic Auth credentials and the full $website
1527 * DB row (including privkey); enabling it in production would expose
1528 * those values to any 3rd-party plugin hooking the filter.
1529 *
1530 * To enable in a dev/test environment, add to wp-config.php:
1531 * define( 'MAINWP_DEV_FILTERS_ENABLED', true );
1532 *
1533 * @since 5.5
1534 *
1535 * @param mixed false
1536 * @param mixed $website
1537 * @param mixed $url
1538 * @param mixed $postdata
1539 * @param mixed $checkConstraints
1540 * @param mixed $verifyCertificate
1541 * @param mixed $http_user
1542 * @param mixed $http_pass
1543 * @param mixed $sslVersion
1544 * @param mixed $others
1545 * @param mixed $output
1546 */
1547 if ( defined( 'MAINWP_DEV_FILTERS_ENABLED' ) && MAINWP_DEV_FILTERS_ENABLED ) {
1548 $dev_data = apply_filters( 'mainwp_dev_return_data_before_connect_site', false, $website, $url, $postdata, $checkConstraints, $verifyCertificate, $http_user, $http_pass, $sslVersion, $others, $output );
1549 if ( false !== $dev_data ) {
1550 return $dev_data;
1551 }
1552 }
1553
1554 $agent = 'Mozilla/5.0 (compatible; MainWP/' . MainWP_System::$version . '; +http://mainwp.com)';
1555
1556 if ( ! empty( $website ) ) {
1557 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url_site', 'Request to [' . $url . '] [' . MainWP_Utility::value_to_string( $postdata, 1 ) . ']' );
1558 }
1559
1560 $identifier = null;
1561 if ( $checkConstraints ) {
1562 static::check_constraints( $identifier, $website );
1563 }
1564
1565 if ( null !== $website ) {
1566 MainWP_DB_Common::instance()->insert_or_update_request_log( $website->id, null, microtime( true ), null );
1567 }
1568
1569 if ( null !== $identifier ) {
1570 static::release( $identifier );
1571 }
1572
1573 $dirs = MainWP_System_Utility::get_mainwp_dir();
1574 $cookieDir = $dirs[0] . 'cookies';
1575
1576 static::init_cookiesdir( $cookieDir );
1577
1578 $fetch_track_id = MainWP_Execution_Helper::execute_call_track( 'start_point', $website, $postdata );
1579
1580 $ch = curl_init();
1581
1582 $proxy = new \WP_HTTP_Proxy();
1583 if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) {
1584 curl_setopt( $ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP );
1585 curl_setopt( $ch, CURLOPT_PROXY, $proxy->host() );
1586 curl_setopt( $ch, CURLOPT_PROXYPORT, $proxy->port() );
1587
1588 if ( $proxy->use_authentication() ) {
1589 curl_setopt( $ch, CURLOPT_PROXYAUTH, CURLAUTH_ANY );
1590 curl_setopt( $ch, CURLOPT_PROXYUSERPWD, $proxy->authentication() );
1591 }
1592 }
1593
1594 if ( ( null !== $website ) && ( ( property_exists( $website, 'wpe' ) && 1 !== $website->wpe ) || ( isset( $others['upgrade'] ) && ( true === $others['upgrade'] ) ) ) ) {
1595 // to fix.
1596 if ( defined( 'LOGGED_IN_SALT' ) && defined( 'NONCE_SALT' ) ) {
1597 $cookie_salt = sha1( sha1( 'mainwp' . LOGGED_IN_SALT . $website->id ) . NONCE_SALT . 'WP_Cookie' ); // NOSONAR - safe for salt file name.
1598 } else {
1599 // MWP-1558: misconfigured WP installs (no salts) previously used unsalted SHA1, which is enumerable. Fall back to the per-install MainWP filename secret instead.
1600 $cookie_salt = MainWP_System_Utility::get_private_filename( 'cookies', $website->id, 'WP_Cookie' );
1601 }
1602 $cookieFile = $cookieDir . '/' . $cookie_salt;
1603 if ( ! file_exists( $cookieFile ) ) {
1604 @file_put_contents( $cookieFile, '' );
1605 }
1606
1607 if ( file_exists( $cookieFile ) ) {
1608 @chmod( $cookieFile, 0644 ); // NOSONAR - correct file permissions, owner: rwe, group & others: r.
1609 curl_setopt( $ch, CURLOPT_COOKIEJAR, $cookieFile );
1610 curl_setopt( $ch, CURLOPT_COOKIEFILE, $cookieFile );
1611 }
1612 }
1613
1614 curl_setopt( $ch, CURLOPT_URL, $url );
1615 curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
1616 curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, true );
1617 curl_setopt( $ch, CURLOPT_POST, true );
1618 curl_setopt( $ch, CURLOPT_POSTFIELDS, $postdata );
1619 curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, 10 );
1620 curl_setopt( $ch, CURLOPT_USERAGENT, $agent );
1621 curl_setopt( $ch, CURLOPT_ENCODING, 'none' );
1622
1623 if ( ! empty( $http_user ) && ! empty( $http_pass ) ) {
1624 $http_pass = stripslashes( $http_pass );
1625 curl_setopt( $ch, CURLOPT_USERPWD, "$http_user:$http_pass" );
1626 }
1627
1628 $ssl_verifyhost = false;
1629 if ( null !== $verifyCertificate ) {
1630 if ( 1 === (int) $verifyCertificate ) {
1631 $ssl_verifyhost = true;
1632 } elseif ( 2 === (int) $verifyCertificate ) {
1633 if ( ( false === get_option( 'mainwp_sslVerifyCertificate' ) ) || ( 1 === (int) get_option( 'mainwp_sslVerifyCertificate' ) ) ) {
1634 $ssl_verifyhost = true;
1635 }
1636 }
1637 } elseif ( ( false === get_option( 'mainwp_sslVerifyCertificate' ) ) || ( 1 === (int) get_option( 'mainwp_sslVerifyCertificate' ) ) ) {
1638 $ssl_verifyhost = true;
1639 }
1640
1641 if ( $ssl_verifyhost ) {
1642 curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, 2 );
1643 curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true );
1644 } else {
1645 curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, false ); // NOSONAR.
1646 curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false ); // NOSONAR.
1647 }
1648
1649 curl_setopt( $ch, CURLOPT_SSLVERSION, $sslVersion );
1650
1651 $http_version = apply_filters( 'mainwp_curl_http_version', false, $website ? $website->id : false, $url );
1652 if ( false !== $http_version ) {
1653 curl_setopt( $ch, CURLOPT_HTTP_VERSION, $http_version );
1654 }
1655
1656 $curlopt_resolve = false;
1657
1658 if ( is_object( $website ) && property_exists( $website, 'id' ) ) {
1659 $curlopt_resolve = apply_filters( 'mainwp_curl_curlopt_resolve', false, $website->id, $website->url );
1660 }
1661
1662 if ( is_array( $curlopt_resolve ) && ! empty( $curlopt_resolve ) ) {
1663 curl_setopt( $ch, CURLOPT_RESOLVE, $curlopt_resolve );
1664 curl_setopt( $ch, CURLOPT_DNS_USE_GLOBAL_CACHE, false );
1665 }
1666
1667 $headers = array( 'X-Requested-With' => 'XMLHttpRequest' );
1668 $headers['Expect'] = static::get_expect_header( $postdata );
1669
1670 $headers = apply_filters( 'mainwp_connect_http_request_headers', $headers, $website );
1671
1672 if ( class_exists( '\WpOrg\Requests\Requests' ) ) {
1673 $headers = \WpOrg\Requests\Requests::flatten( $headers );
1674 } else {
1675 $headers = \Requests::flatten( $headers );
1676 }
1677
1678 curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
1679 curl_setopt( $ch, CURLOPT_REFERER, get_option( 'siteurl' ) );
1680
1681 $force_use_ipv4 = false;
1682 $forceUseIPv4 = isset( $others['force_use_ipv4'] ) ? (int) $others['force_use_ipv4'] : null;
1683 if ( null !== $forceUseIPv4 ) {
1684 if ( 1 === $forceUseIPv4 ) {
1685 $force_use_ipv4 = true;
1686 } elseif ( 2 === $forceUseIPv4 ) {
1687 if ( 1 === (int) get_option( 'mainwp_forceUseIPv4' ) ) {
1688 $force_use_ipv4 = true;
1689 }
1690 }
1691 } elseif ( 1 === (int) get_option( 'mainwp_forceUseIPv4' ) ) {
1692 $force_use_ipv4 = true;
1693 }
1694
1695 if ( $force_use_ipv4 && defined( 'CURLOPT_IPRESOLVE' ) && defined( 'CURL_IPRESOLVE_V4' ) ) {
1696 curl_setopt( $ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4 );
1697 }
1698
1699 $what = '';
1700 if ( is_array( $others ) && isset( $others['function'] ) ) {
1701 $what = $others['function'];
1702 }
1703
1704 if ( 'deactivate' === $what ) {
1705 $timeout = 120; // 2 minutes.
1706 } else {
1707 $timeout = 20 * 60 * 60;
1708 }
1709
1710 curl_setopt( $ch, CURLOPT_TIMEOUT, $timeout );
1711 MainWP_System_Utility::set_time_limit( $timeout );
1712
1713 MainWP_Utility::end_session();
1714
1715 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url_site', 'Executing handlers' );
1716
1717 $disabled_functions = ini_get( 'disable_functions' );
1718 if ( empty( $disabled_functions ) || ( false === stristr( $disabled_functions, 'curl_multi_exec' ) ) ) {
1719 $mh = @curl_multi_init();
1720 @curl_multi_add_handle( $mh, $ch );
1721
1722 $lastRun = 0;
1723 $running = null;
1724
1725 do {
1726 if ( 20 < time() - $lastRun ) {
1727 MainWP_System_Utility::set_time_limit( $timeout );
1728 $lastRun = time();
1729 }
1730
1731 do {
1732 $mrc = curl_multi_exec( $mh, $running );
1733 } while ( CURLM_CALL_MULTI_PERFORM === $mrc );
1734
1735 $rc = curl_multi_select( $mh, 1.0 );
1736 if ( -1 === $rc ) {
1737 usleep( 100000 );
1738 }
1739
1740 while ( $info = @curl_multi_info_read( $mh ) ) {
1741 $data = @curl_multi_getcontent( $info['handle'] );
1742 $http_status = @curl_getinfo( $info['handle'], CURLINFO_HTTP_CODE );
1743 $err = @curl_error( $info['handle'] );
1744 $real_url = @curl_getinfo( $info['handle'], CURLINFO_EFFECTIVE_URL );
1745
1746 @curl_multi_remove_handle( $mh, $info['handle'] );
1747 curl_close( $info['handle'] );
1748 }
1749 usleep( 10000 );
1750 } while ( $running > 0 );
1751
1752 if ( static::is_valid_curl_handle( $mh ) ) {
1753 @curl_multi_close( $mh );
1754 }
1755 } else {
1756 $data = @curl_exec( $ch );
1757 $http_status = @curl_getinfo( $ch, CURLINFO_HTTP_CODE );
1758 $err = @curl_error( $ch );
1759 $real_url = @curl_getinfo( $ch, CURLINFO_EFFECTIVE_URL );
1760 curl_close( $ch );
1761 }
1762
1763 $host = wp_parse_url( $real_url, PHP_URL_HOST );
1764 $ip = gethostbyname( $host );
1765
1766 MainWP_Execution_Helper::execute_call_track( 'end_point', $website, $postdata, $fetch_track_id, 'fetch site' );
1767
1768 if ( null !== $website ) {
1769 MainWP_DB_Common::instance()->insert_or_update_request_log( $website->id, $ip, null, microtime( true ) );
1770 }
1771
1772 $raw_response = isset( $others['raw_response'] ) && 'yes' === $others['raw_response'] ? true : false;
1773
1774 $hidden_data = '[hidden response data]';
1775
1776 if ( ! apply_filters( 'mainwp_hide_raw_connection_response_data', true ) ) {
1777 $hidden_data = $data;
1778 }
1779
1780 if ( ! is_array( $output ) ) {
1781 $output = array();
1782 }
1783
1784 $output['fetch_data'] = $hidden_data;
1785
1786 $output['http_status'] = (int) $http_status;
1787
1788 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url_site', 'http status: [' . $http_status . '] err: [' . $err . ']' );
1789 if ( '400' === $http_status ) {
1790 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url_site', 'post data: [' . MainWP_Utility::value_to_string( $postdata, 1 ) . ']' );
1791 }
1792
1793 MainWP_Logger::instance()->log_execution_time( 'fetch_url_site :: [url=' . $url . ']' );
1794
1795 $thr_error = null;
1796
1797 if ( in_array( $what, array( 'installplugintheme', 'upgradeplugintheme', 'upgradetranslation', 'upgrade', 'stats', 'renew', 'reconnect' ), true ) ) {
1798 MainWP_Cache_Helper::invalidate_cache_group( MainWP_Cache_Helper::CGR_UPDATES );
1799 MainWP_Cache_Warm_Helper::invalidate_pages_by_site_actions( $what );
1800 }
1801
1802 if ( ( false === $data ) && empty( $http_status ) ) {
1803 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url', '[' . $url . '] HTTP Error: [status=0][' . $err . ']' );
1804 $thr_error = new MainWP_Exception( 'HTTPERROR', $err ); //phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
1805 $output['error_category'] = 'http_error';
1806 } elseif ( empty( $data ) && ! empty( $err ) ) {
1807 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url', '[' . $url . '] HTTP Error: [status=' . $http_status . '][' . $err . ']' );
1808 $thr_error = new MainWP_Exception( 'HTTPERROR', $err ); //phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
1809 $output['error_category'] = 'http_error';
1810 $output['error_code'] = 'http_request_failed';
1811 $output['error_message'] = $err;
1812 } elseif ( 0 < preg_match( '/<mainwp>(.*)<\/mainwp>/', $data, $results ) ) {
1813 $output['connection_step'] = 'verify_credentials';
1814 $result = $results[1];
1815 $information = MainWP_System_Utility::get_child_response( base64_decode( $result ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
1816 unset( $output['fetch_data'] ); // hide the data.
1817 $pdt = is_string( $postdata ) ? $postdata : '';
1818 $data_log = is_array( $postdata ) ? print_r( $postdata, true ) : $pdt; //phpcs:ignore -- good.
1819 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url_site', '[' . $url . '] postdata [' . $data_log . '] information: [OK]' ); //phpcs:ignore -- ok.
1820
1821 $error_code = is_array( $information ) && isset( $information['error_code'] ) ? sanitize_text_field( wp_unslash( $information['error_code'] ) ) : '';
1822 if ( ! empty( $error_code ) ) {
1823 $output['child_error_code'] = $error_code;
1824 }
1825 return $information;
1826 } elseif ( 200 === (int) $http_status && ! empty( $err ) ) {
1827 $thr_error = new MainWP_Exception( 'HTTPERROR', $err ); //phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
1828 $output['error_category'] = 'http_error';
1829 $output['error_code'] = 'http_request_failed';
1830 $output['error_message'] = $err;
1831 } elseif ( $raw_response ) {
1832 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url_site', 'Response: [RAW]' );
1833 return $data;
1834 } else {
1835 MainWP_Logger::instance()->debug_for_website( $website, 'fetch_url', '[' . $url . '] Error: NOMAINWP [data=' . $hidden_data . ']' );
1836 $detect_wsidchk = is_string( $data ) ? strpos( $data, 'wsidchk' ) : false;
1837 if ( false !== $detect_wsidchk ) {
1838 $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.';
1839 $thr_error = new MainWP_Exception( 'ERROR:' . $err_msg, $url );
1840 } else {
1841 $thr_error = new MainWP_Exception( 'NOMAINWP', $url ); //phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
1842 $err_msg = 'Connection Failed. Please ensure that the MainWP Child plugin is installed and activated on the child site.';
1843 }
1844 $output['error_category'] = 'child_plugin_missing';
1845 $output['error_message'] = $err_msg;
1846 }
1847
1848 if ( null !== $thr_error ) {
1849 $thr_error->set_data( $hidden_data ); // to compatible.
1850 throw $thr_error;
1851 }
1852 }
1853
1854 /**
1855 * Method check_constraints()
1856 *
1857 * Check connection delay constraints.
1858 *
1859 * @param mixed $identifier Lock identifier.
1860 * @param mixed $website Object child site.
1861 *
1862 * @uses \MainWP\Dashboard\MainWP_DB_Common::close_open_requests()
1863 * @uses \MainWP\Dashboard\MainWP_DB::get_wp_ip()
1864 * @uses \MainWP\Dashboard\MainWP_Utility::end_session()
1865 */
1866 private static function check_constraints( &$identifier, $website ) { // phpcs:ignore -- NOSONAR -Current complexity is the only way to achieve desired results, pull request solutions appreciated.
1867 $semLock = '103218';
1868 $identifier = static::get_lock_identifier( $semLock );
1869 $minimumDelay = ( ( false === get_option( 'mainwp_minimumDelay' ) ) ? 200 : get_option( 'mainwp_minimumDelay' ) );
1870 if ( 0 < $minimumDelay ) {
1871 $minimumDelay = $minimumDelay / 1000;
1872 }
1873 $minimumIPDelay = ( ( false === get_option( 'mainwp_minimumIPDelay' ) ) ? 1000 : get_option( 'mainwp_minimumIPDelay' ) );
1874 if ( 0 < $minimumIPDelay ) {
1875 $minimumIPDelay = $minimumIPDelay / 1000;
1876 }
1877
1878 MainWP_Utility::end_session();
1879 $delay = true;
1880 while ( $delay ) {
1881 static::lock( $identifier );
1882 if ( 0 < $minimumDelay && static::check_constraints_last_request( $identifier, $minimumDelay ) ) {
1883 continue;
1884 }
1885
1886 if ( 0 < $minimumIPDelay && null !== $website ) {
1887 $ip = MainWP_DB::instance()->get_wp_ip( $website->id );
1888 if ( null !== $ip && '' !== $ip && static::check_constraints_last_request( $identifier, $minimumIPDelay, $ip ) ) {
1889 continue;
1890 }
1891 }
1892 $delay = false;
1893 }
1894
1895 $maximumRequests = ( ( false === get_option( 'mainwp_maximumRequests' ) ) ? 4 : get_option( 'mainwp_maximumRequests' ) );
1896 $maximumIPRequests = ( ( false === get_option( 'mainwp_maximumIPRequests' ) ) ? 1 : get_option( 'mainwp_maximumIPRequests' ) );
1897
1898 $first = true;
1899 $delay = true;
1900 while ( $delay ) {
1901 if ( ! $first ) {
1902 static::lock( $identifier );
1903 } else {
1904 $first = false;
1905 }
1906
1907 MainWP_DB_Common::instance()->close_open_requests();
1908
1909 if ( 0 < $maximumRequests && static::check_constraints_open_requests( $identifier, $maximumRequests ) ) {
1910 continue;
1911 }
1912
1913 if ( 0 < $maximumIPRequests && null !== $website ) {
1914 $ip = MainWP_DB::instance()->get_wp_ip( $website->id );
1915 if ( null !== $ip && '' !== $ip && static::check_constraints_open_requests( $identifier, $maximumIPRequests, $ip ) ) {
1916 continue;
1917 }
1918 }
1919 $delay = false;
1920 }
1921 }
1922
1923 /**
1924 * Method check_constraints_last_request().
1925 *
1926 * Check constraints for last requests.
1927 *
1928 * @param mixed $identifier connect identifier.
1929 * @param int $minimumDelay minimum delay.
1930 * @param string|null $ip ip address.
1931 *
1932 * @uses \MainWP\Dashboard\MainWP_DB_Common::get_last_request_timestamp()
1933 */
1934 private static function check_constraints_last_request( $identifier, $minimumDelay, $ip = null ) {
1935 $lastRequest = MainWP_DB_Common::instance()->get_last_request_timestamp( $ip );
1936 if ( $lastRequest > ( ( microtime( true ) ) - $minimumDelay ) ) {
1937 static::release( $identifier );
1938 $sleep = ( $minimumDelay - ( ( microtime( true ) ) - $lastRequest ) ) * 1000 * 1000;
1939 $sleep = max( 0, intval( $sleep ) );
1940 usleep( $sleep );
1941 return true;
1942 }
1943 return false;
1944 }
1945
1946 /**
1947 * Method check_constraints_open_requests().
1948 *
1949 * Check constraints for open requests.
1950 *
1951 * @param mixed $identifier connect identifier.
1952 * @param int $maximumRequests maximum requests.
1953 * @param string|null $ip ip address.
1954 *
1955 * @uses \MainWP\Dashboard\MainWP_DB_Common::get_nrof_open_requests()
1956 */
1957 private static function check_constraints_open_requests( $identifier, $maximumRequests, $ip = null ) {
1958 $nrOfOpenRequests = MainWP_DB_Common::instance()->get_nrof_open_requests( $ip );
1959 if ( $nrOfOpenRequests >= $maximumRequests ) {
1960 static::release( $identifier );
1961 usleep( 200000 );
1962 return true;
1963 }
1964 return false;
1965 }
1966
1967 /**
1968 * Method download_to_file()
1969 *
1970 * Download to file.
1971 *
1972 * @param mixed $url Download URL.
1973 * @param mixed $file File to download to.
1974 * @param bool $size Size of file.
1975 * @param null $http_user htaccess username.
1976 * @param null $http_pass htaccess password.
1977 *
1978 * @throws MainWP_Exception Exception message.
1979 *
1980 * @uses \MainWP\Dashboard\MainWP_Exception
1981 * @uses \MainWP\Dashboard\MainWP_System::$version
1982 * @uses \MainWP\Dashboard\MainWP_System_Utility::get_wp_file_system()
1983 */
1984 public static function download_to_file( $url, $file, $size = false, $http_user = null, $http_pass = null ) { // phpcs:ignore -- NOSONAR - complex.
1985
1986 $hasWPFileSystem = MainWP_System_Utility::get_wp_file_system();
1987
1988 /**
1989 * WordPress files system object.
1990 *
1991 * @global object
1992 */
1993 global $wp_filesystem;
1994
1995 if ( $wp_filesystem->exists( $file ) && ( ( false === $size ) || ( $wp_filesystem->size( $file ) > $size ) ) ) {
1996 $wp_filesystem->delete( $file );
1997 }
1998
1999 if ( ! $wp_filesystem->exists( dirname( $file ) ) ) {
2000 $wp_filesystem->mkdir( dirname( $file ), 0750 ); // MWP-1558: tightened from 0777; downloaded files may contain backup data.
2001 }
2002
2003 if ( ! $wp_filesystem->exists( dirname( $file ) ) ) {
2004 throw new MainWP_Exception( esc_html__( 'MainWP plugin could not create directory in order to download the file.', 'mainwp' ) );
2005 }
2006
2007 if ( $hasWPFileSystem && ! empty( $wp_filesystem ) ) {
2008 if ( ! $wp_filesystem->is_writable( @dirname( $file ) ) ) {
2009 throw new MainWP_Exception( esc_html__( 'MainWP upload directory is not writable.', 'mainwp' ) );
2010 }
2011 } elseif ( ! is_writable( @dirname( $file ) ) ) { //phpcs:ignore -- ok.
2012 throw new MainWP_Exception( esc_html__( 'MainWP upload directory is not writable.', 'mainwp' ) );
2013 }
2014
2015 $fp = fopen( $file, 'a' );
2016 $agent = 'Mozilla/5.0 (compatible; MainWP/' . MainWP_System::$version . '; +http://mainwp.com)';
2017 if ( false !== $size && $wp_filesystem->exists( $file ) ) {
2018 $size = $wp_filesystem->size( $file );
2019 $url .= '&foffset=' . $size;
2020 }
2021 $ch = curl_init( str_replace( ' ', '%20', $url ) );
2022
2023 $proxy = new \WP_HTTP_Proxy();
2024 if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) {
2025 curl_setopt( $ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP );
2026 curl_setopt( $ch, CURLOPT_PROXY, $proxy->host() );
2027 curl_setopt( $ch, CURLOPT_PROXYPORT, $proxy->port() );
2028
2029 if ( $proxy->use_authentication() ) {
2030 curl_setopt( $ch, CURLOPT_PROXYAUTH, CURLAUTH_ANY );
2031 curl_setopt( $ch, CURLOPT_PROXYUSERPWD, $proxy->authentication() );
2032 }
2033 }
2034 curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
2035 curl_setopt( $ch, CURLOPT_HEADER, false );
2036 curl_setopt( $ch, CURLOPT_FILE, $fp );
2037
2038 curl_setopt( $ch, CURLOPT_USERAGENT, $agent );
2039 curl_setopt( $ch, CURLOPT_ENCODING, 'none' );
2040
2041 curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, true );
2042 if ( ! empty( $http_user ) && ! empty( $http_pass ) ) {
2043 $http_pass = stripslashes( $http_pass );
2044 curl_setopt( $ch, CURLOPT_USERPWD, "$http_user:$http_pass" );
2045 }
2046 curl_exec( $ch );
2047 if ( static::is_valid_curl_handle( $ch ) ) {
2048 curl_close( $ch );
2049 }
2050 fclose( $fp );
2051 }
2052
2053 /**
2054 * Method init_coockiesdir()
2055 *
2056 * Check for cookies directory and create it if it doesn't already exist,
2057 * set the file permissions and update htaccess.
2058 *
2059 * @param mixed $cookieDir Cookies directory.
2060 *
2061 * @return void
2062 *
2063 * @uses \MainWP\Dashboard\MainWP_System_Utility::get_wp_file_system()
2064 */
2065 public static function init_cookiesdir( $cookieDir ) {
2066
2067 $hasWPFileSystem = MainWP_System_Utility::get_wp_file_system();
2068
2069 /**
2070 * WordPress files system object.
2071 *
2072 * @global object
2073 */
2074 global $wp_filesystem;
2075
2076 if ( $hasWPFileSystem && ! empty( $wp_filesystem ) ) {
2077
2078 if ( ! $wp_filesystem->is_dir( $cookieDir ) ) {
2079 $wp_filesystem->mkdir( $cookieDir, 0750 ); // MWP-1558: tightened from 0777; cookies/ holds child wp-admin session cookies.
2080 }
2081
2082 if ( ! file_exists( $cookieDir . '/.htaccess' ) ) {
2083 $file_htaccess = $cookieDir . '/.htaccess';
2084 $wp_filesystem->put_contents( $file_htaccess, 'deny from all' );
2085 }
2086
2087 if ( ! file_exists( $cookieDir . '/index.php' ) ) {
2088 $file_index = $cookieDir . '/index.php';
2089 $wp_filesystem->touch( $file_index );
2090 }
2091 } else {
2092
2093 if ( ! file_exists( $cookieDir ) ) {
2094 @mkdir( $cookieDir, 0750, true ); // MWP-1558: tightened from 0777; cookies/ holds child wp-admin session cookies.
2095 }
2096
2097 if ( ! file_exists( $cookieDir . '/.htaccess' ) ) {
2098 $file_htaccess = @fopen( $cookieDir . '/.htaccess', 'w+' );
2099 @fwrite( $file_htaccess, 'deny from all' );
2100 @fclose( $file_htaccess );
2101 }
2102
2103 if ( ! file_exists( $cookieDir . '/index.php' ) ) {
2104 $file_index = @fopen( $cookieDir . '/index.php', 'w+' );
2105 @fclose( $file_index );
2106 }
2107 }
2108 }
2109
2110 /**
2111 * Method get_file_content()
2112 *
2113 * Get contents of file.
2114 *
2115 * @param mixed $url File Location.
2116 *
2117 * @return mixed false|$data
2118 *
2119 * @uses \MainWP\Dashboard\MainWP_System::$version
2120 */
2121 public static function get_file_content( $url ) {
2122 $agent = 'Mozilla/5.0 (compatible; MainWP/' . MainWP_System::$version . '; +http://mainwp.com)';
2123 $ch = curl_init();
2124
2125 $proxy = new \WP_HTTP_Proxy();
2126 if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) {
2127 curl_setopt( $ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP );
2128 curl_setopt( $ch, CURLOPT_PROXY, $proxy->host() );
2129 curl_setopt( $ch, CURLOPT_PROXYPORT, $proxy->port() );
2130
2131 if ( $proxy->use_authentication() ) {
2132 curl_setopt( $ch, CURLOPT_PROXYAUTH, CURLAUTH_ANY );
2133 curl_setopt( $ch, CURLOPT_PROXYUSERPWD, $proxy->authentication() );
2134 }
2135 }
2136
2137 curl_setopt( $ch, CURLOPT_HEADER, 0 );
2138 curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
2139 curl_setopt( $ch, CURLOPT_URL, $url );
2140 curl_setopt( $ch, CURLOPT_USERAGENT, $agent );
2141 curl_setopt( $ch, CURLOPT_ENCODING, 'none' );
2142
2143 $data = @curl_exec( $ch );
2144 $httpCode = @curl_getinfo( $ch, CURLINFO_HTTP_CODE );
2145 if ( static::is_valid_curl_handle( $ch ) ) {
2146 curl_close( $ch );
2147 }
2148 if ( 200 === (int) $httpCode ) {
2149 return $data;
2150 } else {
2151 return false;
2152 }
2153 }
2154
2155 /**
2156 * Method is_valid_curl_handle
2157 *
2158 * @param mixed $ch cURL handle to validate.
2159 * @return bool Valid curl handle.
2160 */
2161 public static function is_valid_curl_handle( $ch ) {
2162 return is_resource( $ch )
2163 || ( is_object( $ch )
2164 && class_exists( 'CurlHandle', false )
2165 && $ch instanceof \CurlHandle
2166 );
2167 }
2168
2169 /**
2170 * Method get_favico_url()
2171 *
2172 * Get Child Site favicon URL.
2173 *
2174 * @param mixed $website Child Site info.
2175 *
2176 * @return mixed $faviurl Favicon URL.
2177 *
2178 * @uses \MainWP\Dashboard\MainWP_DB::get_website_option()
2179 * @uses \MainWP\Dashboard\MainWP_System_Utility::get_icons_dir()
2180 * @uses \MainWP\Dashboard\MainWP_Utility::remove_http_prefix()
2181 */
2182 public static function get_favico_url( $website ) {
2183 $favi = MainWP_DB::instance()->get_website_option( $website, 'favi_icon', '' );
2184 $faviurl = '';
2185
2186 if ( ! empty( $favi ) ) {
2187 if ( false !== strpos( $favi, 'favi-' . intval( $website->id ) . '-' ) ) {
2188 $dirs = MainWP_System_Utility::get_icons_dir();
2189 if ( file_exists( $dirs[0] . $favi ) ) {
2190 $faviurl = $dirs[1] . $favi;
2191 } else {
2192 $faviurl = '';
2193 }
2194 } elseif ( ( 0 === strpos( $favi, '//' ) ) || ( 0 === strpos( $favi, 'http' ) ) ) {
2195 $faviurl = $favi;
2196 } else {
2197 $faviurl = $website->url . $favi;
2198 $faviurl = MainWP_Utility::remove_http_prefix( $faviurl );
2199 }
2200 }
2201
2202 if ( empty( $faviurl ) ) {
2203 $faviurl = false;
2204 }
2205
2206 return $faviurl;
2207 }
2208 }
2209