Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
61.72% covered (warning)
61.72%
158 / 256
18.75% covered (danger)
18.75%
3 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
REST_Products
61.72% covered (warning)
61.72%
158 / 256
18.75% covered (danger)
18.75%
3 / 16
103.72
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
115 / 115
100.00% covered (success)
100.00%
1 / 1
1
 get_products_schema
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 get_interstitials_schema
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
2
 permissions_callback
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 view_products_permissions_callback
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 check_products_string
76.19% covered (warning)
76.19%
16 / 21
0.00% covered (danger)
0.00%
0 / 1
4.22
 check_products_argument
28.57% covered (danger)
28.57%
2 / 7
0.00% covered (danger)
0.00%
0 / 1
3.46
 get_products
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 get_products_api_data
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 get_products_by_ownership
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 edit_permissions_callback
60.00% covered (warning)
60.00%
3 / 5
0.00% covered (danger)
0.00%
0 / 1
5.02
 activate_products
55.56% covered (warning)
55.56%
10 / 18
0.00% covered (danger)
0.00%
0 / 1
5.40
 deactivate_products
41.18% covered (danger)
41.18%
7 / 17
0.00% covered (danger)
0.00%
0 / 1
7.26
 install_plugins
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
20
 get_interstitials_state
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 update_interstitials_state
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * Sets up the Products REST API endpoints.
4 *
5 * @package automattic/my-jetpack
6 */
7
8namespace Automattic\Jetpack\My_Jetpack;
9
10use WP_Error;
11use WP_REST_Request;
12use WP_REST_Response;
13use WP_REST_Server;
14
15/**
16 * Registers the REST routes for Products.
17 *
18 * @phan-constructor-used-for-side-effects
19 */
20class REST_Products {
21    /**
22     * Constructor.
23     */
24    public function __construct() {
25        register_rest_route(
26            'my-jetpack/v1',
27            'site/products',
28            array(
29                array(
30                    'methods'             => \WP_REST_Server::READABLE,
31                    'callback'            => __CLASS__ . '::get_products_api_data',
32                    'permission_callback' => __CLASS__ . '::view_products_permissions_callback',
33                    'args'                => array(
34                        'products' => array(
35                            'description'       => __( 'Comma-separated list of product slugs that should be retrieved.', 'jetpack-my-jetpack' ),
36                            'type'              => 'string',
37                            'required'          => false,
38                            'validate_callback' => __CLASS__ . '::check_products_string',
39                        ),
40                    ),
41                ),
42                'schema' => array( $this, 'get_products_schema' ),
43            )
44        );
45
46        $products_arg = array(
47            'description'       => __( 'Array of Product slugs', 'jetpack-my-jetpack' ),
48            'type'              => 'array',
49            'items'             => array(
50                'enum' => Products::get_products_slugs(),
51                'type' => 'string',
52            ),
53            'required'          => true,
54            'validate_callback' => __CLASS__ . '::check_products_argument',
55        );
56
57        register_rest_route(
58            'my-jetpack/v1',
59            'site/products/install',
60            array(
61                array(
62                    'methods'             => \WP_REST_Server::EDITABLE,
63                    'callback'            => __CLASS__ . '::install_plugins',
64                    'permission_callback' => __CLASS__ . '::edit_permissions_callback',
65                    'args'                => array(
66                        'products' => $products_arg,
67                    ),
68                ),
69            )
70        );
71
72        register_rest_route(
73            'my-jetpack/v1',
74            'site/products/activate',
75            array(
76                array(
77                    'methods'             => \WP_REST_Server::EDITABLE,
78                    'callback'            => __CLASS__ . '::activate_products',
79                    'permission_callback' => __CLASS__ . '::edit_permissions_callback',
80                    'args'                => array(
81                        'products' => $products_arg,
82                    ),
83                ),
84            )
85        );
86
87        register_rest_route(
88            'my-jetpack/v1',
89            'site/products/interstitials',
90            array(
91                array(
92                    'methods'             => WP_REST_Server::READABLE,
93                    'callback'            => array( self::class, 'get_interstitials_state' ),
94                    'permission_callback' => array( self::class, 'edit_permissions_callback' ),
95                ),
96                array(
97                    'methods'             => WP_REST_Server::EDITABLE,
98                    'callback'            => array( self::class, 'update_interstitials_state' ),
99                    'permission_callback' => array( self::class, 'edit_permissions_callback' ),
100                    'args'                => array(
101                        'products' => array(
102                            'description'          => __( 'Key-value pairs of product slugs and their interstitial states.', 'jetpack-my-jetpack' ),
103                            'type'                 => 'object',
104                            'required'             => true,
105                            'properties'           => array_fill_keys(
106                                Products::get_products_slugs(),
107                                array(
108                                    'type' => 'boolean',
109                                )
110                            ),
111                            'additionalProperties' => false,
112                            'minProperties'        => 1,
113                        ),
114                    ),
115                ),
116                'schema' => array( self::class, 'get_interstitials_schema' ),
117            )
118        );
119
120        register_rest_route(
121            'my-jetpack/v1',
122            'site/products/deactivate',
123            array(
124                array(
125                    'methods'             => \WP_REST_Server::DELETABLE,
126                    'callback'            => __CLASS__ . '::deactivate_products',
127                    'permission_callback' => __CLASS__ . '::edit_permissions_callback',
128                    'args'                => array(
129                        'products' => $products_arg,
130                    ),
131                ),
132            )
133        );
134
135        register_rest_route(
136            'my-jetpack/v1',
137            'site/products-ownership',
138            array(
139                array(
140                    'methods'             => \WP_REST_Server::READABLE,
141                    'callback'            => __CLASS__ . '::get_products_by_ownership',
142                    'permission_callback' => __CLASS__ . '::view_products_permissions_callback',
143                ),
144            )
145        );
146    }
147
148    /**
149     * Get the schema for the products endpoint
150     *
151     * @return array
152     */
153    public function get_products_schema() {
154        return array(
155            '$schema'    => 'http://json-schema.org/draft-04/schema#',
156            'title'      => 'products',
157            'type'       => 'object',
158            'properties' => Products::get_product_data_schema(),
159        );
160    }
161
162    /**
163     * Get the schema for the interstitials endpoint
164     *
165     * @return array
166     */
167    public static function get_interstitials_schema() {
168        return array(
169            '$schema'    => 'http://json-schema.org/draft-04/schema#',
170            'title'      => 'Products interstitials',
171            'type'       => 'object',
172            'properties' => array(
173                'products' => array(
174                    'type'        => 'object',
175                    'description' => __( 'Key-value pairs of product slugs and their interstitial states.', 'jetpack-my-jetpack' ),
176                    'properties'  => array_fill_keys(
177                        Products::get_products_slugs(),
178                        array(
179                            'description' => __( 'Interstitial state for the product. True means that the user has seen the interstitial for the product.', 'jetpack-my-jetpack' ),
180                            'type'        => 'boolean',
181                        )
182                    ),
183                ),
184            ),
185        );
186    }
187
188    /**
189     * Check user capability to access the endpoint.
190     *
191     * @access public
192     * @static
193     *
194     * @return true|WP_Error
195     */
196    public static function permissions_callback() {
197        return current_user_can( 'manage_options' );
198    }
199
200    /**
201     * Check if the user is permitted to view the product and product info
202     *
203     * @return bool
204     */
205    public static function view_products_permissions_callback() {
206        return current_user_can( 'edit_posts' );
207    }
208
209    /**
210     * Check Products string (comma-separated string).
211     *
212     * @access public
213     * @static
214     *
215     * @param  mixed $value - Value of the 'product' argument.
216     * @return true|WP_Error   True if the value is valid, WP_Error otherwise.
217     */
218    public static function check_products_string( $value ) {
219        if ( ! is_string( $value ) ) {
220            return new WP_Error(
221                'rest_invalid_param',
222                esc_html__( 'The product argument must be a string.', 'jetpack-my-jetpack' ),
223                array( 'status' => 400 )
224            );
225        }
226
227        $products_array = explode( ',', $value );
228        $all_products   = Products::get_products_slugs();
229
230        foreach ( $products_array as $product_slug ) {
231            if ( ! in_array( $product_slug, $all_products, true ) ) {
232                return new WP_Error(
233                    'rest_invalid_param',
234                    esc_html(
235                        sprintf(
236                            /* translators: %s is the product_slug, it should Not be translated. */
237                            __( 'The specified product argument %s is an invalid product.', 'jetpack-my-jetpack' ),
238                            $product_slug
239                        )
240                    ),
241                    array( 'status' => 400 )
242                );
243            }
244        }
245
246        return true;
247    }
248
249    /**
250     * Check Products argument.
251     *
252     * @access public
253     * @static
254     *
255     * @param  mixed $value - Value of the 'product' argument.
256     * @return true|WP_Error   True if the value is valid, WP_Error otherwise.
257     */
258    public static function check_products_argument( $value ) {
259        if ( ! is_array( $value ) ) {
260            return new WP_Error(
261                'rest_invalid_param',
262                esc_html__( 'The product argument must be an array.', 'jetpack-my-jetpack' ),
263                array( 'status' => 400 )
264            );
265        }
266
267        return true;
268    }
269
270    /**
271     * Site products endpoint.
272     *
273     * @param \WP_REST_Request $request The request object.
274     * @return WP_Error|\WP_REST_Response
275     */
276    public static function get_products( $request ) {
277        $slugs         = $request->get_param( 'products' );
278        $product_slugs = ! empty( $slugs ) ? array_map( 'trim', explode( ',', $slugs ) ) : array();
279
280        $response = Products::get_products( $product_slugs );
281        return rest_ensure_response( $response );
282    }
283
284    /**
285     * Site API product data endpoint
286     *
287     * @param \WP_REST_Request $request The request object.
288     *
289     * @return WP_Error|\WP_REST_Response
290     */
291    public static function get_products_api_data( $request ) {
292        $slugs         = $request->get_param( 'products' );
293        $product_slugs = ! empty( $slugs ) ? array_map( 'trim', explode( ',', $slugs ) ) : array();
294
295        $response = Products::get_products_api_data( $product_slugs );
296        return rest_ensure_response( $response );
297    }
298
299    /**
300     * Site products endpoint.
301     *
302     * @return \WP_REST_Response of site products list.
303     */
304    public static function get_products_by_ownership() {
305        $response = array(
306            'unownedProducts' => Products::get_products_by_ownership( 'unowned' ),
307            'ownedProducts'   => Products::get_products_by_ownership( 'owned' ),
308        );
309        return rest_ensure_response( $response );
310    }
311
312    /**
313     * Check permission to edit product
314     *
315     * @return bool
316     */
317    public static function edit_permissions_callback() {
318        if ( ! current_user_can( 'activate_plugins' ) ) {
319            return false;
320        }
321        if ( is_multisite() && ! current_user_can( 'manage_network' ) ) {
322            return false;
323        }
324        return true;
325    }
326
327    /**
328     * Callback for activating products
329     *
330     * @param \WP_REST_Request $request The request object.
331     * @return \WP_REST_Response|\WP_Error
332     */
333    public static function activate_products( $request ) {
334        $products_array = $request->get_param( 'products' );
335
336        foreach ( $products_array as $product_slug ) {
337            $product = Products::get_product( $product_slug );
338            if ( ! isset( $product['class'] ) ) {
339                return new \WP_Error(
340                    'product_class_handler_not_found',
341                    sprintf(
342                        /* translators: %s is the product_slug */
343                        __( 'The product slug %s does not have an associated class handler.', 'jetpack-my-jetpack' ),
344                        $product_slug
345                    ),
346                    array( 'status' => 501 )
347                );
348            }
349
350            $activate_product_result = call_user_func( array( $product['class'], 'activate' ) );
351            if ( is_wp_error( $activate_product_result ) ) {
352                $activate_product_result->add_data( array( 'status' => 400 ) );
353                return $activate_product_result;
354            }
355        }
356        set_transient( 'my_jetpack_product_activated', implode( ',', $products_array ), 10 );
357
358        return rest_ensure_response( Products::get_products( $products_array ) );
359    }
360
361    /**
362     * Callback for deactivating products
363     *
364     * @param \WP_REST_Request $request The request object.
365     * @return \WP_REST_Response|\WP_Error
366     */
367    public static function deactivate_products( $request ) {
368        $products_array = $request->get_param( 'products' );
369
370        foreach ( $products_array as $product_slug ) {
371            $product = Products::get_product( $product_slug );
372            if ( ! isset( $product['class'] ) ) {
373                return new \WP_Error(
374                    'product_class_handler_not_found',
375                    sprintf(
376                        /* translators: %s is the product_slug */
377                        __( 'The product slug %s does not have an associated class handler.', 'jetpack-my-jetpack' ),
378                        $product_slug
379                    ),
380                    array( 'status' => 501 )
381                );
382            }
383
384            $deactivate_product_result = call_user_func( array( $product['class'], 'deactivate' ) );
385            if ( is_wp_error( $deactivate_product_result ) ) {
386                $deactivate_product_result->add_data( array( 'status' => 400 ) );
387                return $deactivate_product_result;
388            }
389        }
390
391        return rest_ensure_response( Products::get_products( $products_array ) );
392    }
393
394    /**
395     * Callback for installing (and activating) multiple product plugins.
396     *
397     * @param \WP_REST_Request $request The request object.
398     * @return \WP_REST_Response|\WP_Error
399     */
400    public static function install_plugins( $request ) {
401        $products_array = $request->get_param( 'products' );
402
403        foreach ( $products_array as $product_slug ) {
404            $product = Products::get_product( $product_slug );
405            if ( ! isset( $product['class'] ) ) {
406                return new \WP_Error(
407                    'product_class_handler_not_found',
408                    sprintf(
409                        /* translators: %s is the product_slug */
410                        __( 'The product slug %s does not have an associated class handler.', 'jetpack-my-jetpack' ),
411                        $product_slug
412                    ),
413                    array( 'status' => 501 )
414                );
415            }
416
417            $install_product_result = call_user_func( array( $product['class'], 'install_and_activate_standalone' ) );
418            if ( is_wp_error( $install_product_result ) ) {
419                $install_product_result->add_data( array( 'status' => 400 ) );
420                return $install_product_result;
421            }
422        }
423
424        return rest_ensure_response( Products::get_products( $products_array ) );
425    }
426
427    /**
428     * Get interstitials state for the products
429     *
430     * @return WP_REST_Response
431     */
432    public static function get_interstitials_state() {
433
434        return rest_ensure_response(
435            array(
436                'products' => Products::get_interstitials_state(),
437            )
438        );
439    }
440
441    /**
442     * Update interstitials state for the products
443     *
444     * @param WP_REST_Request $request The request object.
445     * @return WP_REST_Response|WP_Error
446     */
447    public static function update_interstitials_state( WP_REST_Request $request ) {
448
449        $success = Products::update_interstitials_state( $request->get_param( 'products' ) );
450
451        if ( ! $success ) {
452            return new WP_Error(
453                'my_jetpack_interstitials_update_error',
454                __( 'Failed to update interstitials state.', 'jetpack-my-jetpack' ),
455                array( 'status' => 500 )
456            );
457        }
458
459        return rest_ensure_response(
460            array(
461                'products' => Products::get_interstitials_state(),
462            )
463        );
464    }
465}