Completed
Push — branch-9.3-built ( cf2e3a...4150c4 )
by Jeremy
241:41 queued 236:52
created

class.jetpack-plan.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php //phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2
/**
3
 * Handles fetching of the site's plan and products from WordPress.com and caching values locally.
4
 *
5
 * Not to be confused with the `Jetpack_Plans` class (in `_inc/lib/plans.php`), which
6
 * fetches general information about all available plans from WordPress.com, side-effect free.
7
 *
8
 * @package Jetpack
9
 */
10
11
use Automattic\Jetpack\Connection\Client;
12
13
/**
14
 * Provides methods methods for fetching the site's plan and products from WordPress.com.
15
 */
16
class Jetpack_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
	 * The name of the option that will store the site's plan.
26
	 *
27
	 * @var string
28
	 */
29
	const PLAN_OPTION = 'jetpack_active_plan';
30
31
	/**
32
	 * The name of the option that will store the site's products.
33
	 *
34
	 * @var string
35
	 */
36
	const SITE_PRODUCTS_OPTION = 'jetpack_site_products';
37
38
	const PLAN_DATA = array(
39
		'free'     => array(
40
			'plans'    => array(
41
				'jetpack_free',
42
			),
43
			'supports' => array(
44
				'opentable',
45
				'calendly',
46
				'send-a-message',
47
				'social-previews',
48
49
				'core/video',
50
				'core/cover',
51
				'core/audio',
52
			),
53
		),
54
		'personal' => array(
55
			'plans'    => array(
56
				'jetpack_personal',
57
				'jetpack_personal_monthly',
58
				'personal-bundle',
59
				'personal-bundle-monthly',
60
				'personal-bundle-2y',
61
			),
62
			'supports' => array(
63
				'akismet',
64
				'recurring-payments',
65
			),
66
		),
67
		'premium'  => array(
68
			'plans'    => array(
69
				'jetpack_premium',
70
				'jetpack_premium_monthly',
71
				'value_bundle',
72
				'value_bundle-monthly',
73
				'value_bundle-2y',
74
			),
75
			'supports' => array(
76
				'donations',
77
				'simple-payments',
78
				'vaultpress',
79
				'videopress',
80
			),
81
		),
82
		'security' => array(
83
			'plans'    => array(
84
				'jetpack_security_daily',
85
				'jetpack_security_daily_monthly',
86
				'jetpack_security_realtime',
87
				'jetpack_security_realtime_monthly',
88
			),
89
			'supports' => array(),
90
		),
91
		'business' => array(
92
			'plans'    => array(
93
				'jetpack_business',
94
				'jetpack_business_monthly',
95
				'business-bundle',
96
				'business-bundle-monthly',
97
				'business-bundle-2y',
98
				'ecommerce-bundle',
99
				'ecommerce-bundle-monthly',
100
				'ecommerce-bundle-2y',
101
				'vip',
102
			),
103
			'supports' => array(),
104
		),
105
106
		'complete' => array(
107
			'plans'    => array(
108
				'jetpack_complete',
109
				'jetpack_complete_monthly',
110
			),
111
			'supports' => array(),
112
		),
113
	);
114
115
	/**
116
	 * Given a response to the `/sites/%d` endpoint, will parse the response and attempt to set the
117
	 * site's plan and products from the response.
118
	 *
119
	 * @param array $response The response from `/sites/%d`.
120
	 * @return bool Was the plan successfully updated?
121
	 */
122
	public static function update_from_sites_response( $response ) {
123
		// Bail if there was an error or malformed response.
124
		if ( is_wp_error( $response ) || ! is_array( $response ) || ! isset( $response['body'] ) ) {
125
			return false;
126
		}
127
128
		$body = wp_remote_retrieve_body( $response );
129
		if ( is_wp_error( $body ) ) {
130
			return false;
131
		}
132
133
		// Decode the results.
134
		$results = json_decode( $body, true );
135
136
		if ( ! is_array( $results ) ) {
137
			return false;
138
		}
139
140
		if ( isset( $results['products'] ) ) {
141
			// Store the site's products in an option and return true if updated.
142
			self::store_data_in_option( self::SITE_PRODUCTS_OPTION, $results['products'] );
143
		}
144
145
		if ( ! isset( $results['plan'] ) ) {
146
			return false;
147
		}
148
149
		$current_plan = get_option( self::PLAN_OPTION, array() );
150
151
		if ( ! empty( $current_plan ) && $current_plan === $results['plan'] ) {
152
			// Bail if the plans array hasn't changed.
153
			return false;
154
		}
155
156
		// Store the new plan in an option and return true if updated.
157
		$result = self::store_data_in_option( self::PLAN_OPTION, $results['plan'] );
158
159
		if ( $result ) {
160
			// Reset the cache since we've just updated the plan.
161
			self::$active_plan_cache = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $active_plan_cache.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
162
		}
163
164
		return $result;
165
	}
166
167
	/**
168
	 * Store data in an option.
169
	 *
170
	 * @param string $option The name of the option that will store the data.
171
	 * @param array  $data Data to be store in an option.
172
	 * @return bool Were the subscriptions successfully updated?
173
	 */
