Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
46.25% covered (danger)
46.25%
74 / 160
50.00% covered (danger)
50.00%
7 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
Products
46.25% covered (danger)
46.25%
74 / 160
50.00% covered (danger)
50.00%
7 / 14
249.59
0.00% covered (danger)
0.00%
0 / 1
 get_products_classes
100.00% covered (success)
100.00%
30 / 30
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
72.73% covered (warning)
72.73%
8 / 11
0.00% covered (danger)
0.00%
0 / 1
5.51
 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            'newsletter'       => Products\Newsletter::class,
150            'site-accelerator' => Products\Site_Accelerator::class,
151            'related-posts'    => Products\Related_Posts::class,
152            'jetpack-forms'    => Products\Jetpack_Forms::class,
153        );
154
155        /**
156         * 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
157         *
158         * 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:
159         *
160         * add_filter( 'my_jetpack_products_classes', function( $classes ) {
161         *  $classes['my_plugin'] = 'My_Plugin'; // a class that extends the original one declared in the My Jetpack package.
162         *  return $classes
163         * } );
164         *
165         * @param array $classes An array where the keys are the product slugs and the values are the class names.
166         */
167        $final_classes = apply_filters( 'my_jetpack_products_classes', $classes );
168
169        // Check that the classes are still child of the same original classes.
170        foreach ( (array) $final_classes as $slug => $final_class ) {
171            if ( $final_class === $classes[ $slug ] ) {
172                continue;
173            }
174            if ( ! class_exists( $final_class ) || ! is_subclass_of( $final_class, $classes[ $slug ] ) ) {
175                throw new \Exception( 'You can only overwrite a Product class with a child of the original class.' );
176            }
177        }
178
179        return $final_classes;
180    }
181
182    /**
183     * Register endpoints related to product classes
184     *
185     * @return void
186     */
187    public static function register_product_endpoints() {
188        $classes = self::get_products_classes();
189
190        foreach ( $classes as $class ) {
191            $class::register_endpoints();
192        }
193    }
194
195    /**
196     * List of product slugs that are displayed on the main My Jetpack page
197     *
198     * @var array
199     */
200    public static $shown_products = array(
201        'anti-spam',
202        'backup',
203        'boost',
204        'crm',
205        'jetpack-ai',
206        'search',
207        'social',
208        'protect',
209        'videopress',
210        'stats',
211    );
212
213    /**
214     * Gets the list of product slugs that are Not displayed on the main My Jetpack page
215     *
216     * @return array
217     */
218    public static function get_not_shown_products() {
219        return array_diff( array_keys( static::get_products_classes() ), self::$shown_products );
220    }
221
222    /**
223     * Product data
224     *
225     * @param array $product_slugs (optional) An array of specified product slugs.
226     * @return array Jetpack products on the site and their availability.
227     */
228    public static function get_products( $product_slugs = array() ) {
229        $all_classes = self::get_products_classes();
230        $products    = array();
231        // If an array of $product_slugs are passed, return only the products specified in $product_slugs array.
232        if ( $product_slugs ) {
233            foreach ( $product_slugs as $product_slug ) {
234                if ( isset( $all_classes[ $product_slug ] ) ) {
235                    $class                     = $all_classes[ $product_slug ];
236                    $products[ $product_slug ] = $class::get_info();
237                }
238            }
239
240            return $products;
241        }
242        // Otherwise return All products.
243        foreach ( $all_classes as $slug => $class ) {
244            $products[ $slug ] = $class::get_info();
245        }
246
247        return $products;
248    }
249
250    /**
251     * Get products data related to the wpcom api
252     *
253     * @param array $product_slugs - (optional) An array of specified product slugs.
254     * @return array
255     */
256    public static function get_products_api_data( $product_slugs = array() ) {
257        $all_classes = self::get_products_classes();
258        $products    = array();
259        // If an array of $product_slugs are passed, return only the products specified in $product_slugs array.
260        if ( $product_slugs ) {
261            foreach ( $product_slugs as $product_slug ) {
262                if ( isset( $all_classes[ $product_slug ] ) ) {
263                    $class                     = $all_classes[ $product_slug ];
264                    $products[ $product_slug ] = $class::get_wpcom_info();
265                }
266            }
267
268            return $products;
269        }
270        // Otherwise return All products.
271        foreach ( $all_classes as $slug => $class ) {
272            $products[ $slug ] = $class::get_wpcom_info();
273        }
274
275        return $products;
276    }
277
278    /**
279     * Get a list of products sorted by whether or not the user owns them
280     * An owned product is defined as a product that is any of the following
281     * - Active
282     * - Has historically been active
283     * - The user has a plan that includes the product
284     * - The user has the standalone plugin for the product installed
285     *
286     * @param string $type The type of ownership to return ('owned' or 'unowned').
287     *
288     * @return array
289     */
290    public static function get_products_by_ownership( $type ) {
291        $owned_active_products   = array();
292        $owned_warning_products  = array();
293        $owned_inactive_products = array();
294        $unowned_products        = array();
295
296        foreach ( self::get_products_classes() as $class ) {
297            $product_slug = $class::$slug;
298            $status       = $class::get_status();
299
300            if ( $class::is_owned() ) {
301                // This sorts the the products in the order of active -> warning -> inactive.
302                // This enables the frontend to display them in that order.
303                // This is not needed for unowned products as those will always have a status of 'inactive'.
304                if ( in_array( $status, self::$active_module_statuses, true ) ) {
305                    array_push( $owned_active_products, $product_slug );
306                } elseif ( in_array( $status, self::$warning_module_statuses, true ) ) {
307                    array_push( $owned_warning_products, $product_slug );
308                } else {
309                    array_push( $owned_inactive_products, $product_slug );
310                }
311                continue;
312            }
313
314            array_push( $unowned_products, $product_slug );
315        }
316
317        $data = array(
318            'owned'   => array_values(
319                array_unique(
320                    array_merge(
321                        $owned_active_products,
322                        $owned_warning_products,
323                        $owned_inactive_products
324                    )
325                )
326            ),
327            'unowned' => array_values(
328                array_unique( $unowned_products )
329            ),
330        );
331
332        return $data[ $type ];
333    }
334
335    /**
336     * Get all plugin filenames associated with the products.
337     *
338     * @return array
339     */
340    public static function get_all_plugin_filenames() {
341        $filenames = array();
342        foreach ( self::get_products_classes() as $class ) {
343            if ( ! isset( $class::$plugin_filename ) ) {
344                continue;
345            }
346
347            if ( is_array( $class::$plugin_filename ) ) {
348                $filenames = array_merge( $filenames, $class::$plugin_filename );
349            } else {
350                $filenames[] = $class::$plugin_filename;
351            }
352        }
353        return $filenames;
354    }
355
356    /**
357     * Get one product data by its slug
358     *
359     * @param string $product_slug The product slug.
360     *
361     * @return ?array
362     */
363    public static function get_product( $product_slug ) {
364        $classes = self::get_products_classes();
365        if ( isset( $classes[ $product_slug ] ) ) {
366            return $classes[ $product_slug ]::get_info();
367        }
368    }
369
370    /**
371     * Get one product Class name
372     *
373     * @param string $product_slug The product slug.
374     *
375     * @return ?string
376     */
377    public static function get_product_class( $product_slug ) {
378        $classes = self::get_products_classes();
379        if ( isset( $classes[ $product_slug ] ) ) {
380            return $classes[ $product_slug ];
381        }
382    }
383
384    /**
385     * Return product slugs list.
386     *
387     * @return array Product slugs array.
388     */
389    public static function get_products_slugs() {
390        return array_keys( self::get_products_classes() );
391    }
392
393    /**
394     * Gets the json schema for the product data
395     *
396     * @return array
397     */
398    public static function get_product_data_schema() {
399        return array(
400            'title'      => 'The requested product data',
401            'type'       => 'object',
402            'properties' => array(
403                'product'     => array(
404                    'description'       => __( 'Product slug', 'jetpack-my-jetpack' ),
405                    'type'              => 'string',
406                    'enum'              => __CLASS__ . '::get_product_slugs',
407                    'required'          => false,
408                    'validate_callback' => __CLASS__ . '::check_product_argument',
409                ),
410                'action'      => array(
411                    'description'       => __( 'Production action to execute', 'jetpack-my-jetpack' ),
412                    'type'              => 'string',
413                    'enum'              => array( 'activate', 'deactivate' ),
414                    'required'          => false,
415                    'validate_callback' => __CLASS__ . '::check_product_argument',
416                ),
417                'slug'        => array(
418                    'title' => 'The product slug',
419                    'type'  => 'string',
420                ),
421                'name'        => array(
422                    'title' => 'The product name',
423                    'type'  => 'string',
424                ),
425                'description' => array(
426                    'title' => 'The product description',
427                    'type'  => 'string',
428                ),
429                'status'      => array(
430                    'title' => 'The product status',
431                    'type'  => 'string',
432                    'enum'  => self::$all_statuses,
433                ),
434                'class'       => array(
435                    'title' => 'The product class handler',
436                    'type'  => 'string',
437                ),
438            ),
439        );
440    }
441
442    /**
443     * Extend actions links for plugins
444     * tied to the Products.
445     */
446    public static function extend_plugins_action_links() {
447        $products = array(
448            'backup',
449            'boost',
450            'crm',
451            'videopress',
452            'social',
453            'protect',
454            'crm',
455            'search',
456            'jetpack-ai',
457        );
458
459        // Add plugin action links for the core Jetpack plugin.
460        Product::extend_core_plugin_action_links();
461
462        // Add plugin action links to standalone products.
463        foreach ( $products as $product ) {
464            $class_name = self::get_product_class( $product );
465            $class_name::extend_plugin_action_links();
466        }
467    }
468
469    /**
470     * Get interstitials state for the products
471     *
472     * @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.
473     */
474    public static function get_interstitials_state() {
475        return get_option( self::INTERSTITIALS_OPTION_NAME, array() );
476    }
477
478    /**
479     * Update interstitials state for the products
480     *
481     * @param array $new_state A key-value array of product slugs and their interstitial states.
482     *
483     * @return bool True if the option was updated successfully, false otherwise.
484     */
485    public static function update_interstitials_state( $new_state ) {
486
487        // Merge the existing interstitials state with the new state.
488        $interstitials_state = array_merge( self::get_interstitials_state(), $new_state );
489
490        return update_option( self::INTERSTITIALS_OPTION_NAME, $interstitials_state );
491    }
492}