PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.3-beta
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 13.7.2 All 507 releases
← All changes | vendor/automattic/jetpack-plans/src/class-current-plan.php +209 -34 12.2.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(
@@ -78,8 +100,11 @@
78 100 'value_bundle',
79 101 'value_bundle-monthly',
80 102 'value_bundle-2y',
81 103 'value_bundle-3y',
104 + 'jetpack_creator_yearly',
105 + 'jetpack_creator_bi_yearly',
106 + 'jetpack_creator_monthly',
82 107 ),
83 108 'supports' => array(
84 109 'simple-payments',
85 110 'vaultpress',
@@ -112,10 +137,20 @@
112 137 'ecommerce-bundle-monthly',
113 138 'ecommerce-bundle-2y',
114 139 'ecommerce-bundle-3y',
115 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',
116 149 ),
117 - 'supports' => array(),
150 + 'supports' => array(
151 + 'ai-seo-enhancer',
152 + ),
118 153 ),
119 154
120 155 'complete' => array(
121 156 'plans' => array(
@@ -122,9 +157,12 @@
122 157 'jetpack_complete',
123 158 'jetpack_complete_monthly',
124 159 'vip',
125 160 ),
126 - 'supports' => array(),
161 + 'supports' => array(
162 + 'field-file', // Forms
163 + 'social-image-generator',
164 + ),
127 165 ),
128 166 );
129 167
130 168 /**
@@ -144,33 +182,42 @@
144 182 if ( is_wp_error( $body ) ) {
145 183 return false;
146 184 }
147 185
148 - // Decode the results.
149 - $results = json_decode( $body, true );
186 + return self::update_from_site_record( json_decode( $body, true ) );
187 + }
150 188
151 - 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 ) ) {
152 199 return false;
153 200 }
154 201
155 - if ( isset( $results['products'] ) ) {
202 + if ( isset( $record['products'] ) ) {
156 203 // Store the site's products in an option and return true if updated.
157 - self::store_data_in_option( self::SITE_PRODUCTS_OPTION, $results['products'] );
204 + self::store_data_in_option( self::SITE_PRODUCTS_OPTION, $record['products'] );
158 205 }
159 206
160 - if ( ! isset( $results['plan'] ) ) {
207 + if ( ! isset( $record['plan'] ) ) {
161 208 return false;
162 209 }
163 210
164 211 $current_plan = get_option( self::PLAN_OPTION, array() );
165 212
166 - if ( ! empty( $current_plan ) && $current_plan === $results['plan'] ) {
213 + if ( ! empty( $current_plan ) && $current_plan === $record['plan'] ) {
167 214 // Bail if the plans array hasn't changed.
168 215 return false;
169 216 }
170 217
171 218 // Store the new plan in an option and return true if updated.
172 - $result = self::store_data_in_option( self::PLAN_OPTION, $results['plan'] );
219 + $result = self::store_data_in_option( self::PLAN_OPTION, $record['plan'] );
173 220
174 221 if ( $result ) {
175 222 // Reset the cache since we've just updated the plan.
176 223 self::$active_plan_cache = null;
@@ -188,15 +235,23 @@
188 235 */
189 236 private static function store_data_in_option( $option, $data ) {
190 237 $result = update_option( $option, $data, true );
191 238
192 - // If something goes wrong with the update, so delete the current option and then update it.
193 - if ( ! $result ) {
194 - delete_option( $option );
195 - $result = update_option( $option, $data, true );
239 + if ( $result ) {
240 + return true;
196 241 }
197 242
198 - 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 );
199 254 }
200 255
201 256 /**
202 257 * Make an API call to WordPress.com for plan status
@@ -207,15 +262,44 @@
207 262 *
208 263 * @access public
209 264 * @static
210 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.
211 271 * @return bool True if plan is updated, false if no update
212 272 */
213 - 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 +
214 284 // Make the API request.
215 285
216 - $response = Client::wpcom_json_api_request_as_blog( sprintf( '/sites/%d', Jetpack_Options::get_option( 'id' ) ) . '?force=wpcom', '1.1' );
217 - 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;
218 302 }
219 303
220 304 /**
221 305 * Get the plan that this Jetpack site is currently using.
@@ -248,19 +332,17 @@
248 332 );
249 333
250 334 list( $plan['class'], $supports ) = self::get_class_and_features( $plan['product_slug'] );
251 335
252 - // get available features if Jetpack is active.
253 - if ( class_exists( 'Jetpack' ) ) {
254 - foreach ( \Jetpack::get_available_modules() as $module_slug ) {
255 - $module = \Jetpack::get_module( $module_slug );
256 - if ( ! isset( $module ) || ! is_array( $module ) ) {
257 - continue;
258 - }
259 - if ( in_array( 'free', $module['plan_classes'], true ) || in_array( $plan['class'], $module['plan_classes'], true ) ) {
260 - $supports[] = $module_slug;
261 - }
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;
262 341 }
342 + if ( in_array( 'free', $module['plan_classes'], true ) || in_array( $plan['class'], $module['plan_classes'], true ) ) {
343 + $supports[] = $module_slug;
344 + }
263 345 }
264 346
265 347 $plan['supports'] = $supports;
266 348
@@ -318,9 +400,9 @@
318 400
319 401 /**
320 402 * Determine whether the active plan supports a particular feature
321 403 *
322 - * @uses Jetpack_Plan::get()
404 + * @uses self::get()
323 405 *
324 406 * @access public
325 407 * @static
326 408 *
@@ -353,9 +435,9 @@
353 435 if ( 'videopress' === $feature ) {
354 436 return true;
355 437 }
356 438
357 - // As of 05 2023 - all plans support Earn features (minus 'simple-payments')
439 + // As of 05 2023 - all plans support Earn features (minus 'simple-payments').
358 440 if ( in_array( $feature, array( 'donations', 'recurring-payments', 'premium-content/container' ), true ) ) {
359 441 return true;
360 442 }
361 443
@@ -368,6 +450,99 @@
368 450 return true;
369 451 }
370 452
371 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;
372 547 }
373 548 }