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