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 +548 -0 16.2-beta → 16.3-beta View file →
@@ -1,0 +1,548 @@
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 +}