Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
75.92% covered (warning)
75.92%
227 / 299
57.89% covered (warning)
57.89%
33 / 57
CRAP
0.00% covered (danger)
0.00%
0 / 1
Product
75.92% covered (warning)
75.92%
227 / 299
57.89% covered (warning)
57.89%
33 / 57
501.18
0.00% covered (danger)
0.00%
0 / 1
 get_plugin_slug
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_plugin_filename
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 register_endpoints
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_ai_assistant_feature
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_wpcom_free_product_slug
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_installed_plugin_filename
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 get_info
96.97% covered (success)
96.97%
32 / 33
0.00% covered (danger)
0.00%
0 / 1
2
 get_related_plan_slugs
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
2.01
 get_wpcom_info
95.45% covered (success)
95.45%
21 / 22
0.00% covered (danger)
0.00%
0 / 1
3
 get_site_features_from_wpcom
52.63% covered (warning)
52.63%
10 / 19
0.00% covered (danger)
0.00%
0 / 1
5.70
 does_site_have_feature
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
3.33
 get_name
n/a
0 / 0
n/a
0 / 0
0
 get_title
n/a
0 / 0
n/a
0 / 0
0
 get_description
n/a
0 / 0
n/a
0 / 0
0
 get_long_description
n/a
0 / 0
n/a
0 / 0
0
 get_tiers
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_features
n/a
0 / 0
n/a
0 / 0
0
 get_features_by_tier
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_pricing_for_ui
n/a
0 / 0
n/a
0 / 0
0
 get_purchase_url
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_manage_url
n/a
0 / 0
n/a
0 / 0
0
 get_manage_urls_by_feature
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_post_activation_url
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_post_checkout_url
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_post_checkout_urls_by_feature
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_wpcom_product_slug
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_disclaimers
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_standalone_info
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 has_paid_plan_for_product
87.50% covered (warning)
87.50%
14 / 16
0.00% covered (danger)
0.00%
0 / 1
9.16
 has_free_plan_for_product
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 has_any_plan_for_product
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 get_paid_plan_product_slugs
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_paid_bundles_that_include_product
38.46% covered (danger)
38.46%
5 / 13
0.00% covered (danger)
0.00%
0 / 1
10.83
 get_paid_plan_purchase_for_product
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
7.02
 get_paid_plan_expiration_date
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 get_paid_plan_expiration_status
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 is_paid_plan_expired
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
 is_paid_plan_expiring
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 get_manage_paid_plan_purchase_url
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 get_renew_paid_plan_purchase_url
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 has_trial_support
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 is_upgradable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 is_bundle_product
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_upgradable_by_bundle
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_supported_products
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_owned
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
6.07
 get_status
