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
jetpack / vendor / automattic / jetpack-plans / src / class-current-plan.php

class-current-plan.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-beta, at vendor/automattic/jetpack-plans/src/class-current-plan.php

549 lines 14.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles fetching of the site's plan and products from WordPress.com and caching values locally.
4 *
5 * @package automattic/jetpack-plans
6 */
7
8 namespace Automattic\Jetpack;
9
10 use Automattic\Jetpack\Connection\Client;
11 use Automattic\Jetpack\Connection\Manager;
12
13 /**
14 * Provides methods methods for fetching the site's plan and products from WordPress.com.
15 */
16 class Current_Plan {
17 /**
18 * A cache variable to hold the active plan for the current request.
19 *
20 * @var array
21 */
22 private static $active_plan_cache;
23
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 /**
42 * The name of the option that will store the site's plan.
43 *
44 * @var string
45 */
46 const PLAN_OPTION = 'jetpack_active_plan';
47
48 /**
49 * The name of the option that will store the site's products.
50 *
51 * @var string
52 */
53 const SITE_PRODUCTS_OPTION = 'jetpack_site_products';
54
55 const PLAN_DATA = array(
56 'free' => array(
57 'plans' => array(
58 'jetpack_free',
59 ),
60 'supports' => array(
61 'advanced-seo',
62 'opentable',
63 'calendly',
64 'send-a-message',
65 'sharing-block',
66 'whatsapp-button',
67 'social-previews',
68 'videopress',
69 'videopress/video',
70 'v6-video-frame-poster',
71
72 'core/video',
73 'core/cover',
74 'core/audio',
75 'multistep-form',
76 'form-webhooks',
77 'form-conditional-logic',
78 ),
79 ),
80 'personal' => array(
81 'plans' => array(
82 'jetpack_personal',
83 'jetpack_personal_monthly',
84 'personal-bundle',
85 'personal-bundle-monthly',
86 'personal-bundle-2y',
87 'personal-bundle-3y',
88 'starter-plan',
89 ),
90 'supports' => array(
91 'akismet',
92 'payments',
93 'videopress',
94 ),
95 ),
96 'premium' => array(
97 'plans' => array(
98 'jetpack_premium',
99 'jetpack_premium_monthly',
100 'value_bundle',
101 'value_bundle-monthly',
102 'value_bundle-2y',
103 'value_bundle-3y',
104 'jetpack_creator_yearly',
105 'jetpack_creator_bi_yearly',
106 'jetpack_creator_monthly',
107 ),
108 'supports' => array(
109 'simple-payments',
110 'vaultpress',
111 'videopress',
112 'republicize',
113 ),
114 ),
115 'security' => array(
116 'plans' => array(
117 'jetpack_security_daily',
118 'jetpack_security_daily_monthly',
119 'jetpack_security_realtime',
120 'jetpack_security_realtime_monthly',
121 'jetpack_security_t1_yearly',
122 'jetpack_security_t1_monthly',
123 'jetpack_security_t2_yearly',
124 'jetpack_security_t2_monthly',
125 ),
126 'supports' => array(),
127 ),
128 'business' => array(
129 'plans' => array(
130 'jetpack_business',
131 'jetpack_business_monthly',
132 'business-bundle',
133 'business-bundle-monthly',
134 'business-bundle-2y',
135 'business-bundle-3y',
136 'ecommerce-bundle',
137 'ecommerce-bundle-monthly',
138 'ecommerce-bundle-2y',
139 'ecommerce-bundle-3y',
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',
149 ),
150 'supports' => array(
151 'ai-seo-enhancer',
152 ),
153 ),
154
155 'complete' => array(
156 'plans' => array(
157 'jetpack_complete',
158 'jetpack_complete_monthly',
159 'vip',
160 ),
161 'supports' => array(
162 'field-file', // Forms
163 'social-image-generator',
164 ),
165 ),
166 );
167
168 /**
169 * Given a response to the `/sites/%d` endpoint, will parse the response and attempt to set the
170 * site's plan and products from the response.
171 *
172 * @param array $response The response from `/sites/%d`.
173 * @return bool Was the plan successfully updated?
174 */
175 public static function update_from_sites_response( $response ) {
176 // Bail if there was an error or malformed response.
177 if ( is_wp_error( $response ) || ! is_array( $response ) || ! isset( $response['body'] ) ) {
178 return false;
179 }
180
181 $body = wp_remote_retrieve_body( $response );
182 if ( is_wp_error( $body ) ) {
183 return false;
184 }
185
186 return self::update_from_site_record( json_decode( $body, true ) );
187 }
188
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 ) ) {
199 return false;
200 }
201
202 if ( isset( $record['products'] ) ) {
203 // Store the site's products in an option and return true if updated.
204 self::store_data_in_option( self::SITE_PRODUCTS_OPTION, $record['products'] );
205 }
206
207 if ( ! isset( $record['plan'] ) ) {
208 return false;
209 }
210
211 $current_plan = get_option( self::PLAN_OPTION, array() );
212
213 if ( ! empty( $current_plan ) && $current_plan === $record['plan'] ) {
214 // Bail if the plans array hasn't changed.
215 return false;
216 }
217
218 // Store the new plan in an option and return true if updated.
219 $result = self::store_data_in_option( self::PLAN_OPTION, $record['plan'] );
220
221 if ( $result ) {
222 // Reset the cache since we've just updated the plan.
223 self::$active_plan_cache = null;
224 }
225
226 return $result;
227 }
228
229 /**
230 * Store data in an option.
231 *
232 * @param string $option The name of the option that will store the data.
233 * @param array $data Data to be store in an option.
234 * @return bool Were the subscriptions successfully updated?
235 */
236 private static function store_data_in_option( $option, $data ) {
237 $result = update_option( $option, $data, true );
238
239 if ( $result ) {
240 return true;
241 }
242
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 );
254 }
255
256 /**
257 * Make an API call to WordPress.com for plan status
258 *
259 * @uses Jetpack_Options::get_option()
260 * @uses Client::wpcom_json_api_request_as_blog()
261 * @uses update_option()
262 *
263 * @access public
264 * @static
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.
271 * @return bool True if plan is updated, false if no update
272 */
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
284 // Make the API request.
285
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;
302 }
303
304 /**
305 * Get the plan that this Jetpack site is currently using.
306 *
307 * @uses get_option()
308 *
309 * @access public
310 * @static
311 *
312 * @return array Active Jetpack plan details
313 */
314 public static function get() {
315 // this can be expensive to compute so we cache for the duration of a request.
316 if ( is_array( self::$active_plan_cache ) && ! empty( self::$active_plan_cache ) ) {
317 return self::$active_plan_cache;
318 }
319
320 $plan = get_option( self::PLAN_OPTION, array() );
321
322 // Set the default options.
323 $plan = wp_parse_args(
324 $plan,
325 array(
326 'product_slug' => 'jetpack_free',
327 'class' => 'free',
328 'features' => array(
329 'active' => array(),
330 ),
331 )
332 );
333
334 list( $plan['class'], $supports ) = self::get_class_and_features( $plan['product_slug'] );
335
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;
341 }
342 if ( in_array( 'free', $module['plan_classes'], true ) || in_array( $plan['class'], $module['plan_classes'], true ) ) {
343 $supports[] = $module_slug;
344 }
345 }
346
347 $plan['supports'] = $supports;
348
349 self::$active_plan_cache = $plan;
350
351 return $plan;
352 }
353
354 /**
355 * Get the site's products.
356 *
357 * @uses get_option()
358 *
359 * @access public
360 * @static
361 *
362 * @return array Active Jetpack products
363 */
364 public static function get_products() {
365 return get_option( self::SITE_PRODUCTS_OPTION, array() );
366 }
367
368 /**
369 * Get the class of plan and a list of features it supports
370 *
371 * @param string $plan_slug The plan that we're interested in.
372 * @return array Two item array, the plan class and the an array of features.
373 */
374 private static function get_class_and_features( $plan_slug ) {
375 $features = array();
376 foreach ( self::PLAN_DATA as $class => $details ) {
377 $features = array_merge( $features, $details['supports'] );
378 if ( in_array( $plan_slug, $details['plans'], true ) ) {
379 return array( $class, $features );
380 }
381 }
382 return array( 'free', self::PLAN_DATA['free']['supports'] );
383 }
384
385 /**
386 * Gets the minimum plan slug that supports the given feature
387 *
388 * @param string $feature The name of the feature.
389 * @return string|bool The slug for the minimum plan that supports.
390 * the feature or false if not found
391 */
392 public static function get_minimum_plan_for_feature( $feature ) {
393 foreach ( self::PLAN_DATA as $details ) {
394 if ( in_array( $feature, $details['supports'], true ) ) {
395 return $details['plans'][0];
396 }
397 }
398 return false;
399 }
400
401 /**
402 * Determine whether the active plan supports a particular feature
403 *
404 * @uses self::get()
405 *
406 * @access public
407 * @static
408 *
409 * @param string $feature The module or feature to check.
410 * @param bool $refresh_from_wpcom Refresh the local plan cache from wpcom.
411 *
412 * @return bool True if plan supports feature, false if not
413 */
414 public static function supports( $feature, $refresh_from_wpcom = false ) {
415 if ( $refresh_from_wpcom ) {
416 self::refresh_from_wpcom();
417 }
418
419 // Hijack the feature eligibility check on WordPress.com sites since they are gated differently.
420 $should_wpcom_gate_feature = (
421 function_exists( 'wpcom_site_has_feature' ) &&
422 function_exists( 'wpcom_feature_exists' ) &&
423 wpcom_feature_exists( $feature )
424 );
425 if ( $should_wpcom_gate_feature ) {
426 return wpcom_site_has_feature( $feature );
427 }
428
429 // Search product bypasses plan feature check.
430 if ( 'search' === $feature && (bool) get_option( 'has_jetpack_search_product' ) ) {
431 return true;
432 }
433
434 // As of Q3 2021 - a videopress free tier is available to all plans.
435 if ( 'videopress' === $feature ) {
436 return true;
437 }
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
444 $plan = self::get();
445
446 if (
447 in_array( $feature, $plan['supports'], true )
448 || in_array( $feature, $plan['features']['active'], true )
449 ) {
450 return true;
451 }
452
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;
547 }
548 }
549