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