60.53% covered (warning)
60.53%
23 / 38
0.00% covered (danger)
0.00%
0 / 1
121.20
 is_active
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
4
 is_plugin_installed
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_plugin_active
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_jetpack_plugin_installed
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_jetpack_plugin_active
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_module_active
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 activate_plugin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 do_activation
58.33% covered (warning)
58.33%
7 / 12
0.00% covered (danger)
0.00%
0 / 1
8.60
 activate
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 do_product_specific_activation
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 deactivate
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 get_plugin_actions_links
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 filter_action_links
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 extend_plugin_action_links
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 extend_core_plugin_action_links
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 install_and_activate_standalone
40.00% covered (danger)
40.00%
4 / 10
0.00% covered (danger)
0.00%
0 / 1
10.40
 does_module_need_attention
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * Base product
4 *
5 * @package my-jetpack
6 */
7
8namespace Automattic\Jetpack\My_Jetpack;
9
10use Automattic\Jetpack\Connection\Client;
11use Automattic\Jetpack\Connection\Manager as Connection_Manager;
12use Automattic\Jetpack\Modules;
13use Automattic\Jetpack\Plugins_Installer;
14use Automattic\Jetpack\Status;
15use Jetpack_Options;
16use WP_Error;
17
18/**
19 * Class responsible for handling the products
20 */
21abstract class Product {
22
23    /**
24     * The product slug
25     *
26     * @var string
27     */
28    public static $slug = null;
29
30    /**
31     * The Jetpack module name, if any.
32     *
33     * @var ?string
34     */
35    public static $module_name = null;
36
37    /**
38     * The filename (id) of the plugin associated with this product. Can be a string with a single value or a list of possible values
39     *
40     * @var string|string[]
41     */
42    public static $plugin_filename = null;
43
44    /**
45     * The slug of the plugin associated with this product. If not defined, it will default to the Jetpack plugin
46     *
47     * @var string
48     */
49    public static $plugin_slug = null;
50
51    /**
52     * The category of the product in the Jetpack ecosystem. The options are performance, growth, security, management, and create
53     *
54     * @var string
55     */
56    public static $category = null;
57
58    /**
59     * The Jetpack plugin slug
60     *
61     * @var string
62     */
63    const JETPACK_PLUGIN_SLUG = 'jetpack';
64
65    /**
66     * The Jetpack plugin filename
67     *
68     * @var array
69     */
70    const JETPACK_PLUGIN_FILENAME = array(
71        'jetpack/jetpack.php',
72        'jetpack-dev/jetpack.php',
73    );
74
75    /**
76     * The duration of time after the plan expiration date that we stop showing the plan status as "expired".
77     *
78     * @var string
79     */
80    const EXPIRATION_CUTOFF_TIME = '+2 months';
81
82    /**
83     * Transient key for storing site features
84     *
85     * @var string;
86     */
87    const MY_JETPACK_SITE_FEATURES_TRANSIENT_KEY = 'my-jetpack-site-features';
88
89    /**
90     * Whether this module is a Jetpack feature
91     *
92     * @var boolean
93     */
94    public static $is_feature = false;
95
96    /**
97     * Whether this product requires a site connection
98     *
99     * @var string
100     */
101    public static $requires_site_connection = true;
102
103    /**
104     * Whether this product requires a user connection
105     *
106     * @var string
107     */
108    public static $requires_user_connection = true;
109
110    /**
111     * Whether this product has a standalone plugin
112     *
113     * @var bool
114     */
115    public static $has_standalone_plugin = false;
116
117    /**
118     * Whether this product has a free offering
119     *
120     * @var bool
121     */
122    public static $has_free_offering = false;
123
124    /**
125     * Whether the product requires a plan to run
126     * The plan could be paid or free
127     *
128     * @var bool
129     */
130    public static $requires_plan = false;
131
132    /**
133     * Defines whether or not to show a product interstitial as tiered pricing or not
134     *
135     * @var bool
136     */
137    public static $is_tiered_pricing = false;
138
139    /**
140     * The feature slug that identifies the paid plan
141     *
142     * @var string
143     */
144    public static $feature_identifying_paid_plan = '';
145
146    /**
147     * Get the plugin slug
148     *
149     * @return ?string
150     */
151    public static function get_plugin_slug() {
152        return static::$plugin_slug;
153    }
154
155    /**
156     * Get the plugin filename
157     *
158     * @return ?string
159     */
160    public static function get_plugin_filename() {
161        return static::$plugin_filename;
162    }
163
164    /**
165     * This method will be called in the class initializer to register the product's endpoints
166     *
167     * @return void
168     */
169    public static function register_endpoints(): void {
170        // This method should be implemented in the child class.
171    }
172    /**
173     * Get data about the AI Assistant feature
174     *
175     * @return array
176     */
177    public static function get_ai_assistant_feature() {
178        // This method should be optionally set in the child class.
179        return array();
180    }
181
182    /**
183     * Get the WPCOM free product slug
184     *
185     * @return ?string
186     */
187    public static function get_wpcom_free_product_slug() {
188        return null;
189    }
190
191    /**
192     * Get the installed plugin filename, considering all possible filenames a plugin might have
193     *
194     * @param string $plugin Which plugin to check. jetpack for the jetpack plugin or product for the product specific plugin.
195     *
196     * @return ?string
197     */
198    public static function get_installed_plugin_filename( $plugin = 'product' ) {
199        $all_plugins = Plugins_Installer::get_plugins();
200        $filename    = 'jetpack' === $plugin ? self::JETPACK_PLUGIN_FILENAME : static::get_plugin_filename();
201        if ( ! is_array( $filename ) ) {
202            $filename = array( $filename );
203        }
204        foreach ( $filename as $name ) {
205            $installed = array_key_exists( $name, $all_plugins );
206            if ( $installed ) {
207                return $name;
208            }
209        }
210    }
211
212    /**
213     * Get the Static Product Info
214     *
215     * @throws \Exception If required attribute is not declared in the child class.
216     * @return array
217     */
218    public static function get_info() {
219        if ( static::$slug === null ) {
220            throw new \Exception( 'Product classes must declare the $slug attribute.' );
221        }
222        return array(
223            'slug'                            => static::$slug,
224            'plugin_slug'                     => static::get_plugin_slug(),
225            'name'                            => static::get_name(),
226            'title'                           => static::get_title(),
227            'category'                        => static::$category,
228            /* Maintain legacy compatibility with the old product info structure. See: #42271 */
229            'description'                     => static::get_description(),
230            'long_description'                => static::get_long_description(),
231            'tiers'                           => static::get_tiers(),
232            'features'                        => static::get_features(),
233            'features_by_tier'                => static::get_features_by_tier(),
234            /* End of legacy compatibility fields. */
235            'disclaimers'                     => static::get_disclaimers(),
236            'is_bundle'                       => static::is_bundle_product(),
237            'is_plugin_active'                => static::is_plugin_active(),
238            'is_tiered_pricing'               => static::$is_tiered_pricing,
239            'is_upgradable_by_bundle'         => static::is_upgradable_by_bundle(),
240            'is_feature'                      => static::$is_feature,
241            'supported_products'              => static::get_supported_products(),
242            'wpcom_product_slug'              => static::get_wpcom_product_slug(),
243            'requires_user_connection'        => static::$requires_user_connection,
244            'feature_identifying_paid_plan'   => static::$feature_identifying_paid_plan,
245            'has_free_offering'               => static::$has_free_offering,
246            'manage_url'                      => static::get_manage_url(),
247            'post_activation_url'             => static::get_post_activation_url(),
248            'post_activation_urls_by_feature' => static::get_manage_urls_by_feature(),
249            'standalone_plugin_info'          => static::get_standalone_info(),
250            'class'                           => static::class,
251            'post_checkout_url'               => static::get_post_checkout_url(),
252            'post_checkout_urls_by_feature'   => static::get_post_checkout_urls_by_feature(),
253            'related_plan_slugs'              => static::get_related_plan_slugs(),
254        );
255    }
256
257    /**
258     * Get the related plan slugs including Free and Paid ones.
259     *
260     * @return array
261     */
262    public static function get_related_plan_slugs() {
263        $slugs = array_merge(
264            static::get_paid_bundles_that_include_product(),
265            static::get_paid_plan_product_slugs()
266        );
267
268        $free_product_slug = static::get_wpcom_free_product_slug();
269
270        if ( $free_product_slug ) {
271            $slugs[] = $free_product_slug;
272        }
273
274        return $slugs;
275    }
276
277    /**
278     * Get the Product Info that requires http requests to get
279     *
280     * @throws \Exception If required attribute is not declared in the child class.
281     * @return array
282     */
283    public static function get_wpcom_info() {
284        if ( static::$slug === null ) {
285            throw new \Exception( 'Product classes must declare the $slug attribute.' );
286        }
287
288        $product_data = array(
289            'status'                        => static::get_status(),
290            'pricing_for_ui'                => static::get_pricing_for_ui(),
291            'is_upgradable'                 => static::is_upgradable(),
292            'description'                   => static::get_description(),
293            'tiers'                         => static::get_tiers(),
294            'features'                      => static::get_features(),
295            'features_by_tier'              => static::get_features_by_tier(),
296            'long_description'              => static::get_long_description(),
297            'has_any_plan_for_product'      => static::has_any_plan_for_product(),
298            'has_free_plan_for_product'     => static::has_free_plan_for_product(),
299            'has_paid_plan_for_product'     => static::has_paid_plan_for_product(),
300            'purchase_url'                  => static::get_purchase_url(),
301            'manage_paid_plan_purchase_url' => static::get_manage_paid_plan_purchase_url(),
302            'renew_paid_plan_purchase_url'  => static::get_renew_paid_plan_purchase_url(),
303            'does_module_need_attention'    => static::does_module_need_attention(),
304        );
305
306        if ( static::$slug === 'jetpack-ai' ) {
307            $product_data['ai-assistant-feature'] = static::get_ai_assistant_feature();
308        }
309
310        return $product_data;
311    }
312
313    /**
314     * Collect the site's active features
315     *
316     * @return WP_Error|array
317     */
318    public static function get_site_features_from_wpcom() {
319        static $features = null;
320
321        if ( $features !== null ) {
322            return $features;
323        }
324
325        // Check for a cached value before doing lookup
326        $stored_features = get_transient( self::MY_JETPACK_SITE_FEATURES_TRANSIENT_KEY );
327        if ( $stored_features !== false ) {
328            return $stored_features;
329        }
330
331        $site_id  = Jetpack_Options::get_option( 'id' );
332        $response = Client::wpcom_json_api_request_as_blog( sprintf( '/sites/%d/features', $site_id ), '1.1' );
333
334        if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
335            $features = new WP_Error( 'site_features_fetch_failed' );
336            return $features;
337        }
338
339        $body           = wp_remote_retrieve_body( $response );
340        $feature_return = json_decode( $body );
341
342        $features = array(
343            'active'    => $feature_return->active,
344            'available' => $feature_return->available,
345        );
346        // set a short transient to help with multiple lookups on the same page load.
347        set_transient( self::MY_JETPACK_SITE_FEATURES_TRANSIENT_KEY, $features, 15 );
348
349        return $features;
350    }
351
352    /**
353     * Check to see if the site has a feature
354     * This will check the features provided by the site plans and products (including free ones)
355     *
356     * @param string $feature - the feature to check for.
357     * @return bool
358     */
359    public static function does_site_have_feature( $feature ) {
360        if ( ! $feature ) {
361            return false;
362        }
363
364        $features = self::get_site_features_from_wpcom();
365        if ( is_wp_error( $features ) ) {
366            return false;
367        }
368
369        return in_array( $feature, $features['active'], true );
370    }
371
372    /**
373     * Get the product name
374     *
375     * @return string
376     */
377    abstract public static function get_name();
378
379    /**
380     * Get the product title
381     *
382     * @return string
383     */
384    abstract public static function get_title();
385
386    /**
387     * Get the internationalized product description
388     *
389     * @return string
390     */
391    abstract public static function get_description();
392
393    /**
394     * Get the internationalized product long description
395     *
396     * @return string
397     */
398    abstract public static function get_long_description();
399
400    /**
401     * Get the tiers for the product
402     *
403     * @return boolean|string[] The slugs of the tiers (i.e. [ "free", "basic", "advanced" ]), or False if the product has no tiers.
404     */
405    public static function get_tiers() {
406        return array();
407    }
408
409    /**
410     * Get the internationalized features list
411     *
412     * @return array
413     */
414    abstract public static function get_features();
415
416    /**
417     * Get the internationalized comparison of features grouped by each tier
418     *
419     * @return array
420     */
421    public static function get_features_by_tier() {
422        return array();
423    }
424
425    /**
426     * Get the product pricing
427     *
428     * @return array
429     */
430    abstract public static function get_pricing_for_ui();
431
432    /**
433     * Get the URL where the user can purchase the product iff it doesn't have an interstitial page in My Jetpack.
434     *
435     * @return ?string
436     */
437    public static function get_purchase_url() {
438        // Declare as concrete method as most Jetpack products use an interstitial page within My Jetpack.
439        return null;
440    }
441
442    /**
443     * Get the URL where the user manages the product
444     *
445     * @return ?string
446     */
447    abstract public static function get_manage_url();
448
449    /**
450     * Get the URL where the user manages the product for each product feature
451     *
452     * @return ?array
453     */
454    public static function get_manage_urls_by_feature() {
455        return null;
456    }
457
458    /**
459     * Get the URL the user is taken after activating the product
460     *
461     * @return ?string
462     */
463    public static function get_post_activation_url() {
464        return static::get_manage_url();
465    }
466
467    /**
468     * Get the URL the user is taken after purchasing the product through the checkout
469     *
470     * @return ?string
471     */
472    public static function get_post_checkout_url() {
473        return null;
474    }
475
476    /**
477     * Get the URL the user is taken after purchasing the product through the checkout for each product feature
478     *
479     * @return ?array
480     */
481    public static function get_post_checkout_urls_by_feature() {
482        return null;
483    }
484
485    /**
486     * Get the WPCOM product slug used to make the purchase
487     *
488     * @return ?string
489     */
490    public static function get_wpcom_product_slug() {
491        return null;
492    }
493
494    /**
495     * Get the disclaimers corresponding to a feature
496     *
497     * @return ?array
498     */
499    public static function get_disclaimers() {
500        return array();
501    }
502
503    /**
504     * Get the standalone plugin related info
505     *
506     * @return array
507     */
508    public static function get_standalone_info() {
509        $is_standalone_installed = static::$has_standalone_plugin && self::is_plugin_installed();
510        $is_standalone_active    = static::$has_standalone_plugin && self::is_plugin_active();
511
512        return array(
513            'has_standalone_plugin'   => static::$has_standalone_plugin,
514            'is_standalone_installed' => $is_standalone_installed,
515            'is_standalone_active'    => $is_standalone_active,
516        );
517    }
518
519    /**
520     * Checks whether the site has a paid plan for the product.
521     *
522     * This function relies on the product's `$feature_identifying_paid_plan` and `get_paid_plan_product_slugs()` function.
523     * If the product does not define a `$feature_identifying_paid_plan`, be sure the product includes functions for both
524     * `get_paid_plan_product_slugs()` and `get_paid_bundles_that_include_product()` which return all the product slugs and
525     * bundle slugs that include the product, respectively.
526     *
527     * @return boolean
528     */
529    public static function has_paid_plan_for_product() {
530        // First check site features (if there's a feature that identifies the paid plan)
531        if ( static::$feature_identifying_paid_plan ) {
532            if ( static::does_site_have_feature( static::$feature_identifying_paid_plan ) ) {
533                return true;
534            }
535        }
536        // Otherwise check site purchases
537        $plans_with_product = array_merge(
538            static::get_paid_bundles_that_include_product(),
539            static::get_paid_plan_product_slugs()
540        );
541
542        $purchases_data = Wpcom_Products::get_site_current_purchases();
543        if ( is_wp_error( $purchases_data ) ) {
544            return false;
545        }
546        if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
547            foreach ( $purchases_data as $purchase ) {
548                foreach ( $plans_with_product as $plan ) {
549                    if ( strpos( $purchase->product_slug, $plan ) !== false ) {
550                        return true;
551                    }
552                }
553            }
554        }
555
556        return false;
557    }
558
559    /**
560     * Checks whether the site has a free plan for the product
561     * Note, this should not return true if a product does not have a WPCOM plan (ex: search free, Akismet Free, stats free)
562     *
563     * @return false
564     */
565    public static function has_free_plan_for_product() {
566        return false;
567    }
568
569    /**
570     * Checks whether the site has any WPCOM plan for a product (paid or free)
571     *
572     * @return bool
573     */
574    public static function has_any_plan_for_product() {
575        return static::has_paid_plan_for_product() || static::has_free_plan_for_product();
576    }
577
578    /**
579     * Get the product-slugs of the paid plans for this product.
580     * (Do not include bundle plans, unless it's a bundle plan itself).
581     *
582     * @return array
583     */
584    public static function get_paid_plan_product_slugs() {
585        return array();
586    }
587
588    /**
589     * Get the product-slugs of the paid bundles/plans that this product/module is included in.
590     *
591     * This function relies on the product's `$feature_identifying_paid_plan`
592     * If the product does not define a `$feature_identifying_paid_plan`, be sure to include this
593     * function in the product's class and have it return all the paid bundle slugs that include
594     * the product.
595     *
596     * @return array
597     */
598    public static function get_paid_bundles_that_include_product() {
599        if ( static::is_bundle_product() ) {
600            return array();
601        }
602        $features = static::get_site_features_from_wpcom();
603        if ( is_wp_error( $features ) ) {
604            return array();
605        }
606        $idendifying_feature = static::$feature_identifying_paid_plan;
607        if ( empty( $features['available'] ) ) {
608            return array();
609        }
610        $paid_bundles   = $features['available']->$idendifying_feature ?? array();
611        $current_bundle = Wpcom_Products::get_site_current_plan( true );
612
613        if ( in_array( static::$feature_identifying_paid_plan, $current_bundle['features']['active'], true ) ) {
614            $paid_bundles[] = $current_bundle['product_slug'];
615        }
616
617        return $paid_bundles;
618    }
619
620    /**
621     * Gets the paid plan's purchase/subsciption info, or null if no paid plan purchases.
622     *
623     * @return object|null
624     */
625    public static function get_paid_plan_purchase_for_product() {
626        $paid_plans = array_merge(
627            static::get_paid_plan_product_slugs(),
628            static::get_paid_bundles_that_include_product()
629        );
630
631        $purchases_data = Wpcom_Products::get_site_current_purchases();
632        if ( is_wp_error( $purchases_data ) ) {
633            return null;
634        }
635
636        if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
637            foreach ( $purchases_data as $purchase ) {
638                foreach ( $paid_plans as $plan ) {
639                    if ( strpos( $purchase->product_slug, $plan ) !== false ) {
640                        return $purchase;
641                    }
642                }
643            }
644        }
645
646        return null;
647    }
648
649    /**
650     * Gets the paid plan's expiry date.
651     *
652     * @return string
653     */
654    public static function get_paid_plan_expiration_date() {
655        $purchase = static::get_paid_plan_purchase_for_product();
656        if ( ! $purchase ) {
657            return 'paid-plan-does-not-exist';
658        }
659
660        return $purchase->expiry_date;
661    }
662
663    /**
664     * Gets the paid plan's expiry status.
665     *
666     * @return string
667     */
668    public static function get_paid_plan_expiration_status() {
669        $purchase = static::get_paid_plan_purchase_for_product();
670        if ( ! $purchase ) {
671            return 'paid-plan-does-not-exist';
672        }
673
674        return $purchase->expiry_status;
675    }
676
677    /**
678     * Checks if the paid plan is expired or not.
679     *
680     * @param bool $not_expired_after_cutoff - whether to not return the plan as expired if the plan has been expired for some duration of time.
681     * @return bool
682     */
683    public static function is_paid_plan_expired( $not_expired_after_cutoff = false ) {
684        $expiry_status = static::get_paid_plan_expiration_status();
685        $expiry_date   = static::get_paid_plan_expiration_date();
686        $expiry_cutoff = strtotime( $expiry_date . ' ' . self::EXPIRATION_CUTOFF_TIME );
687
688        return $not_expired_after_cutoff
689            ? $expiry_status === Products::STATUS_EXPIRED && strtotime( 'now' ) < $expiry_cutoff
690            : $expiry_status === Products::STATUS_EXPIRED;
691    }
692
693    /**
694     * Checks if the paid plan is expiring soon or not.
695     *
696     * @return bool
697     */
698    public static function is_paid_plan_expiring() {
699        $expiry_status = static::get_paid_plan_expiration_status();
700
701        return $expiry_status === Products::STATUS_EXPIRING_SOON;
702    }
703
704    /**
705     * Gets the url to manage the paid plan's purchased subscription (for plan renewal, canceling, removal, etc).
706     *
707     * @return string|null The url to the purchase management page.
708     */
709    public static function get_manage_paid_plan_purchase_url() {
710        $purchase    = static::get_paid_plan_purchase_for_product();
711        $site_suffix = ( new Status() )->get_site_suffix();
712
713        if ( $purchase && $site_suffix ) {
714            return 'https://wordpress.com/me/purchases/' . $site_suffix . '/' . $purchase->ID;
715        }
716
717        return null;
718    }
719
720    /**
721     * Gets the url to renew the paid plan's purchased subscription.
722     *
723     * @return string|null The url to the checkout renewal page.
724     */
725    public static function get_renew_paid_plan_purchase_url() {
726        $purchase    = static::get_paid_plan_purchase_for_product();
727        $site_suffix = ( new Status() )->get_site_suffix();
728
729        if ( $purchase && $site_suffix ) {
730            return 'https://wordpress.com/checkout/' . $purchase->product_slug . '/renew/' . $purchase->ID . '/' . $site_suffix;
731        }
732
733        return null;
734    }
735
736    /**
737     * Checks whether the product supports trial or not
738     *
739     * Returns true if it supports. Return false otherwise.
740     *
741     * Free products will always return false.
742     *
743     * @return boolean
744     */
745    public static function has_trial_support() {
746        return false;
747    }
748
749    /**
750     * Checks whether the product can be upgraded to a different product.
751     *
752     * @return boolean
753     */
754    public static function is_upgradable() {
755        return ! static::has_paid_plan_for_product() && ! static::is_bundle_product();
756    }
757
758    /**
759     * Checks whether product is a bundle.
760     *
761     * @return boolean True if product is a bundle. Otherwise, False.
762     */
763    public static function is_bundle_product() {
764        return false;
765    }
766
767    /**
768     * Check whether the product is upgradable
769     * by a product bundle.
770     *
771     * @return boolean|array Bundles list or False if not upgradable by a bundle.
772     */
773    public static function is_upgradable_by_bundle() {
774        return false;
775    }
776
777    /**
778     * In case it's a bundle product,
779     * return all the products it contains.
780     * Empty array by default.
781     *
782     * @return array Product slugs
783     */
784    public static function get_supported_products() {
785        return array();
786    }
787
788    /**
789     * Determine if the product is owned or not
790     * An owned product is defined as a product that is any of the following
791     * - Active
792     * - Has historically been active
793     * - The user has a plan that includes the product
794     * - The user has the standalone plugin for the product installed
795     *
796     * @return boolean
797     */
798    public static function is_owned() {
799        $historically_active_modules = Jetpack_Options::get_option( 'historically_active_modules', array() );
800        $standalone_info             = static::get_standalone_info();
801        if ( ( static::is_active() && Jetpack_Options::get_option( 'id' ) ) ||
802            $standalone_info['is_standalone_installed'] ||
803            in_array( static::$slug, $historically_active_modules, true ) ||
804            static::has_any_plan_for_product()
805        ) {
806            return true;
807        }
808
809        return false;
810    }
811
812    /**
813     * Undocumented function
814     *
815     * @return string
816     */
817    public static function get_status() {
818        if ( ! static::is_plugin_installed() ) {
819            $status = Products::STATUS_PLUGIN_ABSENT;
820            if ( static::has_paid_plan_for_product() ) {
821                $status = Products::STATUS_PLUGIN_ABSENT_WITH_PLAN;
822            }
823        } elseif ( static::is_active() ) {
824            $status = Products::STATUS_ACTIVE;
825            // We only consider missing site & user connection an error when the Product is active.
826            if ( static::$requires_site_connection && ! ( new Connection_Manager() )->is_connected() ) {
827                // Site has never been connected before
828                if ( ! Jetpack_Options::get_option( 'id' ) && ! static::is_owned() ) {
829                    $status = Products::STATUS_NEEDS_FIRST_SITE_CONNECTION;
830                } else {
831                    $status = Products::STATUS_SITE_CONNECTION_ERROR;
832                }
833            } elseif ( static::$requires_user_connection && ! ( new Connection_Manager() )->has_connected_owner() ) {
834                $status = Products::STATUS_USER_CONNECTION_ERROR;
835            } elseif ( static::has_paid_plan_for_product() ) {
836                $needs_attention = static::does_module_need_attention();
837                if ( ! empty( $needs_attention ) && is_array( $needs_attention ) ) {
838                    $status = Products::STATUS_NEEDS_ATTENTION__WARNING;
839                    if ( isset( $needs_attention['type'] ) && 'error' === $needs_attention['type'] ) {
840                        $status = Products::STATUS_NEEDS_ATTENTION__ERROR;
841                    }
842                }
843                if ( static::is_paid_plan_expired() ) {
844                    $status = Products::STATUS_EXPIRED;
845                } elseif ( static::is_paid_plan_expiring() ) {
846                    $status = Products::STATUS_EXPIRING_SOON;
847                }
848            } elseif ( static::is_upgradable() ) {
849                $status = Products::STATUS_CAN_UPGRADE;
850            }
851            // Check specifically for inactive modules, which will prevent a product from being active
852        } elseif ( static::$module_name && ! static::is_module_active() ) {
853            $status = Products::STATUS_MODULE_DISABLED;
854            // If there is not a plan associated with the disabled module, encourage a plan first
855            // Getting a plan set up should help resolve any connection issues
856            // However if the standalone plugin for this product is active, then we will defer to showing errors that prevent the module from being active
857            // This is because if a standalone plugin is installed, we expect the product to not show as "inactive" on My Jetpack
858            if ( static::$requires_plan || ( ! static::has_any_plan_for_product() && static::$has_standalone_plugin && ! self::is_plugin_active() ) ) {
859                $status = static::is_owned() && static::$has_free_offering && ! static::$requires_plan ? Products::STATUS_NEEDS_ACTIVATION : Products::STATUS_NEEDS_PLAN;
860            } elseif ( static::$requires_site_connection && ! ( new Connection_Manager() )->is_connected() ) {
861                // Site has never been connected before and product is not owned
862                if ( ! Jetpack_Options::get_option( 'id' ) && ! static::is_owned() ) {
863                    $status = Products::STATUS_NEEDS_FIRST_SITE_CONNECTION;
864                } else {
865                    $status = Products::STATUS_SITE_CONNECTION_ERROR;
866                }
867            } elseif ( static::$requires_user_connection && ! ( new Connection_Manager() )->has_connected_owner() ) {
868                $status = Products::STATUS_USER_CONNECTION_ERROR;
869            }
870        } elseif ( ! static::has_any_plan_for_product() ) {
871            $status = static::is_owned() && static::$has_free_offering && ! static::$requires_plan ? Products::STATUS_NEEDS_ACTIVATION : Products::STATUS_NEEDS_PLAN;
872        } else {
873            $status = Products::STATUS_INACTIVE;
874        }
875        return $status;
876    }
877
878    /**
879     * Checks whether the Product is active
880     *
881     * @return boolean
882     */
883    public static function is_active() {
884        return static::is_plugin_active() && ( static::has_any_plan_for_product() || ( ! static::$requires_plan && static::$has_free_offering ) );
885    }
886
887    /**
888     * Checks whether the plugin is installed
889     *
890     * @return boolean
891     */
892    public static function is_plugin_installed() {
893        return (bool) static::get_installed_plugin_filename();
894    }
895
896    /**
897     * Checks whether the plugin is active
898     *
899     * @return boolean
900     */
901    public static function is_plugin_active() {
902        return Plugins_Installer::is_plugin_active( static::get_installed_plugin_filename() );
903    }
904
905    /**
906     * Checks whether the Jetpack plugin is installed
907     *
908     * @return boolean
909     */
910    public static function is_jetpack_plugin_installed() {
911        return (bool) static::get_installed_plugin_filename( 'jetpack' );
912    }
913
914    /**
915     * Checks whether the Jetpack plugin is active
916     *
917     * @return boolean
918     */
919    public static function is_jetpack_plugin_active() {
920        return Plugins_Installer::is_plugin_active( static::get_installed_plugin_filename( 'jetpack' ) );
921    }
922
923    /**
924     * Checks whether the Jetpack module is active only if a module_name is defined
925     *
926     * @return bool
927     */
928    public static function is_module_active() {
929        if ( static::$module_name ) {
930            return ( new Modules() )->is_active( static::$module_name );
931        }
932        return true;
933    }
934
935    /**
936     * Activates the plugin
937     *
938     * @return null|WP_Error Null on success, WP_Error on invalid file.
939     */
940    public static function activate_plugin() {
941        return activate_plugin( static::get_installed_plugin_filename() );
942    }
943
944    /**
945     * Perform the top level activation routines, which is installing and activating the required plugin
946     *
947     * @return bool|WP_Error
948     */
949    private static function do_activation() {
950        if ( static::is_active() ) {
951            return true;
952        }
953
954        // Default to installing the standalone plugin for the product
955        if ( ! self::is_plugin_installed() ) {
956            $installed = Plugins_Installer::install_plugin( static::get_plugin_slug() );
957            if ( is_wp_error( $installed ) ) {
958                return $installed;
959            }
960        }
961
962        if ( ! current_user_can( 'activate_plugins' ) ) {
963            return new WP_Error( 'not_allowed', __( 'You are not allowed to activate plugins on this site.', 'jetpack-my-jetpack' ) );
964        }
965
966        $result = static::activate_plugin();
967        if ( is_wp_error( $result ) ) {
968            return $result;
969        }
970
971        return true;
972    }
973
974    /**
975     * Activates the product by installing and activating its plugin
976     *
977     * @return boolean|WP_Error
978     */
979    final public static function activate() {
980
981        $result = self::do_activation();
982
983        $result = static::do_product_specific_activation( $result );
984
985        $product_slug = static::$slug;
986
987        /**
988         * Fires after My Jetpack activates a product and filters the result
989         * Use this filter to run additional routines for a product activation on stand-alone plugins
990         *
991         * @param bool|WP_Error $result The result of the previous steps of activation.
992         */
993        $result = apply_filters( "my_jetpack_{$product_slug}_activation", $result );
994
995        return $result;
996    }
997
998    /**
999     * Override this method to perform product specific activation routines.
1000     *
1001     * @param bool|WP_Error $current_result Is the result of the top level activation actions. You probably won't do anything if it is an WP_Error.
1002     * @return bool|WP_Error
1003     */
1004    public static function do_product_specific_activation( $current_result ) {
1005        return $current_result;
1006    }
1007
1008    /**
1009     * Deactivate the product
1010     *
1011     * @return boolean
1012     */
1013    public static function deactivate() {
1014        deactivate_plugins( static::get_installed_plugin_filename() );
1015        return true;
1016    }
1017
1018    /**
1019     * Returns filtered Jetpack plugin actions links.
1020     *
1021     * @param array $actions - Jetpack plugin action links.
1022     * @return array           Filtered Jetpack plugin actions links.
1023     */
1024    public static function get_plugin_actions_links( $actions ) {
1025        // My Jetpack action link.
1026        $my_jetpack_home_link = array(
1027            'jetpack-home' => sprintf(
1028                '<a href="%1$s" title="%3$s">%2$s</a>',
1029                admin_url( 'admin.php?page=my-jetpack' ),
1030                __( 'My Jetpack', 'jetpack-my-jetpack' ),
1031                __( 'My Jetpack dashboard', 'jetpack-my-jetpack' )
1032            ),
1033        );
1034
1035        // Otherwise, add it to the beginning of the array.
1036        return array_merge( $my_jetpack_home_link, $actions );
1037    }
1038
1039    /**
1040     * Filter the action links for the plugins specified.
1041     *
1042     * @param string|string[] $filenames The plugin filename(s) to filter the action links for.
1043     */
1044    private static function filter_action_links( $filenames ) {
1045        foreach ( $filenames as $filename ) {
1046            $hook     = 'plugin_action_links_' . $filename;
1047            $callback = array( static::class, 'get_plugin_actions_links' );
1048            if ( ! has_filter( $hook, $callback ) ) {
1049                add_filter( $hook, $callback, 20, 2 );
1050            }
1051        }
1052    }
1053
1054    /**
1055     * Extend the plugin action links.
1056     */
1057    public static function extend_plugin_action_links() {
1058        $filenames = static::get_plugin_filename();
1059        if ( ! is_array( $filenames ) ) {
1060            $filenames = array( $filenames );
1061        }
1062
1063        self::filter_action_links( $filenames );
1064    }
1065
1066    /**
1067     * Extend the Jetpack plugin action links.
1068     */
1069    public static function extend_core_plugin_action_links() {
1070        $filenames = self::JETPACK_PLUGIN_FILENAME;
1071
1072        self::filter_action_links( $filenames );
1073    }
1074
1075    /**
1076     * Install and activate the standalone plugin in the case it's missing.
1077     *
1078     * @return boolean|WP_Error
1079     */
1080    public static function install_and_activate_standalone() {
1081        /**
1082         * Check for the presence of the standalone plugin, ignoring Jetpack presence.
1083         *
1084         * If the standalone plugin is not installed and the user can install plugins, proceed with the installation.
1085         */
1086        if ( ! static::is_plugin_installed() ) {
1087            /**
1088             * Check for permissions
1089             */
1090            if ( ! current_user_can( 'install_plugins' ) ) {
1091                return new WP_Error( 'not_allowed', __( 'You are not allowed to install plugins on this site.', 'jetpack-my-jetpack' ) );
1092            }
1093
1094            /**
1095             * Install the plugin
1096             */
1097            $installed = Plugins_Installer::install_plugin( static::get_plugin_slug() );
1098            if ( is_wp_error( $installed ) ) {
1099                return $installed;
1100            }
1101        }
1102
1103        /**
1104         * Activate the installed plugin
1105         */
1106        $result = static::activate_plugin();
1107
1108        if ( is_wp_error( $result ) ) {
1109            return $result;
1110        }
1111
1112        return true;
1113    }
1114
1115    /**
1116     * Determines whether the module/plugin/product needs the users attention.
1117     * Typically due to some sort of error where user troubleshooting is needed.
1118     *
1119     * @return boolean
1120     */
1121    public static function does_module_need_attention() {
1122        return false;
1123    }
1124}