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