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