PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / trunk
MainWP Dashboard: Self-hosted WordPress Management for Agencies vtrunk
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-premium-update.php

class-mainwp-premium-update.php in MainWP Dashboard: Self-hosted WordPress Management for Agencies trunk, at class/class-mainwp-premium-update.php

512 lines 19.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * MainWP Premium Update
4 *
5 * MainWP Premium Update 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 // phpcs:disable WordPress.DB.RestrictedFunctions, WordPress.WP.AlternativeFunctions, WordPress.PHP.NoSilencedErrors -- Using cURL functions.
18
19 /**
20 * Class MainWP_Premium_Update
21 *
22 * @package MainWP\Dashboard
23 *
24 * Check for premium plugin updates.
25 */
26 class MainWP_Premium_Update { // phpcs:ignore Generic.Classes.OpeningBraceSameLine.ContentAfterBrace -- NOSONAR.
27
28 /**
29 * Raw response of the most recent premium request-update GET, if any.
30 *
31 * Captured so fetch_url_authed() can report the child's real result instead
32 * of fabricating success (MWP-1660). Reset per maybe_request_premium_updates() call.
33 *
34 * @var array|\WP_Error|null
35 */
36 private static $last_request_response = null;
37
38 /**
39 * Method get_class_name()
40 *
41 * Get Class Name.
42 *
43 * @return object
44 */
45 public static function get_class_name() {
46 return __CLASS__;
47 }
48
49 /**
50 * Method check_premium_updates()
51 *
52 * Check for Premium Plugin updates.
53 *
54 * @param array $updates Array of installed items reported by the child site.
55 * @param mixed $type Type of update.
56 *
57 * @return boolean true|false.
58 */
59 public static function check_premium_updates( $updates, $type ) { // phpcs:ignore -- NOSONAR - complex.
60
61 if ( ! is_array( $updates ) || empty( $updates ) ) {
62 return false;
63 }
64
65 if ( 'plugin' === $type ) {
66
67 $premiums = MainWP_Premium_Update_Registry::get_filter_defaults( 'plugin', 'detect' );
68
69 /**
70 * Filter: mainwp_detect_premiums_updates
71 *
72 * Use mainwp_detect_premium_plugins_update instead.
73 *
74 * @deprecated
75 */
76 $premiums = apply_filters( 'mainwp_detect_premiums_updates', $premiums );
77
78 /**
79 * Filter: mainwp_detect_premium_plugins_update
80 *
81 * Filters supported premium plugins to fix compatiblity issues with detecting premium plugin updates.
82 *
83 * @since Unknown
84 */
85 $premiums = apply_filters( 'mainwp_detect_premium_plugins_update', $premiums );
86
87 $prefixes = MainWP_Premium_Update_Registry::get_prefixes( 'plugin', 'detect' );
88
89 if ( ( is_array( $premiums ) && ! empty( $premiums ) ) || ! empty( $prefixes ) ) {
90 foreach ( $updates as $inventory_key => $info ) {
91 $identifier = MainWP_Premium_Update_Registry::get_inventory_identifier( $inventory_key, $info );
92 if ( '' !== $identifier && MainWP_Premium_Update_Registry::slug_matches( $identifier, is_array( $premiums ) ? $premiums : array(), $prefixes ) ) {
93 return true;
94 }
95 }
96 }
97 } elseif ( 'theme' === $type ) {
98
99 $premiums = MainWP_Premium_Update_Registry::get_filter_defaults( 'theme', 'detect' );
100
101 /**
102 * Filter: mainwp_detect_premium_themes_update
103 *
104 * Filters supported premium themes to fix compatiblity issues with detecting premium theme updates.
105 *
106 * @since Unknown
107 */
108 $premiums = apply_filters( 'mainwp_detect_premium_themes_update', $premiums );
109
110 $prefixes = MainWP_Premium_Update_Registry::get_prefixes( 'theme', 'detect' );
111
112 if ( ( is_array( $premiums ) && ! empty( $premiums ) ) || ! empty( $prefixes ) ) {
113 foreach ( $updates as $inventory_key => $info ) {
114 $identifier = MainWP_Premium_Update_Registry::get_inventory_identifier( $inventory_key, $info );
115 if ( '' === $identifier ) {
116 continue;
117 }
118 // A theme updater only runs while its theme (or a child of it) is active,
119 // so the extra checks are limited to the active theme and its parent.
120 // Older child plugins that do not report activity flags keep the
121 // previous match-any-installed behavior.
122 if ( ( isset( $info['active'] ) || isset( $info['parent_active'] ) )
123 && empty( $info['active'] ) && empty( $info['parent_active'] ) ) {
124 continue;
125 }
126 if ( MainWP_Premium_Update_Registry::slug_matches( $identifier, is_array( $premiums ) ? $premiums : array(), $prefixes ) ) {
127 return true;
128 }
129 }
130 }
131 }
132
133 return false;
134 }
135
136 /**
137 * Method maybe_request_premium_updates()
138 *
139 * @param mixed $website Child Site info.
140 * @param mixed $what stats|upgradeplugintheme What function to perform.
141 * @param mixed $params plugin|theme Update Type.
142 * @param mixed $output Output result.
143 *
144 * @return mixed $request_update
145 */
146 public static function maybe_request_premium_updates( $website, $what, $params, &$output ) { // phpcs:ignore -- NOSONAR -Current complexity is the only way to achieve desired results, pull request solutions appreciated.
147 self::$last_request_response = null;
148
149 if ( 'stats' === $what || ( 'upgradeplugintheme' === $what && isset( $params['type'] ) ) ) {
150
151 $update_type = '';
152
153 $check_premi_plugins = array();
154 $check_premi_themes = array();
155
156 if ( 'stats' === $what ) {
157 if ( '' !== $website->plugins ) {
158 $check_premi_plugins = json_decode( $website->plugins, 1 );
159 }
160 if ( '' !== $website->themes ) {
161 $check_premi_themes = json_decode( $website->themes, 1 );
162 }
163 } elseif ( 'upgradeplugintheme' === $what ) {
164 $update_type = ( isset( $params['type'] ) ) ? $params['type'] : '';
165 if ( 'plugin' === $update_type ) {
166 if ( '' !== $website->plugins ) {
167 $check_premi_plugins = json_decode( $website->plugins, 1 );
168 }
169 } elseif ( 'theme' === $update_type ) {
170 if ( '' !== $website->themes ) {
171 $check_premi_themes = json_decode( $website->themes, 1 );
172 }
173 }
174 }
175
176 if ( static::check_premium_updates( $check_premi_plugins, 'plugin' ) ) {
177 static::try_to_detect_premiums_update( $website, 'plugin' );
178 }
179
180 if ( static::check_premium_updates( $check_premi_themes, 'theme' ) ) {
181 static::try_to_detect_premiums_update( $website, 'theme' );
182 }
183
184 if ( 'upgradeplugintheme' === $what && ( 'plugin' === $update_type || 'theme' === $update_type ) && ( static::check_request_update_premium( $params['list'], $update_type ) || static::check_premium_updates( explode( ',', $params['list'] ), $update_type ) ) ) {
185 $output = static::request_premiums_update( $website, $update_type, $params['list'] );
186 return true;
187 }
188 }
189
190 return false;
191 }
192
193 /**
194 * Method check_request_update_premium()
195 *
196 * Check if any updates are on the premiums list.
197 *
198 * @param array $list_items List of updates.
199 * @param string $type Type of update. plugin|theme.
200 *
201 * @return bool true|false.
202 */
203 public static function check_request_update_premium( $list_items, $type ) { // phpcs:ignore -- NOSONAR - complex.
204
205 $updates = explode( ',', $list_items );
206
207 if ( ! is_array( $updates ) || empty( $updates ) ) {
208 return false;
209 }
210
211 if ( 3 < count( $updates ) ) {
212 return false;
213 }
214
215 if ( 'plugin' === $type ) {
216
217 $update_premiums = MainWP_Premium_Update_Registry::get_filter_defaults( 'plugin', 'request' );
218
219 /**
220 * Filter: mainwp_request_update_premium_plugins
221 *
222 * Filters supported premium plugins to fix compatibility problmes with updating premium plugins.
223 *
224 * @since Unknown
225 */
226 $update_premiums = apply_filters( 'mainwp_request_update_premium_plugins', $update_premiums );
227
228 $prefixes = MainWP_Premium_Update_Registry::get_prefixes( 'plugin', 'request' );
229
230 foreach ( $updates as $slug ) {
231 if ( ! empty( $slug ) && MainWP_Premium_Update_Registry::slug_matches( $slug, is_array( $update_premiums ) ? $update_premiums : array(), $prefixes ) ) {
232 return true;
233 }
234 }
235 } elseif ( 'theme' === $type ) {
236
237 $update_premiums = MainWP_Premium_Update_Registry::get_filter_defaults( 'theme', 'request' );
238
239 /**
240 * Filter: mainwp_request_update_premium_themes
241 *
242 * Filters supported premium themes to fix compatibility problmes with updating premium themes.
243 *
244 * @since Unknown
245 */
246 $update_premiums = apply_filters( 'mainwp_request_update_premium_themes', $update_premiums );
247
248 $prefixes = MainWP_Premium_Update_Registry::get_prefixes( 'theme', 'request' );
249
250 foreach ( $updates as $slug ) {
251 if ( ! empty( $slug ) && MainWP_Premium_Update_Registry::slug_matches( $slug, is_array( $update_premiums ) ? $update_premiums : array(), $prefixes ) ) {
252 return true;
253 }
254 }
255 }
256 return false;
257 }
258
259 /**
260 * Method redirect_request_site()
261 *
262 * Redirect to requested Site.
263 *
264 * Deprecated, see function handle_premium_update_actions().
265 *
266 * @param mixed $website Child Site.
267 * @param mixed $where_url page to redirerct to.
268 *
269 * @uses \MainWP\Dashboard\MainWP_Connect::get_get_data_authed()
270 * @uses \MainWP\Dashboard\MainWP_Logger::debug()
271 * @uses \MainWP\Dashboard\MainWP_System::$version
272 */
273 public static function redirect_request_site( $website, $where_url ) {
274
275 $request_url = MainWP_Connect::get_get_data_authed( $website, $where_url );
276
277 $agent = 'Mozilla/5.0 (compatible; MainWP/' . MainWP_System::$version . '; +http://mainwp.com)';
278 $args = array(
279 'timeout' => 25,
280 'httpversion' => '1.1',
281 'User-Agent' => $agent,
282 'sslverify' => static::get_ssl_verify( $website ),
283 );
284
285 if ( ! empty( $website->http_user ) && ! empty( $website->http_pass ) ) {
286 // MWP-1548: decrypt before constructing the Basic Auth header.
287 $http_user_plain = MainWP_Credential_Storage::decrypt_credential( $website->http_user );
288 $http_pass_plain = MainWP_Credential_Storage::decrypt_credential( $website->http_pass );
289 if ( ! empty( $http_user_plain ) && ! empty( $http_pass_plain ) ) {
290 $args['headers'] = array(
291 'Authorization' => 'Basic ' . base64_encode( $http_user_plain . ':' . stripslashes( $http_pass_plain ) ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
292 );
293 }
294 }
295
296 $ssl_version = isset( $website->ssl_version ) ? (int) $website->ssl_version : 0;
297 $force_use_ipv4 = static::get_force_use_ipv4( $website );
298
299 // MWP-1660: honor the site's connection profile, mirroring fetch_url().
300 $curl_extra = function ( $handle ) use ( $ssl_version, $force_use_ipv4 ) {
301 if ( ! empty( $ssl_version ) ) {
302 curl_setopt( $handle, CURLOPT_SSLVERSION, $ssl_version );
303 }
304 if ( $force_use_ipv4 && defined( 'CURLOPT_IPRESOLVE' ) && defined( 'CURL_IPRESOLVE_V4' ) ) {
305 curl_setopt( $handle, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4 );
306 }
307 };
308
309 add_action( 'http_api_curl', $curl_extra );
310 try {
311 MainWP_Logger::instance()->debug( ' :: tryRequest :: [website=' . $website->url . ']' );
312 $response = wp_remote_get( $request_url, $args );
313 } finally {
314 remove_action( 'http_api_curl', $curl_extra );
315 }
316
317 static::log_request_outcome( $website, $where_url, $response );
318
319 return $response;
320 }
321
322
323 /**
324 * Method handle_premium_update_actions()
325 *
326 * Redirect to requested Site.
327 *
328 * @since 6.2
329 *
330 * @param mixed $website Child Site.
331 * @param array $params Other params.
332 *
333 * @return mixed false|array information.
334 */
335 public static function handle_premium_update_actions( $website, $params = array() ) {
336 if ( ! is_array( $params ) ) {
337 return false;
338 }
339 MainWP_Logger::instance()->debug( 'Request premium update :: [siteid=' . $website->id . '] :: [type=' . $params['premium_type'] . '] :: [perform=' . $params['premium_perform'] . ']' );
340 try {
341 return MainWP_Connect::fetch_url_authed( $website, 'process_premium_updates', $params, false, false, true, null, true );
342 } catch ( \Exception $e ) {
343 // Just ignore.
344 $err = $e->getMessage(); //phpcs:ignore -- NOSONAR -for debug.
345 }
346 return false;
347 }
348
349 /**
350 * Resolve the sslverify argument for a website, mirroring fetch_url() semantics.
351 *
352 * @param mixed $website Child Site.
353 *
354 * @return bool
355 */
356 private static function get_ssl_verify( $website ) {
357 $verify = isset( $website->verify_certificate ) ? (int) $website->verify_certificate : 2;
358 if ( 1 === $verify ) {
359 return true;
360 }
361 if ( 0 === $verify ) {
362 return false;
363 }
364 return ( false === get_option( 'mainwp_sslVerifyCertificate' ) ) || ( 1 === (int) get_option( 'mainwp_sslVerifyCertificate' ) );
365 }
366
367 /**
368 * Resolve the force-IPv4 flag for a website, mirroring fetch_url() semantics.
369 *
370 * @param mixed $website Child Site.
371 *
372 * @return bool
373 */
374 private static function get_force_use_ipv4( $website ) {
375 $force = isset( $website->force_use_ipv4 ) && null !== $website->force_use_ipv4 ? (int) $website->force_use_ipv4 : null;
376 if ( 1 === $force ) {
377 return true;
378 }
379 if ( null === $force || 2 === $force ) {
380 return 1 === (int) get_option( 'mainwp_forceUseIPv4' );
381 }
382 return false;
383 }
384
385 /**
386 * Log the outcome of a premium detect/request GET and record failures per site.
387 *
388 * These requests previously discarded their responses entirely, leaving
389 * support blind to sites where the premium checks silently fail (MWP-1660).
390 *
391 * @param mixed $website Child Site.
392 * @param string $where_url Requested wp-admin location.
393 * @param array|\WP_Error|mixed $response HTTP response.
394 */
395 private static function log_request_outcome( $website, $where_url, $response ) {
396 $error = '';
397 if ( is_wp_error( $response ) ) {
398 $error = $response->get_error_message();
399 } else {
400 $code = (int) wp_remote_retrieve_response_code( $response );
401 if ( $code < 200 || $code >= 400 ) {
402 $error = 'HTTP ' . $code;
403 }
404 }
405
406 if ( '' === $error ) {
407 MainWP_Logger::instance()->debug_for_website( $website, 'premium_update', '[where=' . $where_url . '] :: ok' );
408 return;
409 }
410
411 MainWP_Logger::instance()->warning_for_website( $website, 'premium_update', '[where=' . $where_url . '] :: ' . $error, false );
412
413 $count = (int) MainWP_DB::instance()->get_website_option( $website, 'premium_updates_error_count' );
414 MainWP_DB::instance()->update_website_option( $website, 'premium_updates_error_count', (string) ( $count + 1 ) );
415 MainWP_DB::instance()->update_website_option(
416 $website,
417 'premium_updates_last_error',
418 wp_json_encode(
419 array(
420 'time' => time(),
421 'where' => $where_url,
422 'error' => $error,
423 )
424 )
425 );
426 }
427
428 /**
429 * Method request_premiums_update()
430 *
431 * Request to update plugin or theme.
432 *
433 * @param mixed $website Child Site to update.
434 * @param mixed $type Type of update, plugin|theme.
435 * @param mixed $list_items list of plugins & themes installed.
436 *
437 * @return mixed
438 */
439 public static function request_premiums_update( $website, $type, $list_items ) {
440 if ( ! in_array( $type, array( 'plugin', 'theme' ) ) ) {
441 return false;
442 }
443 $params = array(
444 'premium_type' => $type,
445 'premium_perform' => 'premium_update',
446 'list' => $list_items,
447 );
448 return static::handle_premium_update_actions( $website, $params );
449 }
450
451 /**
452 * Parse the child's `<mainwp>` result envelope out of an HTML response body.
453 *
454 * The premium request route runs the regular upgradeplugintheme callable
455 * mid-render of a wp-admin page; the callable terminates via
456 * MainWP_Helper::write(), so its result envelope is embedded in the page
457 * output. Returns null when no valid envelope is present (e.g. timeout).
458 *
459 * @param string $body Response body.
460 *
461 * @return array|null Decoded information array or null.
462 */
463 public static function parse_mainwp_envelope( $body ) {
464 if ( ! is_string( $body ) || '' === $body ) {
465 return null;
466 }
467 if ( ! preg_match( '/<mainwp>(.*)<\/mainwp>/', $body, $results ) ) {
468 return null;
469 }
470 $information = MainWP_System_Utility::get_child_response( base64_decode( $results[1] ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for backwards compatibility.
471 return is_array( $information ) ? $information : null;
472 }
473
474 /**
475 * Get the parsed result of the most recent premium request-update GET.
476 *
477 * Used by fetch_url_authed() to report the child's real result. Returns null
478 * when the request timed out or produced no parsable envelope; callers keep
479 * the previous optimistic behavior in that case (MWP-1660).
480 *
481 * @return array|null
482 */
483 public static function get_last_parsed_response() {
484 if ( null === self::$last_request_response || is_wp_error( self::$last_request_response ) ) {
485 return null;
486 }
487 return static::parse_mainwp_envelope( wp_remote_retrieve_body( self::$last_request_response ) );
488 }
489
490 /**
491 * Method try_to_detect_premiums_update()
492 *
493 * Try to detect if pugin and themes are premium.
494 *
495 * @param mixed $website Child Site.
496 * @param mixed $type Type of update, plugin|theme.
497 *
498 * @return mixed false|static::redirect_request_site()
499 */
500 public static function try_to_detect_premiums_update( $website, $type ) {
501
502 if ( ! in_array( $type, array( 'plugin', 'theme' ), true ) ) {
503 return null;
504 }
505 $params = array(
506 'premium_type' => $type,
507 'premium_perform' => 'detect_update',
508 );
509 static::handle_premium_update_actions( $website, $params );
510 }
511 }
512