Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.15% covered (warning)
86.15%
280 / 325
70.49% covered (warning)
70.49%
43 / 61
CRAP
0.00% covered (danger)
0.00%
0 / 1
Product
86.15% covered (warning)
86.15%
280 / 325
70.49% covered (warning)
70.49%
43 / 61
264.05
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_bundle_pricing
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
13
 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
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 get_module_name
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 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
10.54
 should_use_standalone_plugin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 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     * Prepare supported billing terms, gating only Pro on catalog availability.
490     *
491     * @since $$next-version$$
492     * @param string[] $product_slugs Store product slugs, preferred term first.
493     * @return array Pricing details and term choices.
494     */
495    protected static function get_bundle_pricing( $product_slugs ) {
496        $terms = array();
497        foreach ( $product_slugs as $slug ) {
498            $product           = Wpcom_Products::get_product( $slug );
499            $valid_product     = is_object( $product ) && isset( $product->cost ) && isset( $product->currency_code ) && isset( $product->product_term ) && is_numeric( $product->cost ) && $product->cost >= 0;
500            $term              = array_merge(
501                array(
502                    'available'          => false,
503                    'wpcom_product_slug' => $slug,
504                ),
505                $valid_product ? Wpcom_Products::get_product_pricing( $slug ) : array()
506            );
507            $expected_term     = str_ends_with( $slug, '_monthly' ) ? 'month' : ( str_ends_with( $slug, '_bi_yearly' ) ? 'two years' : 'year' );
508            $on_sale           = ! str_starts_with( $slug, 'jetpack_pro_' ) || true === ( $product->available ?? false );
509            $term['available'] = $valid_product && $on_sale && ( $term['product_term'] ?? '' ) === $expected_term;
510            $terms[]           = $term;
511        }
512        $available = array_values( array_filter( $terms, fn( $term ) => true === $term['available'] ) );
513        return array_merge( $available[0] ?? $terms[0], array( 'terms' => $terms ) );
514    }
515
516    /**
517     * Get the URL where the user can purchase the product iff it doesn't have an interstitial page in My Jetpack.
518     *
519     * @return ?string
520     */
521    public static function get_purchase_url() {
522        // Declare as concrete method as most Jetpack products use an interstitial page within My Jetpack.
523        return null;
524    }
525
526    /**
527     * Get the URL where the user manages the product
528     *
529     * @return ?string
530     */
531    abstract public static function get_manage_url();
532
533    /**
534     * Get the URL where the user manages the product for each product feature
535     *
536     * @return ?array
537     */
538    public static function get_manage_urls_by_feature() {
539        return null;
540    }
541
542    /**
543     * Get the URL the user is taken after activating the product
544     *
545     * @return ?string
546     */
547    public static function get_post_activation_url() {
548        return static::get_manage_url();
549    }
550
551    /**
552     * Get the URL the user is taken after purchasing the product through the checkout
553     *
554     * @return ?string
555     */
556    public static function get_post_checkout_url() {
557        return null;
558    }
559
560    /**
561     * Get the URL the user is taken after purchasing the product through the checkout for each product feature
562     *
563     * @return ?array
564     */
565    public static function get_post_checkout_urls_by_feature() {
566        return null;
567    }
568
569    /**
570     * Get the WPCOM product slug used to make the purchase
571     *
572     * @return ?string
573     */
574    public static function get_wpcom_product_slug() {
575        return null;
576    }
577
578    /**
579     * Get the disclaimers corresponding to a feature
580     *
581     * @return ?array
582     */
583    public static function get_disclaimers() {
584        return array();
585    }
586
587    /**
588     * Get the standalone plugin related info
589     *
590     * @return array
591     */
592    public static function get_standalone_info() {
593        $is_standalone_installed = static::$has_standalone_plugin && self::is_plugin_installed();
594        $is_standalone_active    = static::$has_standalone_plugin && self::is_plugin_active();
595
596        return array(
597            'has_standalone_plugin'   => static::$has_standalone_plugin,
598            'is_standalone_installed' => $is_standalone_installed,
599            'is_standalone_active'    => $is_standalone_active,
600        );
601    }
602
603    /**
604     * Checks whether the site has a paid plan for the product.
605     *
606     * This function relies on the product's `$feature_identifying_paid_plan` and `get_paid_plan_product_slugs()` function.
607     * If the product does not define a `$feature_identifying_paid_plan`, be sure the product includes functions for both
608     * `get_paid_plan_product_slugs()` and `get_paid_bundles_that_include_product()` which return all the product slugs and
609     * bundle slugs that include the product, respectively.
610     *
611     * @return boolean
612     */
613    public static function has_paid_plan_for_product() {
614        // First check site features (if there's a feature that identifies the paid plan)
615        if ( static::$feature_identifying_paid_plan ) {
616            if ( static::does_site_have_feature( static::$feature_identifying_paid_plan ) ) {
617                return true;
618            }
619        }
620        // Otherwise check site purchases
621        $plans_with_product = array_merge(
622            static::get_paid_bundles_that_include_product(),
623            static::get_paid_plan_product_slugs()
624        );
625
626        $purchases_data = Wpcom_Products::get_site_current_purchases();
627        if ( is_wp_error( $purchases_data ) ) {
628            return false;
629        }
630        if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
631            foreach ( $purchases_data as $purchase ) {
632                foreach ( $plans_with_product as $plan ) {
633                    if ( strpos( $purchase->product_slug, $plan ) !== false ) {
634                        return true;
635                    }
636                }
637            }
638        }
639
640        return false;
641    }
642
643    /**
644     * Checks whether the site has a free plan for the product
645     * Note, this should not return true if a product does not have a WPCOM plan (ex: search free, Akismet Free, stats free)
646     *
647     * @return false
648     */
649    public static function has_free_plan_for_product() {
650        return false;
651    }
652
653    /**
654     * Checks whether the site has any WPCOM plan for a product (paid or free)
655     *
656     * @return bool
657     */
658    public static function has_any_plan_for_product() {
659        return static::has_paid_plan_for_product() || static::has_free_plan_for_product();
660    }
661
662    /**
663     * Get the product-slugs of the paid plans for this product.
664     * (Do not include bundle plans, unless it's a bundle plan itself).
665     *
666     * @return array
667     */
668    public static function get_paid_plan_product_slugs() {
669        return array();
670    }
671
672    /**
673     * Get the product-slugs of the paid bundles/plans that this product/module is included in.
674     *
675     * This function relies on the product's `$feature_identifying_paid_plan`
676     * If the product does not define a `$feature_identifying_paid_plan`, be sure to include this
677     * function in the product's class and have it return all the paid bundle slugs that include
678     * the product.
679     *
680     * @return array
681     */
682    public static function get_paid_bundles_that_include_product() {
683        if ( static::is_bundle_product() ) {
684            return array();
685        }
686        $features = static::get_site_features_from_wpcom();
687        if ( is_wp_error( $features ) ) {
688            return array();
689        }
690        $idendifying_feature = static::$feature_identifying_paid_plan;
691        if ( empty( $features['available'] ) ) {
692            return array();
693        }
694        $paid_bundles   = $features['available']->$idendifying_feature ?? array();
695        $current_bundle = Wpcom_Products::get_site_current_plan( true );
696
697        if ( in_array( static::$feature_identifying_paid_plan, $current_bundle['features']['active'], true ) ) {
698            $paid_bundles[] = $current_bundle['product_slug'];
699        }
700
701        return $paid_bundles;
702    }
703
704    /**
705     * Gets the paid plan's purchase/subsciption info, or null if no paid plan purchases.
706     *
707     * @return object|null
708     */
709    public static function get_paid_plan_purchase_for_product() {
710        $paid_plans = array_merge(
711            static::get_paid_plan_product_slugs(),
712            static::get_paid_bundles_that_include_product()
713        );
714
715        $purchases_data = Wpcom_Products::get_site_current_purchases();
716        if ( is_wp_error( $purchases_data ) ) {
717            return null;
718        }
719
720        if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
721            foreach ( $purchases_data as $purchase ) {
722                foreach ( $paid_plans as $plan ) {
723                    if ( strpos( $purchase->product_slug, $plan ) !== false ) {
724                        return $purchase;
725                    }
726                }
727            }
728        }
729
730        return null;
731    }
732
733    /**
734     * Gets the paid plan's expiry date.
735     *
736     * @return string
737     */
738    public static function get_paid_plan_expiration_date() {
739        $purchase = static::get_paid_plan_purchase_for_product();
740        if ( ! $purchase ) {
741            return 'paid-plan-does-not-exist';
742        }
743
744        return $purchase->expiry_date;
745    }
746
747    /**
748     * Gets the paid plan's expiry status.
749     *
750     * @return string
751     */
752    public static function get_paid_plan_expiration_status() {
753        $purchase = static::get_paid_plan_purchase_for_product();
754        if ( ! $purchase ) {
755            return 'paid-plan-does-not-exist';
756        }
757
758        return $purchase->expiry_status;
759    }
760
761    /**
762     * Checks if the paid plan is expired or not.
763     *
764     * @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.
765     * @return bool
766     */
767    public static function is_paid_plan_expired( $not_expired_after_cutoff = false ) {
768        $expiry_status = static::get_paid_plan_expiration_status();
769        $expiry_date   = static::get_paid_plan_expiration_date();
770        $expiry_cutoff = strtotime( $expiry_date . ' ' . self::EXPIRATION_CUTOFF_TIME );
771
772        return $not_expired_after_cutoff
773            ? $expiry_status === Products::STATUS_EXPIRED && strtotime( 'now' ) < $expiry_cutoff
774            : $expiry_status === Products::STATUS_EXPIRED;
775    }
776
777    /**
778     * Checks if the paid plan is expiring soon or not.
779     *
780     * @return bool
781     */
782    public static function is_paid_plan_expiring() {
783        $expiry_status = static::get_paid_plan_expiration_status();
784
785        return $expiry_status === Products::STATUS_EXPIRING_SOON;
786    }
787
788    /**
789     * Gets the url to manage the paid plan's purchased subscription (for plan renewal, canceling, removal, etc).
790     *
791     * @return string|null The url to the purchase management page.
792     */
793    public static function get_manage_paid_plan_purchase_url() {
794        $purchase    = static::get_paid_plan_purchase_for_product();
795        $site_suffix = ( new Status() )->get_site_suffix();
796
797        if ( $purchase && $site_suffix ) {
798            return 'https://wordpress.com/me/purchases/' . $site_suffix . '/' . $purchase->ID;
799        }
800
801        return null;
802    }
803
804    /**
805     * Gets the url to renew the paid plan's purchased subscription.
806     *
807     * @return string|null The url to the checkout renewal page.
808     */
809    public static function get_renew_paid_plan_purchase_url() {
810        $purchase    = static::get_paid_plan_purchase_for_product();
811        $site_suffix = ( new Status() )->get_site_suffix();
812
813        if ( $purchase && $site_suffix ) {
814            return 'https://wordpress.com/checkout/' . $purchase->product_slug . '/renew/' . $purchase->ID . '/' . $site_suffix;
815        }
816
817        return null;
818    }
819
820    /**
821     * Checks whether the product supports trial or not
822     *
823     * Returns true if it supports. Return false otherwise.
824     *
825     * Free products will always return false.
826     *
827     * @return boolean
828     */
829    public static function has_trial_support() {
830        return false;
831    }
832
833    /**
834     * Checks whether the product can be upgraded to a different product.
835     *
836     * @return boolean
837     */
838    public static function is_upgradable() {
839        return ! static::has_paid_plan_for_product() && ! static::is_bundle_product();
840    }
841
842    /**
843     * Checks whether product is a bundle.
844     *
845     * @return boolean True if product is a bundle. Otherwise, False.
846     */
847    public static function is_bundle_product() {
848        return false;
849    }
850
851    /**
852     * Check whether the product is upgradable
853     * by a product bundle.
854     *
855     * @return boolean|array Bundles list or False if not upgradable by a bundle.
856     */
857    public static function is_upgradable_by_bundle() {
858        return false;
859    }
860
861    /**
862     * In case it's a bundle product,
863     * return all the products it contains.
864     * Empty array by default.
865     *
866     * @return array Product slugs
867     */
868    public static function get_supported_products() {
869        return array();
870    }
871
872    /**
873     * Determine if the product is owned or not
874     * An owned product is defined as a product that is any of the following
875     * - Active
876     * - Has historically been active
877     * - The user has a plan that includes the product
878     * - The user has the standalone plugin for the product installed
879     *
880     * @return boolean
881     */
882    public static function is_owned() {
883        $historically_active_modules = Jetpack_Options::get_option( 'historically_active_modules', array() );
884        $standalone_info             = static::get_standalone_info();
885        if ( ( static::is_active() && Jetpack_Options::get_option( 'id' ) ) ||
886            $standalone_info['is_standalone_installed'] ||
887            in_array( static::$slug, $historically_active_modules, true ) ||
888            static::has_any_plan_for_product()
889        ) {
890            return true;
891        }
892
893        return false;
894    }
895
896    /**
897     * Undocumented function
898     *
899     * @return string
900     */
901    public static function get_status() {
902        if ( ! static::is_plugin_installed() ) {
903            $status = Products::STATUS_PLUGIN_ABSENT;
904            if ( static::has_paid_plan_for_product() ) {
905                $status = Products::STATUS_PLUGIN_ABSENT_WITH_PLAN;
906            }
907        } elseif ( static::is_active() ) {
908            $status = Products::STATUS_ACTIVE;
909            // We only consider missing site & user connection an error when the Product is active.
910            if ( static::$requires_site_connection && ! ( new Connection_Manager() )->is_connected() ) {
911                // Site has never been connected before
912                if ( ! Jetpack_Options::get_option( 'id' ) && ! static::is_owned() ) {
913                    $status = Products::STATUS_NEEDS_FIRST_SITE_CONNECTION;
914                } else {
915                    $status = Products::STATUS_SITE_CONNECTION_ERROR;
916                }
917            } elseif ( static::$requires_user_connection && ! ( new Connection_Manager() )->has_connected_owner() ) {
918                $status = Products::STATUS_USER_CONNECTION_ERROR;
919            } elseif ( static::has_paid_plan_for_product() ) {
920                $needs_attention = static::does_module_need_attention();
921                if ( ! empty( $needs_attention ) && is_array( $needs_attention ) ) {
922                    $status = Products::STATUS_NEEDS_ATTENTION__WARNING;
923                    if ( isset( $needs_attention['type'] ) && 'error' === $needs_attention['type'] ) {
924                        $status = Products::STATUS_NEEDS_ATTENTION__ERROR;
925                    }
926                }
927                if ( static::is_paid_plan_expired() ) {
928                    $status = Products::STATUS_EXPIRED;
929                } elseif ( static::is_paid_plan_expiring() ) {
930                    $status = Products::STATUS_EXPIRING_SOON;
931                }
932            } elseif ( static::is_upgradable() ) {
933                $status = Products::STATUS_CAN_UPGRADE;
934            }
935            // Check specifically for inactive modules, which will prevent a product from being active
936            // Bundles have no module of their own, so they fall through to the plan checks below.
937        } elseif ( static::get_module_name() && ! static::is_bundle_product() && ! static::is_module_active() ) {
938            $status = Products::STATUS_MODULE_DISABLED;
939            // If there is not a plan associated with the disabled module, encourage a plan first
940            // Getting a plan set up should help resolve any connection issues
941            // However if the standalone plugin for this product is active, then we will defer to showing errors that prevent the module from being active
942            // This is because if a standalone plugin is installed, we expect the product to not show as "inactive" on My Jetpack
943            if ( ! static::has_any_plan_for_product() && ( static::$requires_plan || ( static::$has_standalone_plugin && ! self::is_plugin_active() ) ) ) {
944                $status = static::is_owned() && static::$has_free_offering && ! static::$requires_plan ? Products::STATUS_NEEDS_ACTIVATION : Products::STATUS_NEEDS_PLAN;
945            } elseif ( static::$requires_site_connection && ! ( new Connection_Manager() )->is_connected() ) {
946                // Site has never been connected before and product is not owned
947                if ( ! Jetpack_Options::get_option( 'id' ) && ! static::is_owned() ) {
948                    $status = Products::STATUS_NEEDS_FIRST_SITE_CONNECTION;
949                } else {
950                    $status = Products::STATUS_SITE_CONNECTION_ERROR;
951                }
952            } elseif ( static::$requires_user_connection && ! ( new Connection_Manager() )->has_connected_owner() ) {
953                $status = Products::STATUS_USER_CONNECTION_ERROR;
954            }
955        } elseif ( ! static::has_any_plan_for_product() ) {
956            $status = static::is_owned() && static::$has_free_offering && ! static::$requires_plan ? Products::STATUS_NEEDS_ACTIVATION : Products::STATUS_NEEDS_PLAN;
957        } else {
958            $status = Products::STATUS_INACTIVE;
959        }
960        return $status;
961    }
962
963    /**
964     * Checks whether the Product is active
965     *
966     * @return boolean
967     */
968    public static function is_active() {
969        return static::is_plugin_active() && ( static::has_any_plan_for_product() || ( ! static::$requires_plan && static::$has_free_offering ) );
970    }
971
972    /**
973     * Checks whether the site has switched the product on, whether or not it has a plan for it.
974     *
975     * Unlike is_active(), this never asks WordPress.com, so it is safe on every admin page load.
976     * An override must keep it that way.
977     *
978     * @return boolean
979     */
980    public static function is_activated() {
981        return static::is_plugin_active();
982    }
983
984    /**
985     * Checks whether the plugin is installed
986     *
987     * @return boolean
988     */
989    public static function is_plugin_installed() {
990        return (bool) static::get_installed_plugin_filename();
991    }
992
993    /**
994     * Checks whether the plugin is active
995     *
996     * @return boolean
997     */
998    public static function is_plugin_active() {
999        return Plugins_Installer::is_plugin_active( static::get_installed_plugin_filename() );
1000    }
1001
1002    /**
1003     * Checks whether the Jetpack plugin is installed
1004     *
1005     * @return boolean
1006     */
1007    public static function is_jetpack_plugin_installed() {
1008        return (bool) static::get_installed_plugin_filename( 'jetpack' );
1009    }
1010
1011    /**
1012     * Checks whether the Jetpack plugin is active
1013     *
1014     * @return boolean
1015     */
1016    public static function is_jetpack_plugin_active() {
1017        return Plugins_Installer::is_plugin_active( static::get_installed_plugin_filename( 'jetpack' ) );
1018    }
1019
1020    /**
1021     * Checks whether the Jetpack module is active only if a module_name is defined
1022     *
1023     * @return bool
1024     */
1025    public static function is_module_active() {
1026        $module_name = static::get_module_name();
1027        if ( $module_name ) {
1028            return ( new Modules() )->is_active( $module_name );
1029        }
1030        return true;
1031    }
1032
1033    /**
1034     * Get the Jetpack module that switches the product on, if any.
1035     *
1036     * @since $$next-version$$
1037     *
1038     * @return ?string
1039     */
1040    public static function get_module_name() {
1041        return static::$module_name;
1042    }
1043
1044    /**
1045     * Activates the plugin
1046     *
1047     * @return null|WP_Error Null on success, WP_Error on invalid file.
1048     */
1049    public static function activate_plugin() {
1050        return activate_plugin( static::get_installed_plugin_filename() );
1051    }
1052
1053    /**
1054     * Perform the top level activation routines, which is installing and activating the required plugin
1055     *
1056     * @return bool|WP_Error
1057     */
1058    private static function do_activation() {
1059        if ( static::is_active() ) {
1060            return true;
1061        }
1062
1063        // Default to installing the standalone plugin for the product
1064        if ( ! self::is_plugin_installed() && static::should_use_standalone_plugin() ) {
1065            $installed = Plugins_Installer::install_plugin( static::get_plugin_slug() );
1066            if ( is_wp_error( $installed ) ) {
1067                return $installed;
1068            }
1069        }
1070
1071        if ( ! current_user_can( 'activate_plugins' ) ) {
1072            return new WP_Error( 'not_allowed', __( 'You are not allowed to activate plugins on this site.', 'jetpack-my-jetpack' ) );
1073        }
1074
1075        $result = static::activate_plugin();
1076        if ( is_wp_error( $result ) ) {
1077            return $result;
1078        }
1079
1080        return true;
1081    }
1082
1083    /**
1084     * Whether activating the product installs and activates its standalone plugin, rather than relying on the Jetpack plugin.
1085     *
1086     * @since $$next-version$$
1087     *
1088     * @return bool
1089     */
1090    public static function should_use_standalone_plugin() {
1091        return true;
1092    }
1093
1094    /**
1095     * Activates the product by installing and activating its plugin
1096     *
1097     * @return boolean|WP_Error
1098     */
1099    final public static function activate() {
1100
1101        $result = self::do_activation();
1102
1103        $result = static::do_product_specific_activation( $result );
1104
1105        $product_slug = static::$slug;
1106
1107        /**
1108         * Fires after My Jetpack activates a product and filters the result
1109         * Use this filter to run additional routines for a product activation on stand-alone plugins
1110         *
1111         * @param bool|WP_Error $result The result of the previous steps of activation.
1112         */
1113        $result = apply_filters( "my_jetpack_{$product_slug}_activation", $result );
1114
1115        return $result;
1116    }
1117
1118    /**
1119     * Override this method to perform product specific activation routines.
1120     *
1121     * @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.
1122     * @return bool|WP_Error
1123     */
1124    public static function do_product_specific_activation( $current_result ) {
1125        return $current_result;
1126    }
1127
1128    /**
1129     * Deactivate the product
1130     *
1131     * @return boolean
1132     */
1133    public static function deactivate() {
1134        deactivate_plugins( static::get_installed_plugin_filename() );
1135        return true;
1136    }
1137
1138    /**
1139     * Returns filtered Jetpack plugin actions links.
1140     *
1141     * @param array $actions - Jetpack plugin action links.
1142     * @return array           Filtered Jetpack plugin actions links.
1143     */
1144    public static function get_plugin_actions_links( $actions ) {
1145        // My Jetpack action link.
1146        $my_jetpack_home_link = array(
1147            'jetpack-home' => sprintf(
1148                '<a href="%1$s" title="%3$s">%2$s</a>',
1149                admin_url( 'admin.php?page=my-jetpack' ),
1150                __( 'My Jetpack', 'jetpack-my-jetpack' ),
1151                __( 'My Jetpack dashboard', 'jetpack-my-jetpack' )
1152            ),
1153        );
1154
1155        // Otherwise, add it to the beginning of the array.
1156        return array_merge( $my_jetpack_home_link, $actions );
1157    }
1158
1159    /**
1160     * Filter the action links for the plugins specified.
1161     *
1162     * @param string|string[] $filenames The plugin filename(s) to filter the action links for.
1163     */
1164    private static function filter_action_links( $filenames ) {
1165        foreach ( $filenames as $filename ) {
1166            $hook     = 'plugin_action_links_' . $filename;
1167            $callback = array( static::class, 'get_plugin_actions_links' );
1168            if ( ! has_filter( $hook, $callback ) ) {
1169                add_filter( $hook, $callback, 20, 2 );
1170            }
1171        }
1172    }
1173
1174    /**
1175     * Extend the plugin action links.
1176     */
1177    public static function extend_plugin_action_links() {
1178        $filenames = static::get_plugin_filename();
1179        if ( ! is_array( $filenames ) ) {
1180            $filenames = array( $filenames );
1181        }
1182
1183        self::filter_action_links( $filenames );
1184    }
1185
1186    /**
1187     * Extend the Jetpack plugin action links.
1188     */
1189    public static function extend_core_plugin_action_links() {
1190        $filenames = self::JETPACK_PLUGIN_FILENAME;
1191
1192        self::filter_action_links( $filenames );
1193    }
1194
1195    /**
1196     * Install and activate the standalone plugin in the case it's missing.
1197     *
1198     * @return boolean|WP_Error
1199     */
1200    public static function install_and_activate_standalone() {
1201        /**
1202         * Check for the presence of the standalone plugin, ignoring Jetpack presence.
1203         *
1204         * If the standalone plugin is not installed and the user can install plugins, proceed with the installation.
1205         */
1206        if ( ! static::is_plugin_installed() ) {
1207            /**
1208             * Check for permissions
1209             */
1210            if ( ! current_user_can( 'install_plugins' ) ) {
1211                return new WP_Error( 'not_allowed', __( 'You are not allowed to install plugins on this site.', 'jetpack-my-jetpack' ) );
1212            }
1213
1214            /**
1215             * Install the plugin
1216             */
1217            $installed = Plugins_Installer::install_plugin( static::get_plugin_slug() );
1218            if ( is_wp_error( $installed ) ) {
1219                return $installed;
1220            }
1221        }
1222
1223        /**
1224         * Activate the installed plugin
1225         */
1226        $result = static::activate_plugin();
1227
1228        if ( is_wp_error( $result ) ) {
1229            return $result;
1230        }
1231
1232        return true;
1233    }
1234
1235    /**
1236     * Determines whether the module/plugin/product needs the users attention.
1237     * Typically due to some sort of error where user troubleshooting is needed.
1238     *
1239     * @return boolean
1240     */
1241    public static function does_module_need_attention() {
1242        return false;
1243    }
1244}