Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
48.45% covered (danger)
48.45%
78 / 161
57.14% covered (warning)
57.14%
8 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
Products
48.45% covered (danger)
48.45%
78 / 161
57.14% covered (warning)
57.14%
8 / 14
224.57
0.00% covered (danger)
0.00%
0 / 1
 get_products_classes
100.00% covered (success)
100.00%
31 / 31
100.00% covered (success)
100.00%
1 / 1
5
 register_product_endpoints
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 get_not_shown_products
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_products
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
 get_products_api_data
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
 get_products_by_ownership
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
30
 get_all_plugin_filenames
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 get_product
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 get_product_class
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 get_products_slugs
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_product_data_schema
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
2
 extend_plugins_action_links
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
2
 get_interstitials_state
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 update_interstitials_state
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Class for manipulating products
4 *
5 * @package automattic/my-jetpack
6 */
7
8namespace Automattic\Jetpack\My_Jetpack;
9
10/**
11 * A class for everything related to product handling in My Jetpack
12 */
13class Products {
14    /**
15     * Constants for the status of a product on a site
16     *
17     * @var string
18     */
19    public const STATUS_SITE_CONNECTION_ERROR       = 'site_connection_error';
20    public const STATUS_USER_CONNECTION_ERROR       = 'user_connection_error';
21    public const STATUS_ACTIVE                      = 'active';
22    public const STATUS_CAN_UPGRADE                 = 'can_upgrade';
23    public const STATUS_EXPIRING_SOON               = 'expiring';
24    public const STATUS_EXPIRED                     = 'expired';
25    public const STATUS_INACTIVE                    = 'inactive';
26    public const STATUS_MODULE_DISABLED             = 'module_disabled';
27    public const STATUS_PLUGIN_ABSENT               = 'plugin_absent';
28    public const STATUS_PLUGIN_ABSENT_WITH_PLAN     = 'plugin_absent_with_plan';
29    public const STATUS_NEEDS_PLAN                  = 'needs_plan';
30    public const STATUS_NEEDS_ACTIVATION            = 'needs_activation';
31    public const STATUS_NEEDS_FIRST_SITE_CONNECTION = 'needs_first_site_connection';
32    public const STATUS_NEEDS_ATTENTION__WARNING    = 'needs_attention_warning';
33    public const STATUS_NEEDS_ATTENTION__ERROR      = 'needs_attention_error';
34
35    public const INTERSTITIALS_OPTION_NAME = 'my_jetpack_products_interstitials_state';
36
37    /**
38     * List of statuses that display the module as disabled
39     * This is defined as the statuses in which the user willingly has the module disabled whether it be by
40     * default, uninstalling the plugin, disabling the module, or not renewing their plan.
41     *
42     * @var array
43     */
44    public static $disabled_module_statuses = array(
45        self::STATUS_INACTIVE,
46        self::STATUS_MODULE_DISABLED,
47        self::STATUS_PLUGIN_ABSENT,
48        self::STATUS_PLUGIN_ABSENT_WITH_PLAN,
49        self::STATUS_NEEDS_ACTIVATION,
50        self::STATUS_NEEDS_FIRST_SITE_CONNECTION,
51    );
52
53    /**
54     * List of statuses that display the module as broken
55     *
56     * @var array
57     */
58    public static $broken_module_statuses = array(
59        self::STATUS_SITE_CONNECTION_ERROR,
60        self::STATUS_USER_CONNECTION_ERROR,
61    );
62
63    /**
64     * List of statuses that display the module as needing attention with a warning
65     *
66     * @var array
67     */
68    public static $warning_module_statuses = array(
69        self::STATUS_SITE_CONNECTION_ERROR,
70        self::STATUS_USER_CONNECTION_ERROR,
71        self::STATUS_PLUGIN_ABSENT_WITH_PLAN,
72        self::STATUS_NEEDS_PLAN,
73        self::STATUS_NEEDS_ATTENTION__ERROR,
74        self::STATUS_NEEDS_ATTENTION__WARNING,
75    );
76
77    /**
78     * List of statuses that display the module as active
79     *
80     * @var array
81     */
82    public static $active_module_statuses = array(
83        self::STATUS_ACTIVE,
84        self::STATUS_CAN_UPGRADE,
85    );
86
87    /**
88     * List of statuses that display the module as active
89     *
90     * @var array
91     */
92    public static $expiring_or_expired_module_statuses = array(
93        self::STATUS_EXPIRING_SOON,
94        self::STATUS_EXPIRED,
95    );
96
97    /**
98     * List of all statuses that a product can have
99     *
100     * @var array
101     */
102    public static $all_statuses = array(
103        self::STATUS_SITE_CONNECTION_ERROR,
104        self::STATUS_USER_CONNECTION_ERROR,
105        self::STATUS_ACTIVE,
106        self::STATUS_CAN_UPGRADE,
107        self::STATUS_EXPIRING_SOON,
108        self::STATUS_EXPIRED,
109        self::STATUS_INACTIVE,
110        self::STATUS_MODULE_DISABLED,
111        self::STATUS_PLUGIN_ABSENT,
112        self::STATUS_PLUGIN_ABSENT_WITH_PLAN,
113        self::STATUS_NEEDS_PLAN,
114        self::STATUS_NEEDS_ACTIVATION,
115        self::STATUS_NEEDS_FIRST_SITE_CONNECTION,
116        self::STATUS_NEEDS_ATTENTION__WARNING,
117        self::STATUS_NEEDS_ATTENTION__ERROR,
118    );
119
120    /**
121     * Get the list of Products classes
122     *
123     * Here's where all the existing Products are registered
124     *
125     * @throws \Exception If the result of a filter has invalid classes.
126     * @return array List of class names
127     */
128    public static function get_products_classes() {
129        $classes = array(
130            'anti-spam'        => Products\Anti_Spam::class,
131            'backup'           => Products\Backup::class,
132            'boost'            => Products\Boost::class,
133            'crm'              => Products\Crm::class,
134            'creator'          => Products\Creator::class,
135            'extras'           => Products\Extras::class,
136            'jetpack-ai'       => Products\Jetpack_Ai::class,
137            // TODO: Remove this duplicate class ('ai')? See: https://github.com/Automattic/jetpack/pull/35910#pullrequestreview-2456462227.
138            'ai'               => Products\Jetpack_Ai::class,
139            'scan'             => Products\Scan::class,
140            'search'           => Products\Search::class,
141            'social'           => Products\Social::class,
142            'security'         => Products\Security::class,
143            'protect'          => Products\Protect::class,
144            'videopress'       => Products\Videopress::class,
145            'stats'            => Products\Stats::class,
146            'growth'           => Products\Growth::class,
147            'complete'         => Products\Complete::class,
148            // Features.
149            'activity-log'     => Products\Activity_Log::class,
150            'newsletter'       => Products\Newsletter::class,
151            'site-accelerator' => Products\Site_Accelerator::class,
152            'related-posts'    => Products\Related_Posts::class,
153            'jetpack-forms'    => Products\Jetpack_Forms::class,
154        );
155
156        /**
157         * This filter allows plugin to override the Product class of a given product. The new class must be a child class of the default one declared in My Jetpack
158         *
159         * For example, a stand-alone plugin could overwrite its product class to control specific behavior of the product in the My Jetpack page after it is active without having to commit changes to the My Jetpack package:
160         *
161         * add_filter( 'my_jetpack_products_classes', function( $classes ) {
162         *  $classes['my_plugin'] = 'My_Plugin'; // a class that extends the original one declared in the My Jetpack package.
163         *  return $classes
164         * } );
165         *
166         * @param array $classes An array where the keys are the product slugs and the values are the class names.
167         */
168        $final_classes = apply_filters( 'my_jetpack_products_classes', $classes );
169
170        // Check that the classes are still child of the same original classes.
171        foreach ( (array) $final_classes as $slug => $final_class ) {
172            if ( $final_class === $classes[ $slug ] ) {
173                continue;
174            }
175            if ( ! class_exists( $final_class ) || ! is_subclass_of( $final_class, $classes[ $slug ] ) ) {
176                throw new \Exception( 'You can only overwrite a Product class with a child of the original class.' );
177            }
178        }
179
180        return $final_classes;
181    }
182
183    /**
184     * Register endpoints related to product classes
185     *
186     * @return void
187     */
188    public static function register_product_endpoints() {
189        $classes = self::get_products_classes();
190
191        foreach ( $classes as $class ) {
192            $class::register_endpoints();
193        }
194    }
195
196    /**
197     * List of product slugs that are displayed on the main My Jetpack page
198     *
199     * @var array
200     */
201    public static $shown_products = array(
202        'anti-spam',
203        'backup',
204        'boost',
205        'crm',
206        'jetpack-ai',
207        'search',
208        'social',
209        'protect',
210        'videopress',
211        'stats',
212    );
213
214    /**
215     * Gets the list of product slugs that are Not displayed on the main My Jetpack page
216     *
217     * @return array
218     */
219    public static function get_not_shown_products() {
220        return array_diff( array_keys( static::get_products_classes() ), self::$shown_products );
221    }
222
223    /**
224     * Product data
225     *
226     * @param array $product_slugs (optional) An array of specified product slugs.
227     * @return array Jetpack products on the site and their availability.
228     */
229    public static function get_products( $product_slugs = array() ) {
230        $all_classes = self::get_products_classes();
231        $products    = array();
232        // If an array of $product_slugs are passed, return only the products specified in $product_slugs array.
233        if ( $product_slugs ) {
234            foreach ( $product_slugs as $product_slug ) {
235                if ( isset( $all_classes[ $product_slug ] ) ) {
236                    $class                     = $all_classes[ $product_slug ];
237                    $products[ $product_slug ] = $class::get_info();
238                }
239            }
240
241            return $products;
242        }
243        // Otherwise return All products.
244        foreach ( $all_classes as $slug => $class ) {
245            $products[ $slug ] = $class::get_info();
246        }
247
248        return $products;
249    }
250
251    /**
252     * Get products data related to the wpcom api
253     *
254     * @param array $product_slugs - (optional) An array of specified product slugs.
255     * @return array
256     */
257    public static function get_products_api_data( $product_slugs = array() ) {
258        $all_classes = self::get_products_classes();
259        $products    = array();
260        // If an array of $product_slugs are passed, return only the products specified in $product_slugs array.
261        if ( $product_slugs ) {
262            foreach ( $product_slugs as $product_slug ) {
263                if ( isset( $all_classes[ $product_slug ] ) ) {
264                    $class                     = $all_classes[ $product_slug ];
265                    $products[ $product_slug ] = $class::get_wpcom_info();
266                }
267            }
268
269            return $products;
270        }
271        // Otherwise return All products.
272        foreach ( $all_classes as $slug => $class ) {
273            $products[ $slug ] = $class::get_wpcom_info();
274        }
275
276        return $products;
277    }
278
279    /**
280     * Get a list of products sorted by whether or not the user owns them
281     * An owned product is defined as a product that is any of the following
282     * - Active
283     * - Has historically been active
284     * - The user has a plan that includes the product
285     * - The user has the standalone plugin for the product installed
286     *
287     * @param string $type The type of ownership to return ('owned' or 'unowned').
288     *
289     * @return array
290     */
291    public static function get_products_by_ownership( $type ) {
292        $owned_active_products   = array();
293        $owned_warning_products  = array();
294        $owned_inactive_products = array();
295        $unowned_products        = array();
296
297        foreach ( self::get_products_classes() as $class ) {
298            $product_slug = $class::$slug;
299            $status       = $class::get_status();
300
301            if ( $class::is_owned() ) {
302                // This sorts the the products in the order of active -> warning -> inactive.
303                // This enables the frontend to display them in that order.
304                // This is not needed for unowned products as those will always have a status of 'inactive'.
305                if ( in_array( $status, self::$active_module_statuses, true ) ) {
306                    array_push( $owned_active_products, $product_slug );
307                } elseif ( in_array( $status, self::$warning_module_statuses, true ) ) {
308                    array_push( $owned_warning_products, $product_slug );
309                } else {
310                    array_push( $owned_inactive_products, $product_slug );
311                }
312                continue;
313            }
314
315            array_push( $unowned_products, $product_slug );
316        }
317
318        $data = array(
319            'owned'   => array_values(
320                array_unique(
321                    array_merge(
322                        $owned_active_products,
323                        $owned_warning_products,
324                        $owned_inactive_products
325                    )
326                )
327            ),
328            'unowned' => array_values(
329                array_unique( $unowned_products )
330            ),
331        );
332
333        return $data[ $type ];
334    }
335
336    /**
337     * Get all plugin filenames associated with the products.
338     *
339     * @return array
340     */
341    public static function get_all_plugin_filenames() {
342        $filenames = array();
343        foreach ( self::get_products_classes() as $class ) {
344            if ( ! isset( $class::$plugin_filename ) ) {
345                continue;
346            }
347
348            if ( is_array( $class::$plugin_filename ) ) {
349                $filenames = array_merge( $filenames, $class::$plugin_filename );
350            } else {
351                $filenames[] = $class::$plugin_filename;
352            }
353        }
354        return $filenames;
355    }
356
357    /**
358     * Get one product data by its slug
359     *
360     * @param string $product_slug The product slug.
361     *
362     * @return ?array
363     */
364    public static function get_product( $product_slug ) {
365        $classes = self::get_products_classes();
366        if ( isset( $classes[ $product_slug ] ) ) {
367            return $classes[ $product_slug ]::get_info();
368        }
369    }
370
371    /**
372     * Get one product Class name
373     *
374     * @param string $product_slug The product slug.
375     *
376     * @return ?string
377     */
378    public static function get_product_class( $product_slug ) {
379        $classes = self::get_products_classes();
380        if ( isset( $classes[ $product_slug ] ) ) {
381            return $classes[ $product_slug ];
382        }
383    }
384
385    /**
386     * Return product slugs list.
387     *
388     * @return array Product slugs array.
389     */
390    public static function get_products_slugs() {
391        return array_keys( self::get_products_classes() );
392    }
393
394    /**
395     * Gets the json schema for the product data
396     *
397     * @return array
398     */
399    public static function get_product_data_schema() {
400        return array(
401            'title'      => 'The requested product data',
402            'type'       => 'object',
403            'properties' => array(
404                'product'     => array(
405                    'description'       => __( 'Product slug', 'jetpack-my-jetpack' ),
406                    'type'              => 'string',
407                    'enum'              => __CLASS__ . '::get_product_slugs',
408                    'required'          => false,
409                    'validate_callback' => __CLASS__ . '::check_product_argument',
410                ),
411                'action'      => array(
412                    'description'       => __( 'Production action to execute', 'jetpack-my-jetpack' ),
413                    'type'              => 'string',
414                    'enum'              => array( 'activate', 'deactivate' ),
415                    'required'          => false,
416                    'validate_callback' => __CLASS__ . '::check_product_argument',
417                ),
418                'slug'        => array(
419                    'title' => 'The product slug',
420                    'type'  => 'string',
421                ),
422                'name'        => array(
423                    'title' => 'The product name',
424                    'type'  => 'string',
425                ),
426                'description' => array(
427                    'title' => 'The product description',
428                    'type'  => 'string',
429                ),
430                'status'      => array(
431                    'title' => 'The product status',
432                    'type'  => 'string',
433                    'enum'  => self::$all_statuses,
434                ),
435                'class'       => array(
436                    'title' => 'The product class handler',
437                    'type'  => 'string',
438                ),
439            ),
440        );
441    }
442
443    /**
444     * Extend actions links for plugins
445     * tied to the Products.
446     */
447    public static function extend_plugins_action_links() {
448        $products = array(
449            'backup',
450            'boost',
451            'crm',
452            'videopress',
453            'social',
454            'protect',
455            'crm',
456            'search',
457            'jetpack-ai',
458        );
459
460        // Add plugin action links for the core Jetpack plugin.
461        Product::extend_core_plugin_action_links();
462
463        // Add plugin action links to standalone products.
464        foreach ( $products as $product ) {
465            $class_name = self::get_product_class( $product );
466            $class_name::extend_plugin_action_links();
467        }
468    }
469
470    /**
471     * Get interstitials state for the products
472     *
473     * @return array A key-value array of product slugs and their interstitial states. True means the interstitial was seen by the user for that product.
474     */
475    public static function get_interstitials_state() {
476        return get_option( self::INTERSTITIALS_OPTION_NAME, array() );
477    }
478
479    /**
480     * Update interstitials state for the products
481     *
482     * @param array $new_state A key-value array of product slugs and their interstitial states.
483     *
484     * @return bool True if the option was updated successfully, false otherwise.
485     */
486    public static function update_interstitials_state( $new_state ) {
487
488        // Merge the existing interstitials state with the new state.
489        $interstitials_state = array_merge( self::get_interstitials_state(), $new_state );
490
491        return update_option( self::INTERSTITIALS_OPTION_NAME, $interstitials_state );
492    }
493}