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