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 +186 -19 12.4.2 → 16.3-beta View file →
@@ -21,8 +21,25 @@
21 21 */
22 22 private static $active_plan_cache;
23 23
24 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 + /**
25 42 * The name of the option that will store the site's plan.
26 43 *
27 44 * @var string
28 45 */
@@ -44,8 +61,9 @@
44 61 'advanced-seo',
45 62 'opentable',
46 63 'calendly',
47 64 'send-a-message',
65 + 'sharing-block',
48 66 'whatsapp-button',
49 67 'social-previews',
50 68 'videopress',
51 69 'videopress/video',
@@ -53,8 +71,11 @@
53 71
54 72 'core/video',
55 73 'core/cover',
56 74 'core/audio',
75 + 'multistep-form',
76 + 'form-webhooks',
77 + 'form-conditional-logic',
57 78 ),
58 79 ),
59 80 'personal' => array(
60 81 'plans' => array(
@@ -79,8 +100,11 @@
79 100 'value_bundle',
80 101 'value_bundle-monthly',
81 102 'value_bundle-2y',
82 103 'value_bundle-3y',
104 + 'jetpack_creator_yearly',
105 + 'jetpack_creator_bi_yearly',
106 + 'jetpack_creator_monthly',
83 107 ),
84 108 'supports' => array(
85 109 'simple-payments',
86 110 'vaultpress',
@@ -113,10 +137,20 @@
113 137 'ecommerce-bundle-monthly',
114 138 'ecommerce-bundle-2y',
115 139 'ecommerce-bundle-3y',
116 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',
117 149 ),
118 - 'supports' => array(),
150 + 'supports' => array(
151 + 'ai-seo-enhancer',
152 + ),
119 153 ),
120 154
121 155 'complete' => array(
122 156 'plans' => array(
@@ -123,9 +157,12 @@
123 157 'jetpack_complete',
124 158 'jetpack_complete_monthly',
125 159 'vip',
126 160 ),
127 - 'supports' => array(),
161 + 'supports' => array(
162 + 'field-file', // Forms
163 + 'social-image-generator',
164 + ),
128 165 ),
129 166 );
130 167
131 168 /**
@@ -145,33 +182,42 @@
145 182 if ( is_wp_error( $body ) ) {
146 183 return false;
147 184 }
148 185
149 - // Decode the results.
150 - $results = json_decode( $body, true );
186 + return self::update_from_site_record( json_decode( $body, true ) );
187 + }
151 188
152 - 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 ) ) {
153 199 return false;
154 200 }
155 201
156 - if ( isset( $results['products'] ) ) {
202 + if ( isset( $record['products'] ) ) {
157 203 // Store the site's products in an option and return true if updated.
158 - self::store_data_in_option( self::SITE_PRODUCTS_OPTION, $results['products'] );
204 + self::store_data_in_option( self::SITE_PRODUCTS_OPTION, $record['products'] );
159 205 }
160 206
161 - if ( ! isset( $results['plan'] ) ) {
207 + if ( ! isset( $record['plan'] ) ) {
162 208 return false;
163 209 }
164 210
165 211 $current_plan = get_option( self::PLAN_OPTION, array() );
166 212
167 - if ( ! empty( $current_plan ) && $current_plan === $results['plan'] ) {
213 + if ( ! empty( $current_plan ) && $current_plan === $record['plan'] ) {
168 214 // Bail if the plans array hasn't changed.
169 215 return false;
170 216 }
171 217
172 218 // Store the new plan in an option and return true if updated.
173 - $result = self::store_data_in_option( self::PLAN_OPTION, $results['plan'] );
219 + $result = self::store_data_in_option( self::PLAN_OPTION, $record['plan'] );
174 220
175 221 if ( $result ) {
176 222 // Reset the cache since we've just updated the plan.
177 223 self::$active_plan_cache = null;
@@ -189,15 +235,23 @@
189 235 */
190 236 private static function store_data_in_option( $option, $data ) {
191 237 $result = update_option( $option, $data, true );
192 238
193 - // If something goes wrong with the update, so delete the current option and then update it.
194 - if ( ! $result ) {
195 - delete_option( $option );
196 - $result = update_option( $option, $data, true );
239 + if ( $result ) {
240 + return true;
197 241 }
198 242
199 - 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 );
200 254 }
201 255
202 256 /**
203 257 * Make an API call to WordPress.com for plan status
@@ -208,11 +262,21 @@
208 262 *
209 263 * @access public
210 264 * @static
211 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.
212 271 * @return bool True if plan is updated, false if no update
213 272 */
214 - 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 +
215 279 $site_id = Manager::get_site_id();
216 280 if ( is_wp_error( $site_id ) ) {
217 281 return false;
218 282 }
@@ -220,12 +284,22 @@
220 284 // Make the API request.
221 285
222 286 $response = Client::wpcom_json_api_request_as_blog(
223 287 sprintf( '/sites/%d?force=wpcom', $site_id ),
224 - '1.1'
288 + '1.1',
289 + $args
225 290 );
226 291
227 - return self::update_from_sites_response( $response );
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;
228 302 }
229 303
230 304 /**
231 305 * Get the plan that this Jetpack site is currently using.
@@ -361,9 +435,9 @@
361 435 if ( 'videopress' === $feature ) {
362 436 return true;
363 437 }
364 438
365 - // 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').
366 440 if ( in_array( $feature, array( 'donations', 'recurring-payments', 'premium-content/container' ), true ) ) {
367 441 return true;
368 442 }
369 443
@@ -376,6 +450,99 @@
376 450 return true;
377 451 }
378 452
379 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;
380 547 }
381 548 }