PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.3
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 13.8.3 All 506 releases
jetpack / vendor / automattic / jetpack-plans / src / class-current-plan.php

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

470 lines 12.0 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 * The name of the option that will store the site's plan.
34 *
35 * @var string
36 */
37 const PLAN_OPTION = 'jetpack_active_plan';
38
39 /**
40 * The name of the option that will store the site's products.
41 *
42 * @var string
43 */
44 const SITE_PRODUCTS_OPTION = 'jetpack_site_products';
45
46 const PLAN_DATA = array(
47 'free' => array(
48 'plans' => array(
49 'jetpack_free',
50 ),
51 'supports' => array(
52 'advanced-seo',
53 'opentable',
54 'calendly',
55 'send-a-message',
56 'sharing-block',
57 'whatsapp-button',
58 'social-previews',
59 'videopress',
60 'videopress/video',
61 'v6-video-frame-poster',
62
63 'core/video',
64 'core/cover',
65 'core/audio',
66 'multistep-form',
67 'form-webhooks',
68 ),
69 ),
70 'personal' => array(
71 'plans' => array(
72 'jetpack_personal',
73 'jetpack_personal_monthly',
74 'personal-bundle',
75 'personal-bundle-monthly',
76 'personal-bundle-2y',
77 'personal-bundle-3y',
78 'starter-plan',
79 ),
80 'supports' => array(
81 'akismet',
82 'payments',
83 'videopress',
84 ),
85 ),
86 'premium' => array(
87 'plans' => array(
88 'jetpack_premium',
89 'jetpack_premium_monthly',
90 'value_bundle',
91 'value_bundle-monthly',
92 'value_bundle-2y',
93 'value_bundle-3y',
94 'jetpack_creator_yearly',
95 'jetpack_creator_bi_yearly',
96 'jetpack_creator_monthly',
97 ),
98 'supports' => array(
99 'simple-payments',
100 'vaultpress',
101 'videopress',
102 'republicize',
103 ),
104 ),
105 'security' => array(
106 'plans' => array(
107 'jetpack_security_daily',
108 'jetpack_security_daily_monthly',
109 'jetpack_security_realtime',
110 'jetpack_security_realtime_monthly',
111 'jetpack_security_t1_yearly',
112 'jetpack_security_t1_monthly',
113 'jetpack_security_t2_yearly',
114 'jetpack_security_t2_monthly',
115 ),
116 'supports' => array(),
117 ),
118 'business' => array(
119 'plans' => array(
120 'jetpack_business',
121 'jetpack_business_monthly',
122 'business-bundle',
123 'business-bundle-monthly',
124 'business-bundle-2y',
125 'business-bundle-3y',
126 'ecommerce-bundle',
127 'ecommerce-bundle-monthly',
128 'ecommerce-bundle-2y',
129 'ecommerce-bundle-3y',
130 'pro-plan',
131 'wp_bundle_migration_trial_monthly',
132 'wp_bundle_hosting_trial_monthly',
133 'ecommerce-trial-bundle-monthly',
134 'wooexpress-small-bundle-yearly',
135 'wooexpress-small-bundle-monthly',
136 'wooexpress-medium-bundle-yearly',
137 'wooexpress-medium-bundle-monthly',
138 'wp_com_hundred_year_bundle_centennially',
139 ),
140 'supports' => array(
141 'ai-seo-enhancer',
142 ),
143 ),
144
145 'complete' => array(
146 'plans' => array(
147 'jetpack_complete',
148 'jetpack_complete_monthly',
149 'vip',
150 ),
151 'supports' => array(
152 'field-file', // Forms
153 'social-image-generator',
154 ),
155 ),
156 );
157
158 /**
159 * Given a response to the `/sites/%d` endpoint, will parse the response and attempt to set the
160 * site's plan and products from the response.
161 *
162 * @param array $response The response from `/sites/%d`.
163 * @return bool Was the plan successfully updated?
164 */
165 public static function update_from_sites_response( $response ) {
166 // Bail if there was an error or malformed response.
167 if ( is_wp_error( $response ) || ! is_array( $response ) || ! isset( $response['body'] ) ) {
168 return false;
169 }
170
171 $body = wp_remote_retrieve_body( $response );
172 if ( is_wp_error( $body ) ) {
173 return false;
174 }
175
176 return self::update_from_site_record( json_decode( $body, true ) );
177 }
178
179 /**
180 * Given a decoded `/sites/%d` record, attempt to set the site's plan and products from it.
181 *
182 * @since 0.12.0
183 *
184 * @param array $record The decoded site record from the WordPress.com `/sites/%d` endpoint.
185 * @return bool Was the plan successfully updated?
186 */
187 public static function update_from_site_record( $record ) {
188 if ( ! is_array( $record ) ) {
189 return false;
190 }
191
192 if ( isset( $record['products'] ) ) {
193 // Store the site's products in an option and return true if updated.
194 self::store_data_in_option( self::SITE_PRODUCTS_OPTION, $record['products'] );
195 }
196
197 if ( ! isset( $record['plan'] ) ) {
198 return false;
199 }
200
201 $current_plan = get_option( self::PLAN_OPTION, array() );
202
203 if ( ! empty( $current_plan ) && $current_plan === $record['plan'] ) {
204 // Bail if the plans array hasn't changed.
205 return false;
206 }
207
208 // Store the new plan in an option and return true if updated.
209 $result = self::store_data_in_option( self::PLAN_OPTION, $record['plan'] );
210
211 if ( $result ) {
212 // Reset the cache since we've just updated the plan.
213 self::$active_plan_cache = null;
214 }
215
216 return $result;
217 }
218
219 /**
220 * Store data in an option.
221 *
222 * @param string $option The name of the option that will store the data.
223 * @param array $data Data to be store in an option.
224 * @return bool Were the subscriptions successfully updated?
225 */
226 private static function store_data_in_option( $option, $data ) {
227 $result = update_option( $option, $data, true );
228
229 if ( $result ) {
230 return true;
231 }
232
233 // update_option() also reports false when the stored value already matches, which is not a
234 // failure. Both options are autoloaded, so reading it as one rewrites them on every
235 // unchanged fetch and drops the alloptions cache with it.
236 if ( get_option( $option ) === $data ) {
237 return true;
238 }
239
240 // If the update genuinely failed, delete the option and write it again.
241 delete_option( $option );
242
243 return update_option( $option, $data, true );
244 }
245
246 /**
247 * Make an API call to WordPress.com for plan status
248 *
249 * @uses Jetpack_Options::get_option()
250 * @uses Client::wpcom_json_api_request_as_blog()
251 * @uses update_option()
252 *
253 * @access public
254 * @static
255 *
256 * @return bool True if plan is updated, false if no update
257 */
258 public static function refresh_from_wpcom() {
259 $site_id = Manager::get_site_id();
260 if ( is_wp_error( $site_id ) ) {
261 return false;
262 }
263
264 // Make the API request.
265
266 $response = Client::wpcom_json_api_request_as_blog(
267 sprintf( '/sites/%d?force=wpcom', $site_id ),
268 '1.1'
269 );
270
271 $updated = self::update_from_sites_response( $response );
272
273 // The shared site record cache can still hold a record older than this response, and a
274 // cached read stores the plan again. Dropping it keeps that older record from reverting
275 // what this fetch just stored.
276 if ( ! is_wp_error( $response ) ) {
277 Manager::delete_cached_site_data();
278 }
279
280 return $updated;
281 }
282
283 /**
284 * Get the plan that this Jetpack site is currently using.
285 *
286 * @uses get_option()
287 *
288 * @access public
289 * @static
290 *
291 * @return array Active Jetpack plan details
292 */
293 public static function get() {
294 // this can be expensive to compute so we cache for the duration of a request.
295 if ( is_array( self::$active_plan_cache ) && ! empty( self::$active_plan_cache ) ) {
296 return self::$active_plan_cache;
297 }
298
299 $plan = get_option( self::PLAN_OPTION, array() );
300
301 // Set the default options.
302 $plan = wp_parse_args(
303 $plan,
304 array(
305 'product_slug' => 'jetpack_free',
306 'class' => 'free',
307 'features' => array(
308 'active' => array(),
309 ),
310 )
311 );
312
313 list( $plan['class'], $supports ) = self::get_class_and_features( $plan['product_slug'] );
314
315 $modules = new Modules();
316 foreach ( $modules->get_available() as $module_slug ) {
317 $module = $modules->get( $module_slug );
318 if ( ! isset( $module ) || ! is_array( $module ) ) {
319 continue;
320 }
321 if ( in_array( 'free', $module['plan_classes'], true ) || in_array( $plan['class'], $module['plan_classes'], true ) ) {
322 $supports[] = $module_slug;
323 }
324 }
325
326 $plan['supports'] = $supports;
327
328 self::$active_plan_cache = $plan;
329
330 return $plan;
331 }
332
333 /**
334 * Get the site's products.
335 *
336 * @uses get_option()
337 *
338 * @access public
339 * @static
340 *
341 * @return array Active Jetpack products
342 */
343 public static function get_products() {
344 return get_option( self::SITE_PRODUCTS_OPTION, array() );
345 }
346
347 /**
348 * Get the class of plan and a list of features it supports
349 *
350 * @param string $plan_slug The plan that we're interested in.
351 * @return array Two item array, the plan class and the an array of features.
352 */
353 private static function get_class_and_features( $plan_slug ) {
354 $features = array();
355 foreach ( self::PLAN_DATA as $class => $details ) {
356 $features = array_merge( $features, $details['supports'] );
357 if ( in_array( $plan_slug, $details['plans'], true ) ) {
358 return array( $class, $features );
359 }
360 }
361 return array( 'free', self::PLAN_DATA['free']['supports'] );
362 }
363
364 /**
365 * Gets the minimum plan slug that supports the given feature
366 *
367 * @param string $feature The name of the feature.
368 * @return string|bool The slug for the minimum plan that supports.
369 * the feature or false if not found
370 */
371 public static function get_minimum_plan_for_feature( $feature ) {
372 foreach ( self::PLAN_DATA as $details ) {
373 if ( in_array( $feature, $details['supports'], true ) ) {
374 return $details['plans'][0];
375 }
376 }
377 return false;
378 }
379
380 /**
381 * Determine whether the active plan supports a particular feature
382 *
383 * @uses self::get()
384 *
385 * @access public
386 * @static
387 *
388 * @param string $feature The module or feature to check.
389 * @param bool $refresh_from_wpcom Refresh the local plan cache from wpcom.
390 *
391 * @return bool True if plan supports feature, false if not
392 */
393 public static function supports( $feature, $refresh_from_wpcom = false ) {
394 if ( $refresh_from_wpcom ) {
395 self::refresh_from_wpcom();
396 }
397
398 // Hijack the feature eligibility check on WordPress.com sites since they are gated differently.
399 $should_wpcom_gate_feature = (
400 function_exists( 'wpcom_site_has_feature' ) &&
401 function_exists( 'wpcom_feature_exists' ) &&
402 wpcom_feature_exists( $feature )
403 );
404 if ( $should_wpcom_gate_feature ) {
405 return wpcom_site_has_feature( $feature );
406 }
407
408 // Search product bypasses plan feature check.
409 if ( 'search' === $feature && (bool) get_option( 'has_jetpack_search_product' ) ) {
410 return true;
411 }
412
413 // As of Q3 2021 - a videopress free tier is available to all plans.
414 if ( 'videopress' === $feature ) {
415 return true;
416 }
417
418 // As of 05 2023 - all plans support Earn features (minus 'simple-payments').
419 if ( in_array( $feature, array( 'donations', 'recurring-payments', 'premium-content/container' ), true ) ) {
420 return true;
421 }
422
423 $plan = self::get();
424
425 if (
426 in_array( $feature, $plan['supports'], true )
427 || in_array( $feature, $plan['features']['active'], true )
428 ) {
429 return true;
430 }
431
432 return false;
433 }
434
435 /**
436 * Retrieve site-specific features for Simple sites.
437 *
438 * See Jetpack_Gutenberg::get_site_specific_features()
439 *
440 * @return array
441 */
442 public static function get_simple_site_specific_features() {
443 $is_simple_site = defined( 'IS_WPCOM' ) && constant( 'IS_WPCOM' );
444
445 if ( ! $is_simple_site ) {
446 return array(
447 'active' => array(),
448 'available' => array(),
449 );
450 }
451
452 $current_blog_id = get_current_blog_id();
453
454 // Return the cached value if it exists.
455 if ( isset( self::$simple_site_specific_features[ $current_blog_id ] ) ) {
456 return self::$simple_site_specific_features[ $current_blog_id ];
457 }
458
459 if ( ! class_exists( '\Store_Product_List' ) ) {
460 require WP_CONTENT_DIR . '/admin-plugins/wpcom-billing/store-product-list.php';
461 }
462
463 $simple_site_specific_features = \Store_Product_List::get_site_specific_features_data( $current_blog_id );
464
465 self::$simple_site_specific_features[ $current_blog_id ] = $simple_site_specific_features;
466
467 return $simple_site_specific_features;
468 }
469 }
470