Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
69.10% covered (warning)
69.10%
123 / 178
50.00% covered (danger)
50.00%
6 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
WPCOM_REST_API_V2_Endpoint_Service_API_Keys
70.29% covered (warning)
70.29%
123 / 175
50.00% covered (danger)
50.00%
6 / 12
53.61
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 register_routes
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
1
 edit_others_posts_check
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 get_public_item_schema
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
2
 get_service_api_key
85.71% covered (warning)
85.71%
18 / 21
0.00% covered (danger)
0.00%
0 / 1
5.07
 update_service_api_key
52.63% covered (warning)
52.63%
10 / 19
0.00% covered (danger)
0.00%
0 / 1
5.70
 delete_service_api_key
86.36% covered (warning)
86.36%
19 / 22
0.00% covered (danger)
0.00%
0 / 1
5.06
 validate_service_api_service
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 service_api_invalid_service_response
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 validate_service_api_key
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 validate_service_api_key_mapbox
81.25% covered (warning)
81.25%
26 / 32
0.00% covered (danger)
0.00%
0 / 1
5.16
 key_for_api_service
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2/**
3 * Get and save API keys for a site.
4 *
5 * @package automattic/jetpack
6 */
7
8if ( ! defined( 'ABSPATH' ) ) {
9    exit( 0 );
10}
11
12/**
13 * Service API Keys: Exposes 3rd party api keys that are used on a site.
14 *
15 * [
16 *   { # Availability Object. See schema for more detail.
17 *      code:                   (string) Displays success if the operation was successfully executed and an error code if it was not
18 *      service:                (string) The name of the service in question
19 *      service_api_key:        (string) The API key used by the service empty if one is not set yet
20 *      service_api_key_source: (string) The source of the API key, defaults to "site"
21 *      message:                (string) User friendly message
22 *   },
23 *   ...
24 * ]
25 *
26 * @since 6.9
27 */
28class WPCOM_REST_API_V2_Endpoint_Service_API_Keys extends WP_REST_Controller {
29
30    /**
31     * Constructor.
32     */
33    public function __construct() {
34        $this->namespace = 'wpcom/v2';
35        $this->rest_base = 'service-api-keys';
36
37        add_action( 'rest_api_init', array( $this, 'register_routes' ) );
38    }
39
40    /**
41     * Register endpoint routes.
42     */
43    public function register_routes() {
44        register_rest_route(
45            'wpcom/v2',
46            '/service-api-keys/(?P<service>[a-z\-_]+)',
47            array(
48                array(
49                    'methods'             => WP_REST_Server::READABLE,
50                    'callback'            => array( __CLASS__, 'get_service_api_key' ),
51                    'permission_callback' => '__return_true',
52                ),
53                array(
54                    'methods'             => WP_REST_Server::EDITABLE,
55                    'callback'            => array( __CLASS__, 'update_service_api_key' ),
56                    'permission_callback' => array( __CLASS__, 'edit_others_posts_check' ),
57                    'args'                => array(
58                        'service_api_key' => array(
59                            'required' => true,
60                            'type'     => 'string',
61                        ),
62                    ),
63                ),
64                array(
65                    'methods'             => WP_REST_Server::DELETABLE,
66                    'callback'            => array( __CLASS__, 'delete_service_api_key' ),
67                    'permission_callback' => array( __CLASS__, 'edit_others_posts_check' ),
68                ),
69            )
70        );
71    }
72
73    /**
74     * Permission check.
75     */
76    public static function edit_others_posts_check() {
77        if ( current_user_can( 'edit_others_posts' ) ) {
78            return true;
79        }
80
81        $user_permissions_error_msg = esc_html__(
82            'You do not have the correct user permissions to perform this action.
83            Please contact your site admin if you think this is a mistake.',
84            'jetpack'
85        );
86
87        return new WP_Error( 'invalid_user_permission_edit_others_posts', $user_permissions_error_msg, rest_authorization_required_code() );
88    }
89
90    /**
91     * Return the available Gutenberg extensions schema
92     *
93     * @return array Service API Key schema
94     */
95    public function get_public_item_schema() {
96        $schema = array(
97            '$schema'    => 'http://json-schema.org/draft-04/schema#',
98            'title'      => 'service-api-keys',
99            'type'       => 'object',
100            'properties' => array(
101                'code'                   => array(
102                    'description' => __( 'Displays success if the operation was successfully executed and an error code if it was not', 'jetpack' ),
103                    'type'        => 'string',
104                ),
105                'service'                => array(
106                    'description' => __( 'The name of the service in question', 'jetpack' ),
107                    'type'        => 'string',
108                ),
109                'service_api_key'        => array(
110                    'description' => __( 'The API key used by the service. Empty if none has been set yet', 'jetpack' ),
111                    'type'        => 'string',
112                ),
113                'service_api_key_source' => array(
114                    'description' => __( 'The source of the API key. Defaults to "site"', 'jetpack' ),
115                    'type'        => 'string',
116                ),
117                'message'                => array(
118                    'description' => __( 'User friendly message', 'jetpack' ),
119                    'type'        => 'string',
120                ),
121            ),
122        );
123
124        return $this->add_additional_fields_schema( $schema );
125    }
126
127    /**
128     * Get third party plugin API keys.
129     *
130     * @param WP_REST_Request $request {
131     *     Array of parameters received by request.
132     *
133     *     @type string $slug Plugin slug with the syntax 'plugin-directory/plugin-main-file.php'.
134     * }
135     */
136    public static function get_service_api_key( $request ) {
137        $service = self::validate_service_api_service( $request['service'] );
138        if ( ! $service ) {
139            return self::service_api_invalid_service_response();
140        }
141
142        switch ( $service ) {
143            case 'mapbox':
144                if ( ! class_exists( 'Jetpack_Mapbox_Helper' ) ) {
145                    require_once JETPACK__PLUGIN_DIR . '_inc/lib/class-jetpack-mapbox-helper.php';
146                }
147                $mapbox                 = Jetpack_Mapbox_Helper::get_access_token();
148                $service_api_key        = $mapbox['key'];
149                $service_api_key_source = $mapbox['source'];
150                break;
151            default:
152                $option                 = self::key_for_api_service( $service );
153                $service_api_key        = Jetpack_Options::get_option( $option, '' );
154                $service_api_key_source = 'site';
155        }
156
157        $message = esc_html__( 'API key retrieved successfully.', 'jetpack' );
158
159        return array(
160            'code'                   => 'success',
161            'service'                => $service,
162            'service_api_key'        => $service_api_key,
163            'service_api_key_source' => $service_api_key_source,
164            'message'                => $message,
165        );
166    }
167
168    /**
169     * Update third party plugin API keys.
170     *
171     * @param WP_REST_Request $request {
172     *     Array of parameters received by request.
173     *
174     *     @type string $slug Plugin slug with the syntax 'plugin-directory/plugin-main-file.php'.
175     * }
176     */
177    public static function update_service_api_key( $request ) {
178        $service = self::validate_service_api_service( $request['service'] );
179        if ( ! $service ) {
180            return self::service_api_invalid_service_response();
181        }
182        $json_params     = $request->get_json_params();
183        $params          = ! empty( $json_params ) ? $json_params : $request->get_body_params();
184        $service_api_key = trim( $params['service_api_key'] );
185        $option          = self::key_for_api_service( $service );
186
187        $validation = self::validate_service_api_key( $service_api_key, $service );
188        if ( ! $validation['status'] ) {
189            return new WP_Error( 'invalid_key', esc_html__( 'Invalid API Key', 'jetpack' ), array( 'status' => 404 ) );
190        }
191        $message = esc_html__( 'API key updated successfully.', 'jetpack' );
192        Jetpack_Options::update_option( $option, $service_api_key );
193        return array(
194            'code'                   => 'success',
195            'service'                => $service,
196            'service_api_key'        => Jetpack_Options::get_option( $option, '' ),
197            'service_api_key_source' => 'site',
198            'message'                => $message,
199        );
200    }
201
202    /**
203     * Delete a third party plugin API key.
204     *
205     * @param WP_REST_Request $request {
206     *     Array of parameters received by request.
207     *
208     *     @type string $slug Plugin slug with the syntax 'plugin-directory/plugin-main-file.php'.
209     * }
210     */
211    public static function delete_service_api_key( $request ) {
212        $service = self::validate_service_api_service( $request['service'] );
213        if ( ! $service ) {
214            return self::service_api_invalid_service_response();
215        }
216        $option = self::key_for_api_service( $service );
217        Jetpack_Options::delete_option( $option );
218        $message = esc_html__( 'API key deleted successfully.', 'jetpack' );
219
220        switch ( $service ) {
221            case 'mapbox':
222                // After deleting a custom Mapbox key, try to revert to the WordPress.com one if available.
223                if ( ! class_exists( 'Jetpack_Mapbox_Helper' ) ) {
224                    require_once JETPACK__PLUGIN_DIR . '_inc/lib/class-jetpack-mapbox-helper.php';
225                }
226                $mapbox                 = Jetpack_Mapbox_Helper::get_access_token();
227                $service_api_key        = $mapbox['key'];
228                $service_api_key_source = $mapbox['source'];
229                break;
230            default:
231                $service_api_key        = Jetpack_Options::get_option( $option, '' );
232                $service_api_key_source = 'site';
233        }
234
235        return array(
236            'code'                   => 'success',
237            'service'                => $service,
238            'service_api_key'        => $service_api_key,
239            'service_api_key_source' => $service_api_key_source,
240            'message'                => $message,
241        );
242    }
243
244    /**
245     * Validate the service provided in /service-api-keys/ endpoints.
246     * To add a service to these endpoints, add the service name to $valid_services
247     * and add '{service name}_api_key' to the non-compact return array in get_option_names(),
248     * in class-jetpack-options.php
249     *
250     * @param string $service The service the API key is for.
251     * @return string Returns the service name if valid, null if invalid.
252     */
253    public static function validate_service_api_service( $service = null ) {
254        $valid_services = array(
255            'mapbox',
256        );
257        return in_array( $service, $valid_services, true ) ? $service : null;
258    }
259
260    /**
261     * Error response for invalid service API key requests with an invalid service.
262     */
263    public static function service_api_invalid_service_response() {
264        return new WP_Error(
265            'invalid_service',
266            esc_html__( 'Invalid Service', 'jetpack' ),
267            array( 'status' => 404 )
268        );
269    }
270
271    /**
272     * Validate API Key
273     *
274     * @param string $key The API key to be validated.
275     * @param string $service The service the API key is for.
276     */
277    public static function validate_service_api_key( $key = null, $service = null ) {
278        $validation = false;
279        switch ( $service ) {
280            case 'mapbox':
281                $validation = self::validate_service_api_key_mapbox( $key );
282                break;
283        }
284        return $validation;
285    }
286
287    /**
288     * Validate Mapbox API key
289     * Based loosely on https://github.com/mapbox/geocoding-example/blob/master/php/MapboxTest.php
290     *
291     * @param string $key The API key to be validated.
292     */
293    public static function validate_service_api_key_mapbox( $key ) {
294        $status          = true;
295        $msg             = null;
296        $mapbox_url      = sprintf(
297            'https://api.mapbox.com?%s',
298            $key
299        );
300        $mapbox_response = wp_safe_remote_get( esc_url_raw( $mapbox_url ) );
301        $mapbox_body     = wp_remote_retrieve_body( $mapbox_response );
302        if ( '{"api":"mapbox"}' !== $mapbox_body ) {
303            $status = false;
304            $msg    = esc_html__( 'Can\'t connect to Mapbox', 'jetpack' );
305            return array(
306                'status'        => $status,
307                'error_message' => $msg,
308            );
309        }
310        $mapbox_geocode_url      = esc_url_raw(
311            sprintf(
312                'https://api.mapbox.com/geocoding/v5/mapbox.places/%s.json?access_token=%s',
313                '1+broadway+new+york+ny+usa',
314                $key
315            )
316        );
317        $mapbox_geocode_response = wp_safe_remote_get( esc_url_raw( $mapbox_geocode_url ) );
318        $mapbox_geocode_body     = wp_remote_retrieve_body( $mapbox_geocode_response );
319        $mapbox_geocode_json     = json_decode( $mapbox_geocode_body );
320        if ( isset( $mapbox_geocode_json->message ) || ! isset( $mapbox_geocode_json->query ) ) {
321            $status = false;
322            $msg    = isset( $mapbox_geocode_json->message ) ? $mapbox_geocode_json->message : 'Unknown error';
323        }
324        return array(
325            'status'        => $status,
326            'error_message' => $msg,
327        );
328    }
329
330    /**
331     * Create site option key for service
332     *
333     * @param string $service The service  to create key for.
334     */
335    private static function key_for_api_service( $service ) {
336        return $service . '_api_key';
337    }
338}
339wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Service_API_Keys' );