174
	private static function store_data_in_option( $option, $data ) {
175
		$result = update_option( $option, $data, true );
176
177
		// If something goes wrong with the update, so delete the current option and then update it.
178
		if ( ! $result ) {
179
			delete_option( $option );
180
			$result = update_option( $option, $data, true );
181
		}
182
183
		return $result;
184
	}
185
186
	/**
187
	 * Make an API call to WordPress.com for plan status
188
	 *
189
	 * @uses Jetpack_Options::get_option()
190
	 * @uses Client::wpcom_json_api_request_as_blog()
191
	 * @uses update_option()
192
	 *
193
	 * @access public
194
	 * @static
195
	 *
196
	 * @return bool True if plan is updated, false if no update
197
	 */
198
	public static function refresh_from_wpcom() {
199
		// Make the API request.
200
		$request  = sprintf( '/sites/%d', Jetpack_Options::get_option( 'id' ) );
201
		$response = Client::wpcom_json_api_request_as_blog( $request, '1.1' );
202
203
		return self::update_from_sites_response( $response );
204
	}
205
206
	/**
207
	 * Get the plan that this Jetpack site is currently using.
208
	 *
209
	 * @uses get_option()
210
	 *
211
	 * @access public
212
	 * @static
213
	 *
214
	 * @return array Active Jetpack plan details
215
	 */
216
	public static function get() {
217
		// this can be expensive to compute so we cache for the duration of a request.
218
		if ( is_array( self::$active_plan_cache ) && ! empty( self::$active_plan_cache ) ) {
219
			return self::$active_plan_cache;
220
		}
221
222
		$plan = get_option( self::PLAN_OPTION, array() );
223
224
		// Set the default options.
225
		$plan = wp_parse_args(
226
			$plan,
227
			array(
228
				'product_slug' => 'jetpack_free',
229
				'class'        => 'free',
230
				'features'     => array(
231
					'active' => array(),
232
				),
233
			)
234
		);
235
236
		list( $plan['class'], $supports ) = self::get_class_and_features( $plan['product_slug'] );
237
238
		// get available features.
239
		foreach ( Jetpack::get_available_modules() as $module_slug ) {
240
			$module = Jetpack::get_module( $module_slug );
241
			if ( ! isset( $module ) || ! is_array( $module ) ) {
242
				continue;
243
			}
244
			if ( in_array( 'free', $module['plan_classes'], true ) || in_array( $plan['class'], $module['plan_classes'], true ) ) {
245
				$supports[] = $module_slug;
246
			}
247
		}
248
249
		$plan['supports'] = $supports;
250
251
		self::$active_plan_cache = $plan;
252
253
		return $plan;
254
	}
255
256
	/**
257
	 * Get the site's products.
258
	 *
259
	 * @uses get_option()
260
	 *
261
	 * @access public
262
	 * @static
263
	 *
264
	 * @return array Active Jetpack products
265
	 */
266
	public static function get_products() {
267
		return get_option( self::SITE_PRODUCTS_OPTION, array() );
268
	}
269
270
	/**
271
	 * Get the class of plan and a list of features it supports
272
	 *
273
	 * @param string $plan_slug The plan that we're interested in.
274
	 * @return array Two item array, the plan class and the an array of features.
275
	 */
276
	private static function get_class_and_features( $plan_slug ) {
277
		$features = array();
278
		foreach ( self::PLAN_DATA as $class => $details ) {
279
			$features = array_merge( $features, $details['supports'] );
280
			if ( in_array( $plan_slug, $details['plans'], true ) ) {
281
				return array( $class, $features );
282
			}
283
		}
284
		return array( 'free', self::PLAN_DATA['free']['supports'] );
285
	}
286
287
	/**
288
	 * Gets the minimum plan slug that supports the given feature
289
	 *
290
	 * @param string $feature The name of the feature.
291
	 * @return string|bool The slug for the minimum plan that supports.
292
	 *  the feature or false if not found
293
	 */
294
	public static function get_minimum_plan_for_feature( $feature ) {
295
		foreach ( self::PLAN_DATA as $details ) {
296
			if ( in_array( $feature, $details['supports'], true ) ) {
297
				return $details['plans'][0];
298
			}
299
		}
300
		return false;
301
	}
302
303
	/**
304
	 * Determine whether the active plan supports a particular feature
305
	 *
306
	 * @uses Jetpack_Plan::get()
307
	 *
308
	 * @access public
309
	 * @static
310
	 *
311
	 * @param string $feature The module or feature to check.
312
	 *
313
	 * @return bool True if plan supports feature, false if not
314
	 */
315
	public static function supports( $feature ) {
316
		// Search product bypasses plan feature check.
317
		if ( 'search' === $feature && (bool) get_option( 'has_jetpack_search_product' ) ) {
318
			return true;
319
		}
320
321
		$plan = self::get();
322
323
		// Manually mapping WordPress.com features to Jetpack module slugs.
324
		foreach ( $plan['features']['active'] as $wpcom_feature ) {
325
			switch ( $wpcom_feature ) {
326
				case 'wordads-jetpack':
327
					// WordAds are supported for this site.
328
					if ( 'wordads' === $feature ) {
329
						return true;
330
					}
331
					break;
332
			}
333
		}
334
335
		if (
336
			in_array( $feature, $plan['supports'], true )
337
			|| in_array( $feature, $plan['features']['active'], true )
338
		) {
339
			return true;
340
		}
341
342
		return false;
343
	}
344
}
345