Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
3.68% covered (danger)
3.68%
7 / 190
15.38% covered (danger)
15.38%
2 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
Endpoints
3.68% covered (danger)
3.68%
7 / 190
15.38% covered (danger)
15.38%
2 / 13
538.65
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 register_endpoints
0.00% covered (danger)
0.00%
0 / 93
0.00% covered (danger)
0.00%
0 / 1
2
 set_jetpack_license_key_permission_check
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 can_manage_options_check
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 validate_string
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 validate_non_neg_int
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 update_licensing_error
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 get_licensing_error
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 set_jetpack_license
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 get_user_licenses
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
6
 get_user_license_counts
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
6
 update_licensing_activation_notice_dismiss
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
20
 attach_jetpack_licenses
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * A Licensing Endpoints class for Jetpack.
4 *
5 * @package automattic/jetpack-licensing
6 */
7
8namespace Automattic\Jetpack\Licensing;
9
10use Automattic\Jetpack\Connection\Client;
11use Automattic\Jetpack\Licensing;
12use Automattic\Jetpack\Status\Visitor;
13use Jetpack_Options;
14use WP_Error;
15use WP_REST_Request;
16use WP_REST_Response;
17
18/**
19 * Class Endpoints.
20 * Helper class that is responsible for registering and responding to licensing endpoint requests.
21 *
22 * @since 1.7.0
23 */
24class Endpoints {
25    /**
26     * This property stores the localized "Insufficient Permissions" error message.
27     *
28     * @var string Generic error message when user is not allowed to perform an action.
29     */
30    private static $user_permissions_error_msg;
31
32    /**
33     * Constructor.
34     */
35    public function __construct() {
36        self::$user_permissions_error_msg = esc_html__(
37            'You do not have the correct user permissions to perform this action.
38            Please contact your site admin if you think this is a mistake.',
39            'jetpack-licensing'
40        );
41    }
42
43    /**
44     * Declare the endpoints for the licensing package.
45     *
46     * @since 1.7.0
47     *
48     * @since-jetpack 10.9.0
49     */
50    public function register_endpoints() {
51        /*
52         * Get and update the last licensing error message.
53         */
54        register_rest_route(
55            'jetpack/v4',
56            '/licensing/error',
57            array(
58                array(
59                    'methods'             => \WP_REST_Server::READABLE,
60                    'callback'            => __CLASS__ . '::get_licensing_error',
61                    'permission_callback' => __CLASS__ . '::can_manage_options_check',
62                ),
63                array(
64                    'methods'             => \WP_REST_Server::EDITABLE,
65                    'callback'            => __CLASS__ . '::update_licensing_error',
66                    'permission_callback' => __CLASS__ . '::can_manage_options_check',
67                    'args'                => array(
68                        'error' => array(
69                            'required'          => true,
70                            'type'              => 'string',
71                            'validate_callback' => __CLASS__ . '::validate_string',
72                            'sanitize_callback' => 'sanitize_text_field',
73                        ),
74                    ),
75                ),
76            )
77        );
78
79        /**
80         * Sets a license. This is still used as part of the first pass at licensing done for partners.
81         *
82         * See https://github.com/Automattic/jetpack/pull/23687 for more details.
83         */
84        register_rest_route(
85            'jetpack/v4',
86            '/licensing/set-license',
87            array(
88                'methods'             => \WP_REST_Server::EDITABLE,
89                'callback'            => __CLASS__ . '::set_jetpack_license',
90                'permission_callback' => __CLASS__ . '::set_jetpack_license_key_permission_check',
91                'args'                => array(
92                    'license' => array(
93                        'required'          => true,
94                        'type'              => 'string',
95                        'validate_callback' => __CLASS__ . '::validate_string',
96                        'sanitize_callback' => 'sanitize_text_field',
97                    ),
98                ),
99            )
100        );
101
102        /**
103         * Get Jetpack user licenses.
104         */
105        register_rest_route(
106            'jetpack/v4',
107            'licensing/user/licenses',
108            array(
109                'methods'             => \WP_REST_Server::READABLE,
110                'callback'            => __CLASS__ . '::get_user_licenses',
111                'permission_callback' => __CLASS__ . '::can_manage_options_check',
112            )
113        );
114
115        /**
116         * Get Jetpack user license counts.
117         */
118        register_rest_route(
119            'jetpack/v4',
120            'licensing/user/counts',
121            array(
122                'methods'             => \WP_REST_Server::READABLE,
123                'callback'            => __CLASS__ . '::get_user_license_counts',
124                'permission_callback' => __CLASS__ . '::can_manage_options_check',
125            )
126        );
127
128        /**
129         * Update user-licensing activation notice dismiss info.
130         */
131        register_rest_route(
132            'jetpack/v4',
133            'licensing/user/activation-notice-dismiss',
134            array(
135                'methods'             => \WP_REST_Server::EDITABLE,
136                'callback'            => __CLASS__ . '::update_licensing_activation_notice_dismiss',
137                'permission_callback' => __CLASS__ . '::can_manage_options_check',
138                'args'                => array(
139                    'last_detached_count' => array(
140                        'required'          => true,
141                        'type'              => 'integer',
142                        'validate_callback' => __CLASS__ . '::validate_non_neg_int',
143                    ),
144                ),
145            )
146        );
147
148        /**
149         * Attach licenses to user account
150         */
151        register_rest_route(
152            'jetpack/v4',
153            '/licensing/attach-licenses',
154            array(
155                'methods'             => \WP_REST_Server::EDITABLE,
156                'callback'            => __CLASS__ . '::attach_jetpack_licenses',
157                'permission_callback' => __CLASS__ . '::can_manage_options_check',
158                'args'                => array(
159                    'licenses' => array(
160                        'required' => true,
161                        'type'     => 'array',
162                        'items'    => array(
163                            'type' => 'string',
164                        ),
165                    ),
166                ),
167            )
168        );
169    }
170
171    /**
172     * Verify that the user can set a Jetpack license key
173     *
174     * @since 1.7.0
175     *
176     * @since-jetpack 9.5.0
177     *
178     * @return bool|WP_Error True if user is able to set a Jetpack license key
179     */
180    public static function set_jetpack_license_key_permission_check() {
181        if ( Licensing::instance()->is_licensing_input_enabled() ) {
182            return true;
183        }
184
185        return new WP_Error( 'invalid_user_permission_set_jetpack_license_key', self::$user_permissions_error_msg, array( 'status' => rest_authorization_required_code() ) );
186    }
187
188    /**
189     * Verify that user can manage_options
190     *
191     * @since 1.7.0
192     *
193     * @return bool Whether user has the capability 'manage_options'.
194     */
195    public static function can_manage_options_check() {
196        if ( current_user_can( 'manage_options' ) ) {
197            return true;
198        }
199
200        return new WP_Error( 'invalid_user_permission_view_admin', self::$user_permissions_error_msg, array( 'status' => rest_authorization_required_code() ) );
201    }
202
203    /**
204     * Validates that the parameter is a string.
205     *
206     * @since 1.7.0
207     *
208     * @since-jetpack 4.3.0
209     *
210     * @param string          $value Value to check.
211     * @param WP_REST_Request $request The request sent to the WP REST API.
212     * @param string          $param Name of the parameter passed to endpoint holding $value.
213     *
214     * @return bool|WP_Error
215     */
216    public static function validate_string( $value, $request, $param ) {
217        if ( ! is_string( $value ) ) {
218            /* translators: %s: The literal parameter name. Should not be translated. */
219            return new WP_Error( 'invalid_param', sprintf( esc_html__( '%s must be a string.', 'jetpack-licensing' ), $param ) );
220        }
221        return true;
222    }
223
224    /**
225     * Validates that the parameter is a non-negative integer (includes 0).
226     *
227     * @since 1.7.0
228     *
229     * @since-jetpack 10.4.0
230     *
231     * @param int             $value Value to check.
232     * @param WP_REST_Request $request The request sent to the WP REST API.
233     * @param string          $param Name of the parameter passed to endpoint holding $value.
234     *
235     * @return bool|WP_Error
236     */
237    public static function validate_non_neg_int( $value, $request, $param ) {
238        if ( ! is_numeric( $value ) || $value < 0 ) {
239            return new WP_Error(
240                'invalid_param',
241                /* translators: %s: The literal parameter name. Should not be translated. */
242                sprintf( esc_html__( '%s must be a non-negative integer.', 'jetpack-licensing' ), $param )
243            );
244        }
245        return true;
246    }
247
248    /**
249     * Update the last licensing error message.
250     *
251     * @since 1.7.0
252     *
253     * @since-jetpack 9.0.0
254     *
255     * @param WP_REST_Request $request The request.
256     *
257     * @return bool true.
258     */
259    public static function update_licensing_error( $request ) {
260        Licensing::instance()->log_error( $request['error'] );
261
262        return true;
263    }
264
265    /**
266     * Get the last licensing error message, if any.
267     *
268     * @since 1.7.0
269     *
270     * @since-jetpack 9.0.0
271     *
272     * @return string Licensing error message or empty string.
273     */
274    public static function get_licensing_error() {
275        return Licensing::instance()->last_error();
276    }
277
278    /**
279     * Set a Jetpack license
280     *
281     * @since 1.7.0
282     *
283     * @since-jetpack 9.6.0
284     *
285     * @param WP_REST_Request $request The request.
286     *
287     * @return WP_REST_Response|WP_Error A response object if the option was successfully updated, or a WP_Error if it failed.
288     */
289    public static function set_jetpack_license( $request ) {
290        $license = trim( sanitize_text_field( $request['license'] ) );
291
292        if ( Licensing::instance()->append_license( $license ) ) {
293            return rest_ensure_response( array( 'code' => 'success' ) );
294        }
295
296        return new WP_Error(
297            'setting_license_key_failed',
298            esc_html__( 'Could not set this license key. Please try again.', 'jetpack-licensing' ),
299            array( 'status' => 500 )
300        );
301    }
302
303    /**
304     * Gets the users licenses.
305     *
306     * @since 1.7.0
307     *
308     * @since-jetpack 10.4.0
309     *
310     * @return string|WP_Error A JSON object of user licenses if the request was successful, or a WP_Error otherwise.
311     */
312    public static function get_user_licenses() {
313        $wpcom_request = Client::wpcom_json_api_request_as_user(
314            '/jetpack-licensing/user/licenses',
315            '2',
316            array(
317                'method'  => 'GET',
318                'headers' => array(
319                    'Content-Type'    => 'application/json',
320                    'X-Forwarded-For' => ( new Visitor() )->get_ip( true ),
321                ),
322            )
323        );
324
325        $response_code = wp_remote_retrieve_response_code( $wpcom_request );
326        if ( 200 === $response_code ) {
327            $licenses = json_decode( wp_remote_retrieve_body( $wpcom_request ) );
328            return $licenses;
329        } else {
330            return new WP_Error(
331                'failed_to_fetch_data',
332                esc_html__( 'Unable to fetch the requested data.', 'jetpack-licensing' ),
333                array( 'status' => $response_code )
334            );
335        }
336    }
337
338    /**
339     * Gets the users licenses counts.
340     *
341     * @since 1.7.0
342     *
343     * @since-jetpack 10.4.0
344     *
345     * @return string|WP_Error A JSON object of user license counts if the request was successful, or a WP_Error otherwise.
346     */
347    public static function get_user_license_counts() {
348        $wpcom_request = Client::wpcom_json_api_request_as_user(
349            '/jetpack-licensing/user/licenses/counts',
350            '2',
351            array(
352                'method'  => 'GET',
353                'headers' => array(
354                    'Content-Type'    => 'application/json',
355                    'X-Forwarded-For' => ( new Visitor() )->get_ip( true ),
356                ),
357            )
358        );
359
360        $response_code = wp_remote_retrieve_response_code( $wpcom_request );
361        if ( 200 === $response_code ) {
362            $license_counts = json_decode( wp_remote_retrieve_body( $wpcom_request ) );
363            return $license_counts;
364        } else {
365            return new WP_Error(
366                'failed_to_fetch_data',
367                esc_html__( 'Unable to fetch the requested data.', 'jetpack-licensing' ),
368                array( 'status' => $response_code )
369            );
370        }
371    }
372
373    /**
374     * Update the user-licenses activation notice dismissal data.
375     *
376     * @since 1.7.0
377     *
378     * @since-jetpack 10.4.0
379     *
380     * @param WP_REST_Request $request The request sent to the WP REST API.
381     *
382     * @return array|WP_Error
383     */
384    public static function update_licensing_activation_notice_dismiss( $request ) {
385
386        if ( ! isset( $request['last_detached_count'] ) ) {
387            return new WP_Error( 'invalid_param', esc_html__( 'Missing parameter "last_detached_count".', 'jetpack-licensing' ), array( 'status' => 404 ) );
388        }
389
390        $default             = array(
391            'last_detached_count' => null,
392            'last_dismissed_time' => null,
393        );
394        $last_detached_count = ( '' === $request['last_detached_count'] )
395            ? $default['last_detached_count']
396            : $request['last_detached_count'];
397        $last_dismissed_time = ( '' === $request['last_detached_count'] )
398            ? $default['last_dismissed_time']
399            // Use UTC timezone and convert to ISO8601 format(DateTime::W3C) for best compatibility with JavaScript Date in all browsers.
400            : ( new \DateTime( 'NOW', new \DateTimeZone( 'UTC' ) ) )->format( \DateTime::W3C );
401
402        $notice_data = array(
403            'last_detached_count' => $last_detached_count,
404            'last_dismissed_time' => $last_dismissed_time,
405        );
406
407        Jetpack_Options::update_option( 'licensing_activation_notice_dismiss', $notice_data, true );
408        return rest_ensure_response( $notice_data );
409    }
410
411    /**
412     * Attach Jetpack licenses
413     *
414     * @since 1.7.0
415     *
416     * @since-jetpack 10.4.0
417     *
418     * @param WP_REST_Request $request The request.
419     *
420     * @return WP_REST_Response|WP_Error A response object
421     */
422    public static function attach_jetpack_licenses( $request ) {
423        $licenses = array_map(
424            function ( $license ) {
425                return trim( sanitize_text_field( $license ) );
426            },
427            $request['licenses']
428        );
429        return rest_ensure_response( Licensing::instance()->attach_licenses( $licenses ) );
430    }
431}