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