PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.3-beta
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
← All changes | vendor/automattic/jetpack-plans/src/class-current-plan.php +217 -36 12.0.3 → 16.3-beta View file →
@@ -1,17 +1,15 @@
1 1 <?php
2 2 /**
3 3 * Handles fetching of the site's plan and products from WordPress.com and caching values locally.
4 4 *
5 - * This file was copied and adapted from the Jetpack plugin on Mar 2022
6 - *
7 - * @package automattic/jetpack
5 + * @package automattic/jetpack-plans
8 6 */
9 7
10 8 namespace Automattic\Jetpack;
11 9
12 10 use Automattic\Jetpack\Connection\Client;
13 -use Jetpack_Options;
11 +use Automattic\Jetpack\Connection\Manager;
14 12
15 13 /**
16 14 * Provides methods methods for fetching the site's plan and products from WordPress.com.
17 15 */
@@ -23,8 +21,25 @@
23 21 */
24 22 private static $active_plan_cache;
25 23
26 24 /**
25 + * Simple Site-specific features available.
26 + * Their calculation can be expensive and slow, so we're caching it for the request.
27 + *
28 + * @var array Site-specific features
29 + */
30 + private static $simple_site_specific_features = array();
31 +
32 + /**
33 + * Atomic site-specific features available, cached for the request alongside the Simple ones.
34 + *
35 + * Not keyed on a blog ID: an Atomic site can only ever answer for itself.
36 + *
37 + * @var array|null Site-specific features.
38 + */
39 + private static $atomic_site_specific_features = null;
40 +
41 + /**
27 42 * The name of the option that will store the site's plan.
28 43 *
29 44 * @var string
30 45 */
@@ -42,18 +57,25 @@
42 57 'plans' => array(
43 58 'jetpack_free',
44 59 ),
45 60 'supports' => array(
61 + 'advanced-seo',
46 62 'opentable',
47 63 'calendly',
48 64 'send-a-message',
65 + 'sharing-block',
49 66 'whatsapp-button',
50 67 'social-previews',
51 68 'videopress',
69 + 'videopress/video',
70 + 'v6-video-frame-poster',
52 71
53 72 'core/video',
54 73 'core/cover',
55 74 'core/audio',
75 + 'multistep-form',
76 + 'form-webhooks',
77 + 'form-conditional-logic',
56 78 ),
57 79 ),
58 80 'personal' => array(
59 81 'plans' => array(
@@ -61,15 +83,14 @@
61 83 'jetpack_personal_monthly',
62 84 'personal-bundle',
63 85 'personal-bundle-monthly',
64 86 'personal-bundle-2y',
87 + 'personal-bundle-3y',
65 88 'starter-plan',
66 89 ),
67 90 'supports' => array(
68 91 'akismet',
69 92 'payments',
70 - 'recurring-payments',
71 - 'premium-content/container',
72 93 'videopress',
73 94 ),
74 95 ),
75 96 'premium' => array(
@@ -78,11 +99,14 @@
78 99 'jetpack_premium_monthly',
79 100 'value_bundle',
80 101 'value_bundle-monthly',
81 102 'value_bundle-2y',
103 + 'value_bundle-3y',
104 + 'jetpack_creator_yearly',
105 + 'jetpack_creator_bi_yearly',
106 + 'jetpack_creator_monthly',
82 107 ),
83 108 'supports' => array(
84 - 'donations',
85 109 'simple-payments',
86 110 'vaultpress',
87 111 'videopress',
88 112 'republicize',
@@ -107,14 +131,26 @@
107 131 'jetpack_business_monthly',
108 132 'business-bundle',
109 133 'business-bundle-monthly',
110 134 'business-bundle-2y',
135 + 'business-bundle-3y',
111 136 'ecommerce-bundle',
112 137 'ecommerce-bundle-monthly',
113 138 'ecommerce-bundle-2y',
139 + 'ecommerce-bundle-3y',
114 140 'pro-plan',
141 + 'wp_bundle_migration_trial_monthly',
142 + 'wp_bundle_hosting_trial_monthly',
143 + 'ecommerce-trial-bundle-monthly',
144 + 'wooexpress-small-bundle-yearly',
145 + 'wooexpress-small-bundle-monthly',
146 + 'wooexpress-medium-bundle-yearly',
147 + 'wooexpress-medium-bundle-monthly',
148 + 'wp_com_hundred_year_bundle_centennially',
115 149 ),
116 - 'supports' => array(),
150 + 'supports' => array(
151 + 'ai-seo-enhancer',
152 + ),
117 153 ),
118 154
119 155 'complete' => array(
120 156 'plans' => array(
@@ -121,9 +157,12 @@
121 157 'jetpack_complete',
122 158 'jetpack_complete_monthly',
123 159 'vip',
124 160 ),
125 - 'supports' => array(),
161 + 'supports' => array(
162 + 'field-file', // Forms
163 + 'social-image-generator',
164 + ),
126 165 ),
127 166 );
128 167
129 168 /**
@@ -143,33 +182,42 @@
143 182 if ( is_wp_error( $body ) ) {
144 183 return false;
145 184 }
146 185
147 - // Decode the results.
148 - $results = json_decode( $body, true );
186 + return self::update_from_site_record( json_decode( $body, true ) );
187 + }
149 188
150 - if ( ! is_array( $results ) ) {
189 + /**
190 + * Given a decoded `/sites/%d` record, attempt to set the site's plan and products from it.
191 + *
192 + * @since 0.12.0
193 + *
194 + * @param array $record The decoded site record from the WordPress.com `/sites/%d` endpoint.
195 + * @return bool Was the plan successfully updated?
196 + */
197 + public static function update_from_site_record( $record ) {
198 + if ( ! is_array( $record ) ) {
151 199 return false;
152 200 }
153 201
154 - if ( isset( $results['products'] ) ) {
202 + if ( isset( $record['products'] ) ) {
155 203 // Store the site's products in an option and return true if updated.
156 - self::store_data_in_option( self::SITE_PRODUCTS_OPTION, $results['products'] );
204 + self::store_data_in_option( self::SITE_PRODUCTS_OPTION, $record['products'] );
157 205 }
158 206
159 - if ( ! isset( $results['plan'] ) ) {
207 + if ( ! isset( $record['plan'] ) ) {
160 208 return false;
161 209 }
162 210
163 211 $current_plan = get_option( self::PLAN_OPTION, array() );
164 212
165 - if ( ! empty( $current_plan ) && $current_plan === $results['plan'] ) {
213 + if ( ! empty( $current_plan ) && $current_plan === $record['plan'] ) {
166 214 // Bail if the plans array hasn't changed.
167 215 return false;
168 216 }
169 217
170 218 // Store the new plan in an option and return true if updated.
171 - $result = self::store_data_in_option( self::PLAN_OPTION, $results['plan'] );
219 + $result = self::store_data_in_option( self::PLAN_OPTION, $record['plan'] );
172 220
173 221 if ( $result ) {
174 222 // Reset the cache since we've just updated the plan.
175 223 self::$active_plan_cache = null;
@@ -187,15 +235,23 @@
187 235 */
188 236 private static function store_data_in_option( $option, $data ) {
189 237 $result = update_option( $option, $data, true );
190 238
191 - // If something goes wrong with the update, so delete the current option and then update it.
192 - if ( ! $result ) {
193 - delete_option( $option );
194 - $result = update_option( $option, $data, true );
239 + if ( $result ) {
240 + return true;
195 241 }
196 242
197 - return $result;
243 + // update_option() also reports false when the stored value already matches, which is not a
244 + // failure. Both options are autoloaded, so reading it as one rewrites them on every
245 + // unchanged fetch and drops the alloptions cache with it.
246 + if ( get_option( $option ) === $data ) {
247 + return true;
248 + }
249 +
250 + // If the update genuinely failed, delete the option and write it again.
251 + delete_option( $option );
252 +
253 + return update_option( $option, $data, true );
198 254 }
199 255
200 256 /**
201 257 * Make an API call to WordPress.com for plan status
@@ -206,15 +262,44 @@
206 262 *
207 263 * @access public
208 264 * @static
209 265 *
266 + * @since 0.14.0 Accepts request arguments.
267 + *
268 + * @param array $args Request arguments, as accepted by `Client::wpcom_json_api_request_as_blog()`.
269 + * A caller refreshing in front of a page render can cap `timeout` here; the
270 + * `http_request_timeout` filter cannot, because the client always sends one.
210 271 * @return bool True if plan is updated, false if no update
211 272 */
212 - public static function refresh_from_wpcom() {
273 + public static function refresh_from_wpcom( $args = array() ) {
274 + // Also registered as an action callback, and `do_action()` hands those an empty string.
275 + if ( ! is_array( $args ) ) {
276 + $args = array();
277 + }
278 +
279 + $site_id = Manager::get_site_id();
280 + if ( is_wp_error( $site_id ) ) {
281 + return false;
282 + }
283 +
213 284 // Make the API request.
214 285
215 - $response = Client::wpcom_json_api_request_as_blog( sprintf( '/sites/%d', Jetpack_Options::get_option( 'id' ) ) . '?force=wpcom', '1.1' );
216 - return self::update_from_sites_response( $response );
286 + $response = Client::wpcom_json_api_request_as_blog(
287 + sprintf( '/sites/%d?force=wpcom', $site_id ),
288 + '1.1',
289 + $args
290 + );
291 +
292 + $updated = self::update_from_sites_response( $response );
293 +
294 + // The shared site record cache can still hold a record older than this response, and a
295 + // cached read stores the plan again. Dropping it keeps that older record from reverting
296 + // what this fetch just stored.
297 + if ( ! is_wp_error( $response ) ) {
298 + Manager::delete_cached_site_data();
299 + }
300 +
301 + return $updated;
217 302 }
218 303
219 304 /**
220 305 * Get the plan that this Jetpack site is currently using.
@@ -247,19 +332,17 @@
247 332 );
248 333
249 334 list( $plan['class'], $supports ) = self::get_class_and_features( $plan['product_slug'] );
250 335
251 - // get available features if Jetpack is active.
252 - if ( class_exists( 'Jetpack' ) ) {
253 - foreach ( \Jetpack::get_available_modules() as $module_slug ) {
254 - $module = \Jetpack::get_module( $module_slug );
255 - if ( ! isset( $module ) || ! is_array( $module ) ) {
256 - continue;
257 - }
258 - if ( in_array( 'free', $module['plan_classes'], true ) || in_array( $plan['class'], $module['plan_classes'], true ) ) {
259 - $supports[] = $module_slug;
260 - }
336 + $modules = new Modules();
337 + foreach ( $modules->get_available() as $module_slug ) {
338 + $module = $modules->get( $module_slug );
339 + if ( ! isset( $module ) || ! is_array( $module ) ) {
340 + continue;
261 341 }
342 + if ( in_array( 'free', $module['plan_classes'], true ) || in_array( $plan['class'], $module['plan_classes'], true ) ) {
343 + $supports[] = $module_slug;
344 + }
262 345 }
263 346
264 347 $plan['supports'] = $supports;
265 348
@@ -317,9 +400,9 @@
317 400
318 401 /**
319 402 * Determine whether the active plan supports a particular feature
320 403 *
321 - * @uses Jetpack_Plan::get()
404 + * @uses self::get()
322 405 *
323 406 * @access public
324 407 * @static
325 408 *
@@ -352,8 +435,13 @@
352 435 if ( 'videopress' === $feature ) {
353 436 return true;
354 437 }
355 438
439 + // As of 05 2023 - all plans support Earn features (minus 'simple-payments').
440 + if ( in_array( $feature, array( 'donations', 'recurring-payments', 'premium-content/container' ), true ) ) {
441 + return true;
442 + }
443 +
356 444 $plan = self::get();
357 445
358 446 if (
359 447 in_array( $feature, $plan['supports'], true )
@@ -362,6 +450,99 @@
362 450 return true;
363 451 }
364 452
365 453 return false;
454 + }
455 +
456 + /**
457 + * Retrieve site-specific features for Simple sites.
458 + *
459 + * See Jetpack_Gutenberg::get_site_specific_features()
460 + *
461 + * @return array
462 + */
463 + public static function get_simple_site_specific_features() {
464 + $is_simple_site = defined( 'IS_WPCOM' ) && constant( 'IS_WPCOM' );
465 +
466 + if ( ! $is_simple_site ) {
467 + return array(
468 + 'active' => array(),
469 + 'available' => array(),
470 + );
471 + }
472 +
473 + $current_blog_id = get_current_blog_id();
474 +
475 + // Return the cached value if it exists.
476 + if ( isset( self::$simple_site_specific_features[ $current_blog_id ] ) ) {
477 + return self::$simple_site_specific_features[ $current_blog_id ];
478 + }
479 +
480 + if ( ! class_exists( '\Store_Product_List' ) ) {
481 + require WP_CONTENT_DIR . '/admin-plugins/wpcom-billing/store-product-list.php';
482 + }
483 +
484 + $simple_site_specific_features = \Store_Product_List::get_site_specific_features_data( $current_blog_id );
485 +
486 + self::$simple_site_specific_features[ $current_blog_id ] = $simple_site_specific_features;
487 +
488 + return $simple_site_specific_features;
489 + }
490 +
491 + /**
492 + * Retrieve site-specific features from the WordPress.com registry the site itself carries.
493 + *
494 + * Simple sites read the store; Atomic sites read the purchases wpcomsh keeps in sync. Both
495 + * answer as of this request, where `self::PLAN_OPTION` is only as current as its last fetch.
496 + *
497 + * @since 0.14.0
498 + *
499 + * @return array|null Active and available features, or null where the site carries no registry.
500 + */
501 + public static function get_wpcom_site_specific_features() {
502 + if ( defined( 'IS_WPCOM' ) && constant( 'IS_WPCOM' ) ) {
503 + return self::get_simple_site_specific_features();
504 + }
505 +
506 + if (
507 + ! Constants::is_true( 'IS_ATOMIC' )
508 + || ! class_exists( '\WPCOM_Features' )
509 + || ! function_exists( 'wpcom_get_site_purchases' )
510 + ) {
511 + return null;
512 + }
513 +
514 + if ( null !== self::$atomic_site_specific_features ) {
515 + return self::$atomic_site_specific_features;
516 + }
517 +
518 + // No blog ID anywhere below. wpcomsh resolves it from `jetpack_options`, where WordPress
519 + // reports 1, and throws when the two disagree.
520 + $purchases = \wpcom_get_site_purchases();
521 +
522 + /*
523 + * A site whose Atomic persistent data has not synced reads exactly like one that bought
524 + * nothing, and answering would gate a paid site. Only WordPress.com can tell the two
525 + * apart, so report no answer and leave the caller its own fallback.
526 + */
527 + if ( ! $purchases ) {
528 + return null;
529 + }
530 +
531 + $active = array();
532 +
533 + foreach ( \WPCOM_Features::get_feature_slugs() as $feature ) {
534 + if ( \WPCOM_Features::has_feature( $feature, $purchases, 'wpcom' ) ) {
535 + $active[] = $feature;
536 + }
537 + }
538 +
539 + // `available` names the plans that would grant each feature the site lacks, which only the
540 + // WordPress.com product catalogue can answer. Callers here read `active`.
541 + self::$atomic_site_specific_features = array(
542 + 'active' => $active,
543 + 'available' => array(),
544 + );
545 +
546 + return self::$atomic_site_specific_features;
366 547 }
367 548 }