Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
50.45% covered (warning)
50.45%
168 / 333
19.05% covered (danger)
19.05%
4 / 21
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_XMLRPC_Server
50.45% covered (warning)
50.45%
168 / 333
19.05% covered (danger)
19.05%
4 / 21
985.74
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 xmlrpc_methods
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
12
 bootstrap_xmlrpc_methods
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 authorize_xmlrpc_methods
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 provision_xmlrpc_methods
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 get_user
94.29% covered (success)
94.29%
33 / 35
0.00% covered (danger)
0.00%
0 / 1
6.01
 remote_authorize
65.22% covered (warning)
65.22%
15 / 23
0.00% covered (danger)
0.00%
0 / 1
12.41
 remote_register
57.14% covered (warning)
57.14%
32 / 56
0.00% covered (danger)
0.00%
0 / 1
26.30
 remote_already_registered
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 remote_provision
22.22% covered (danger)
22.22%
6 / 27
0.00% covered (danger)
0.00%
0 / 1
16.76
 remote_connect
100.00% covered (success)
100.00%
51 / 51
100.00% covered (success)
100.00%
1 / 1
9
 fetch_and_verify_local_user
27.27% covered (danger)
27.27%
3 / 11
0.00% covered (danger)
0.00%
0 / 1
3.54
 get_user_by_anything
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 verify_action
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
2.01
 login
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 error
66.67% covered (warning)
66.67%
10 / 15
0.00% covered (danger)
0.00%
0 / 1
8.81
 test_connection
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 test_api_user_code
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
90
 unlink_user
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 validate_urls_for_idc_mitigation
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 update_attachment_parent
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 disconnect_blog
n/a
0 / 0
n/a
0 / 0
2
 json_api
n/a
0 / 0
n/a
0 / 0
2
1<?php
2/**
3 * Jetpack XMLRPC Server.
4 *
5 * @package automattic/jetpack-connection
6 */
7
8use Automattic\Jetpack\Connection\Client;
9use Automattic\Jetpack\Connection\Manager as Connection_Manager;
10use Automattic\Jetpack\Connection\Secrets;
11use Automattic\Jetpack\Connection\Tokens;
12use Automattic\Jetpack\Connection\Urls;
13use Automattic\Jetpack\Constants;
14use Automattic\Jetpack\Roles;
15
16/**
17 * Just a sack of functions.  Not actually an IXR_Server
18 */
19class Jetpack_XMLRPC_Server {
20    /**
21     * The current error object
22     *
23     * @var \WP_Error
24     */
25    public $error = null;
26
27    /**
28     * The current user
29     *
30     * @var \WP_User
31     */
32    public $user = null;
33
34    /**
35     * The connection manager object.
36     *
37     * @var Automattic\Jetpack\Connection\Manager
38     */
39    private $connection;
40
41    /**
42     * Creates a new XMLRPC server object.
43     */
44    public function __construct() {
45        $this->connection = new Connection_Manager();
46    }
47
48    /**
49     * Whitelist of the XML-RPC methods available to the Jetpack Server. If the
50     * user is not authenticated (->login()) then the methods are never added,
51     * so they will get a "does not exist" error.
52     *
53     * @param array $core_methods Core XMLRPC methods.
54     */
55    public function xmlrpc_methods( $core_methods ) {
56        $jetpack_methods = array(
57            'jetpack.verifyAction'     => array( $this, 'verify_action' ),
58            'jetpack.idcUrlValidation' => array( $this, 'validate_urls_for_idc_mitigation' ),
59            'jetpack.unlinkUser'       => array( $this, 'unlink_user' ),
60            'jetpack.testConnection'   => array( $this, 'test_connection' ),
61        );
62
63        $jetpack_methods = array_merge( $jetpack_methods, $this->provision_xmlrpc_methods() );
64
65        $this->user = $this->login();
66
67        if ( $this->user ) {
68            $jetpack_methods = array_merge(
69                $jetpack_methods,
70                array(
71                    'jetpack.testAPIUserCode' => array( $this, 'test_api_user_code' ),
72                )
73            );
74
75            if ( isset( $core_methods['metaWeblog.editPost'] ) ) {
76                $jetpack_methods['metaWeblog.newMediaObject']      = $core_methods['metaWeblog.newMediaObject'];
77                $jetpack_methods['jetpack.updateAttachmentParent'] = array( $this, 'update_attachment_parent' );
78            }
79
80            /**
81             * Filters the XML-RPC methods available to Jetpack for authenticated users.
82             *
83             * @since 1.7.0
84             * @since-jetpack 1.1.0
85             *
86             * @param array    $jetpack_methods XML-RPC methods available to the Jetpack Server.
87             * @param array    $core_methods    Available core XML-RPC methods.
88             * @param \WP_User $user            Information about the user authenticated in the request.
89             */
90            $jetpack_methods = apply_filters( 'jetpack_xmlrpc_methods', $jetpack_methods, $core_methods, $this->user );
91        }
92
93        /**
94         * Filters the XML-RPC methods available to Jetpack for requests signed both with a blog token or a user token.
95         *
96         * @since 1.7.0
97         * @since 1.9.5 Introduced the $user parameter.
98         * @since-jetpack 3.0.0
99         *
100         * @param array         $jetpack_methods XML-RPC methods available to the Jetpack Server.
101         * @param array         $core_methods    Available core XML-RPC methods.
102         * @param \WP_User|bool $user            Information about the user authenticated in the request. False if authenticated with blog token.
103         */
104        return apply_filters( 'jetpack_xmlrpc_unauthenticated_methods', $jetpack_methods, $core_methods, $this->user );
105    }
106
107    /**
108     * Whitelist of the bootstrap XML-RPC methods
109     */
110    public function bootstrap_xmlrpc_methods() {
111        return array(
112            'jetpack.remoteAuthorize' => array( $this, 'remote_authorize' ),
113            'jetpack.remoteRegister'  => array( $this, 'remote_register' ),
114        );
115    }
116
117    /**
118     * Additional method needed for authorization calls.
119     */
120    public function authorize_xmlrpc_methods() {
121        return array(
122            'jetpack.remoteAuthorize' => array( $this, 'remote_authorize' ),
123            'jetpack.remoteRegister'  => array( $this, 'remote_already_registered' ),
124        );
125    }
126
127    /**
128     * Remote provisioning methods.
129     */
130    public function provision_xmlrpc_methods() {
131        return array(
132            'jetpack.remoteRegister'  => array( $this, 'remote_register' ),
133            'jetpack.remoteProvision' => array( $this, 'remote_provision' ),
134            'jetpack.remoteConnect'   => array( $this, 'remote_connect' ),
135            'jetpack.getUser'         => array( $this, 'get_user' ),
136        );
137    }
138
139    /**
140     * Used to verify whether a local user exists and what role they have.
141     *
142     * @param int|string|array $request One of:
143     *                         int|string The local User's ID, username, or email address.
144     *                         array      A request array containing:
145     *                                    0: int|string The local User's ID, username, or email address.
146     *
147     * @return array|\IXR_Error Information about the user, or error if no such user found:
148     *                          roles:     string[] The user's rols.
149     *                          login:     string   The user's username.
150     *                          email_hash string[] The MD5 hash of the user's normalized email address.
151     *                          caps       string[] The user's capabilities.
152     *                          allcaps    string[] The user's granular capabilities, merged from role capabilities.
153     *                          token_key  string   The Token Key of the user's Jetpack token. Empty string if none.
154     */
155    public function get_user( $request ) {
156        $user_id = is_array( $request ) ? $request[0] : $request;
157
158        if ( ! $user_id ) {
159            return $this->error(
160                new \WP_Error(
161                    'invalid_user',
162                    __( 'Invalid user identifier.', 'jetpack-connection' ),
163                    400
164                ),
165                'get_user'
166            );
167        }
168
169        $user = $this->get_user_by_anything( $user_id );
170
171        if ( ! $user ) {
172            return $this->error(
173                new \WP_Error(
174                    'user_unknown',
175                    __( 'User not found.', 'jetpack-connection' ),
176                    404
177                ),
178                'get_user'
179            );
180        }
181
182        $user_token = ( new Tokens() )->get_access_token( $user->ID );
183
184        if ( $user_token ) {
185            list( $user_token_key ) = explode( '.', $user_token->secret );
186            if ( $user_token_key === $user_token->secret ) {
187                $user_token_key = '';
188            }
189        } else {
190            $user_token_key = '';
191        }
192
193        return array(
194            'id'         => $user->ID,
195            'login'      => $user->user_login,
196            'email_hash' => md5( strtolower( trim( $user->user_email ) ) ),
197            'roles'      => $user->roles,
198            'caps'       => $user->caps,
199            'allcaps'    => $user->allcaps,
200            'token_key'  => $user_token_key,
201        );
202    }
203
204    /**
205     * Remote authorization XMLRPC method handler.
206     *
207     * @param array $request the request.
208     */
209    public function remote_authorize( $request ) {
210        $user = get_user_by( 'id', $request['state'] );
211
212        /**
213         * Happens on various request handling events in the Jetpack XMLRPC server.
214         * The action combines several types of events:
215         *    - remote_authorize
216         *    - remote_provision
217         *    - get_user.
218         *
219         * @since 1.7.0
220         * @since-jetpack 8.0.0
221         *
222         * @param String  $action the action name, i.e., 'remote_authorize'.
223         * @param String  $stage  the execution stage, can be 'begin', 'success', 'error', etc.
224         * @param array   $parameters extra parameters from the event.
225         * @param WP_User $user the acting user.
226         */
227        do_action( 'jetpack_xmlrpc_server_event', 'remote_authorize', 'begin', array(), $user );
228
229        foreach ( array( 'secret', 'state', 'redirect_uri', 'code' ) as $required ) {
230            if ( ! isset( $request[ $required ] ) || empty( $request[ $required ] ) ) {
231                return $this->error(
232                    new \WP_Error( 'missing_parameter', 'One or more parameters is missing from the request.', 400 ),
233                    'remote_authorize'
234                );
235            }
236        }
237
238        if ( ! $user ) {
239            return $this->error( new \WP_Error( 'user_unknown', 'User not found.', 404 ), 'remote_authorize' );
240        }
241
242        if ( $this->connection->has_connected_owner() && $this->connection->is_user_connected( $request['state'] ) ) {
243            return $this->error( new \WP_Error( 'already_connected', 'User already connected.', 400 ), 'remote_authorize' );
244        }
245
246        $verified = $this->verify_action( array( 'authorize', $request['secret'], $request['state'] ) );
247
248        if ( is_a( $verified, 'IXR_Error' ) ) {
249            return $this->error( $verified, 'remote_authorize' );
250        }
251
252        wp_set_current_user( $request['state'] );
253
254        $result = $this->connection->authorize( $request );
255
256        if ( is_wp_error( $result ) ) {
257            return $this->error( $result, 'remote_authorize' );
258        }
259
260        // This action is documented in class.jetpack-xmlrpc-server.php.
261        do_action( 'jetpack_xmlrpc_server_event', 'remote_authorize', 'success' );
262
263        return array(
264            'result' => $result,
265        );
266    }
267
268    /**
269     * This XML-RPC method is called from the /jpphp/provision endpoint on WPCOM in order to
270     * register this site so that a plan can be provisioned.
271     *
272     * @param array|ArrayAccess $request An array containing at minimum nonce and local_user keys.
273     *
274     * @return \WP_Error|array
275     */
276    public function remote_register( $request ) {
277        // This action is documented in class.jetpack-xmlrpc-server.php.
278        do_action( 'jetpack_xmlrpc_server_event', 'remote_register', 'begin', array() );
279
280        $user = $this->fetch_and_verify_local_user( $request );
281
282        if ( ! $user ) {
283            return $this->error(
284                new WP_Error( 'input_error', __( 'Valid user is required', 'jetpack-connection' ), 400 ),
285                'remote_register'
286            );
287        }
288
289        if ( is_wp_error( $user ) || is_a( $user, 'IXR_Error' ) ) {
290            return $this->error( $user, 'remote_register' );
291        }
292
293        if ( empty( $request['nonce'] ) ) {
294            return $this->error(
295                new \WP_Error(
296                    'nonce_missing',
297                    __( 'The required "nonce" parameter is missing.', 'jetpack-connection' ),
298                    400
299                ),
300                'remote_register'
301            );
302        }
303
304        $nonce = sanitize_text_field( $request['nonce'] );
305        unset( $request['nonce'] );
306
307        $api_url = $this->connection->api_url( 'partner_provision_nonce_check' );
308        // @phan-suppress-next-line PhanAccessMethodInternal -- Phan is correct, but the usage is intentional.
309        $response = Client::_wp_remote_request(
310            esc_url_raw( add_query_arg( 'nonce', $nonce, $api_url ) ),
311            array( 'method' => 'GET' ),
312            true
313        );
314
315        if (
316            200 !== wp_remote_retrieve_response_code( $response ) ||
317            'OK' !== trim( wp_remote_retrieve_body( $response ) )
318        ) {
319            return $this->error(
320                new \WP_Error(
321                    'invalid_nonce',
322                    __( 'There was an issue validating this request.', 'jetpack-connection' ),
323                    400
324                ),
325                'remote_register'
326            );
327        }
328
329        if ( ! Jetpack_Options::get_option( 'id' ) || ! ( new Tokens() )->get_access_token() || ! empty( $request['force'] ) ) {
330            wp_set_current_user( $user->ID );
331
332            // This code mostly copied from Jetpack::admin_page_load.
333            if ( isset( $request['from'] ) ) {
334                $this->connection->add_register_request_param( 'from', (string) $request['from'] );
335            }
336            $registered = $this->connection->try_registration();
337            if ( is_wp_error( $registered ) ) {
338                return $this->error( $registered, 'remote_register' );
339            } elseif ( ! $registered ) {
340                return $this->error(
341                    new \WP_Error(
342                        'registration_error',
343                        __( 'There was an unspecified error registering the site', 'jetpack-connection' ),
344                        400
345                    ),
346                    'remote_register'
347                );
348            }
349        }
350
351        // This action is documented in class.jetpack-xmlrpc-server.php.
352        do_action( 'jetpack_xmlrpc_server_event', 'remote_register', 'success' );
353
354        return array(
355            'client_id' => Jetpack_Options::get_option( 'id' ),
356        );
357    }
358
359    /**
360     * This is a substitute for remote_register() when the blog is already registered which returns an error code
361     * signifying that state.
362     * This is an unauthorized call and we should not be responding with any data other than the error code.
363     *
364     * @return \IXR_Error
365     */
366    public function remote_already_registered() {
367        return $this->error(
368            new \WP_Error( 'already_registered', __( 'Blog is already registered', 'jetpack-connection' ), 400 ),
369            'remote_register'
370        );
371    }
372
373    /**
374     * This XML-RPC method is called from the /jpphp/provision endpoint on WPCOM in order to
375     * register this site so that a plan can be provisioned.
376     *
377     * @param array|ArrayAccess $request An array containing at minimum a nonce key and a local_username key.
378     *
379     * @return \WP_Error|array
380     */
381    public function remote_provision( $request ) {
382        $user = $this->fetch_and_verify_local_user( $request );
383
384        if ( ! $user ) {
385            return $this->error(
386                new WP_Error( 'input_error', __( 'Valid user is required', 'jetpack-connection' ), 400 ),
387                'remote_provision'
388            );
389        }
390
391        if ( is_wp_error( $user ) || is_a( $user, 'IXR_Error' ) ) {
392            return $this->error( $user, 'remote_provision' );
393        }
394
395        $site_icon = get_site_icon_url();
396
397        /**
398         * Filters the Redirect URI returned by the remote_register XMLRPC method
399         *
400         * @param string $redirect_uri The Redirect URI
401         *
402         * @since 1.9.7
403         */
404        $redirect_uri = apply_filters( 'jetpack_xmlrpc_remote_register_redirect_uri', admin_url() );
405
406        // Generate secrets.
407        $roles   = new Roles();
408        $role    = $roles->translate_user_to_role( $user );
409        $secrets = ( new Secrets() )->generate( 'authorize', $user->ID );
410
411        $response = array(
412            'jp_version'   => Constants::get_constant( 'JETPACK__VERSION' ),
413            'redirect_uri' => $redirect_uri,
414            'user_id'      => $user->ID,
415            'user_email'   => $user->user_email,
416            'user_login'   => $user->user_login,
417            'scope'        => $this->connection->sign_role( $role, $user->ID ),
418            'secret'       => $secrets['secret_1'],
419            'is_active'    => $this->connection->has_connected_owner(),
420        );
421
422        if ( $site_icon ) {
423            $response['site_icon'] = $site_icon;
424        }
425
426        /**
427         * Filters the response of the remote_provision XMLRPC method
428         *
429         * @param array    $response The response.
430         * @param array    $request An array containing at minimum a nonce key and a local_username key.
431         * @param \WP_User $user The local authenticated user.
432         *
433         * @since 1.9.7
434         */
435        $response = apply_filters( 'jetpack_remote_xmlrpc_provision_response', $response, $request, $user );
436
437        return $response;
438    }
439
440    /**
441     * Given an array containing a local user identifier and a nonce, will attempt to fetch and set
442     * an access token for the given user.
443     *
444     * @param array|ArrayAccess $request An array containing local_user and nonce keys at minimum.
445     * @param \IXR_Client       $ixr_client The client object, optional.
446     * @return mixed
447     */
448    public function remote_connect( $request, $ixr_client = false ) {
449        if ( $this->connection->has_connected_owner() ) {
450            return $this->error(
451                new WP_Error(
452                    'already_connected',
453                    __( 'Jetpack is already connected.', 'jetpack-connection' ),
454                    400
455                ),
456                'remote_connect'
457            );
458        }
459
460        $user = $this->fetch_and_verify_local_user( $request );
461
462        if ( ! $user || is_wp_error( $user ) || is_a( $user, 'IXR_Error' ) ) {
463            return $this->error(
464                new WP_Error(
465                    'input_error',
466                    __( 'Valid user is required.', 'jetpack-connection' ),
467                    400
468                ),
469                'remote_connect'
470            );
471        }
472
473        if ( empty( $request['nonce'] ) ) {
474            return $this->error(
475                new WP_Error(
476                    'input_error',
477                    __( 'A non-empty nonce must be supplied.', 'jetpack-connection' ),
478                    400
479                ),
480                'remote_connect'
481            );
482        }
483
484        if ( ! $ixr_client ) {
485            $ixr_client = new Jetpack_IXR_Client();
486        }
487        // TODO: move this query into the Tokens class?
488        $ixr_client->query(
489            'jetpack.getUserAccessToken',
490            array(
491                'nonce'            => sanitize_text_field( $request['nonce'] ),
492                'external_user_id' => $user->ID,
493            )
494        );
495
496        $token = $ixr_client->isError() ? false : $ixr_client->getResponse();
497        if ( empty( $token ) ) {
498            return $this->error(
499                new WP_Error(
500                    'token_fetch_failed',
501                    __( 'Failed to fetch user token from WordPress.com.', 'jetpack-connection' ),
502                    400
503                ),
504                'remote_connect'
505            );
506        }
507        $token = sanitize_text_field( $token );
508
509        ( new Tokens() )->update_user_token( $user->ID, sprintf( '%s.%d', $token, $user->ID ), true );
510
511        /**
512         * Hook fired at the end of the jetpack.remoteConnect XML-RPC callback
513         *
514         * @since 1.9.7
515         */
516        do_action( 'jetpack_remote_connect_end' );
517
518        return $this->connection->has_connected_owner();
519    }
520
521    /**
522     * Getter for the local user to act as.
523     *
524     * @param array $request the current request data.
525     * @return WP_User|IXR_Error|false IXR_Error if the request is missing a local_user field, WP_User object on success, or false on failure to find a user.
526     */
527    private function fetch_and_verify_local_user( $request ) {
528        if ( empty( $request['local_user'] ) ) {
529            return $this->error(
530                new \WP_Error(
531                    'local_user_missing',
532                    __( 'The required "local_user" parameter is missing.', 'jetpack-connection' ),
533                    400
534                ),
535                'remote_provision'
536            );
537        }
538
539        // Local user is used to look up by login, email or ID.
540        $local_user_info = $request['local_user'];
541
542        return $this->get_user_by_anything( $local_user_info );
543    }
544
545    /**
546     * Gets the user object by its data.
547     *
548     * @param string $user_id can be any identifying user data.
549     * @return WP_User|false WP_User object on success, false on failure.
550     */
551    private function get_user_by_anything( $user_id ) {
552        $user = get_user_by( 'login', $user_id );
553
554        if ( ! $user ) {
555            $user = get_user_by( 'email', $user_id );
556        }
557
558        if ( ! $user ) {
559            $user = get_user_by( 'ID', $user_id );
560        }
561
562        return $user;
563    }
564
565    /**
566     * Possible error_codes:
567     *
568     * - verify_secret_1_missing
569     * - verify_secret_1_malformed
570     * - verify_secrets_missing: verification secrets are not found in database
571     * - verify_secrets_incomplete: verification secrets are only partially found in database
572     * - verify_secrets_expired: verification secrets have expired
573     * - verify_secrets_mismatch: stored secret_1 does not match secret_1 sent by Jetpack.WordPress.com
574     * - state_missing: required parameter of state not found
575     * - state_malformed: state is not a digit
576     * - invalid_state: state in request does not match the stored state
577     *
578     * The 'authorize' and 'register' actions have additional error codes
579     *
580     * state_missing: a state ( user id ) was not supplied
581     * state_malformed: state is not the correct data type
582     * invalid_state: supplied state does not match the stored state
583     *
584     * @param array $params action An array of 3 parameters:
585     *     [0]: string action. Possible values are `authorize`, `publicize` and `register`.
586     *     [1]: string secret_1.
587     *     [2]: int state.
588     * @return \IXR_Error|string IXR_Error on failure, secret_2 on success.
589     */
590    public function verify_action( $params ) {
591        $action        = $params[0] ?? '';
592        $verify_secret = $params[1] ?? '';
593        $state         = $params[2] ?? '';
594
595        $result = ( new Secrets() )->verify( $action, $verify_secret, $state );
596
597        if ( is_wp_error( $result ) ) {
598            return $this->error( $result );
599        }
600
601        return $result;
602    }
603
604    /**
605     * Wrapper for wp_authenticate( $username, $password );
606     *
607     * @return \WP_User|bool
608     */
609    public function login() {
610        $this->connection->require_jetpack_authentication();
611        $user = wp_authenticate( 'username', 'password' );
612        if ( is_wp_error( $user ) ) {
613            if ( 'authentication_failed' === $user->get_error_code() ) { // Generic error could mean most anything.
614                $this->error = new \WP_Error( 'invalid_request', 'Invalid Request', 403 );
615            } else {
616                $this->error = $user;
617            }
618            return false;
619        } elseif ( ! $user ) { // Shouldn't happen.
620            $this->error = new \WP_Error( 'invalid_request', 'Invalid Request', 403 );
621            return false;
622        }
623
624        wp_set_current_user( $user->ID );
625
626        return $user;
627    }
628
629    /**
630     * Returns the current error as an \IXR_Error
631     *
632     * @param \WP_Error|\IXR_Error $error             The error object, optional.
633     * @param string               $event_name The event name.
634     * @param \WP_User             $user              The user object.
635     * @return bool|\IXR_Error
636     */
637    public function error( $error = null, $event_name = null, $user = null ) {
638        if ( null !== $event_name ) {
639            // This action is documented in class.jetpack-xmlrpc-server.php.
640            do_action( 'jetpack_xmlrpc_server_event', $event_name, 'fail', $error, $user );
641        }
642
643        if ( $error !== null ) {
644            $this->error = $error;
645        }
646
647        if ( is_wp_error( $this->error ) ) {
648            $code = $this->error->get_error_data();
649            if ( ! $code ) {
650                $code = -10520;
651            }
652            $message = sprintf( 'Jetpack: [%s] %s', $this->error->get_error_code(), $this->error->get_error_message() );
653            if ( ! class_exists( \IXR_Error::class ) ) {
654                require_once ABSPATH . WPINC . '/class-IXR.php';
655            }
656            return new \IXR_Error( $code, $message );
657        } elseif ( is_a( $this->error, 'IXR_Error' ) ) {
658            return $this->error;
659        }
660
661        return false;
662    }
663
664    /* API Methods */
665
666    /**
667     * Just authenticates with the given Jetpack credentials.
668     *
669     * @return string A success string. The Jetpack plugin filters it and make it return the Jetpack plugin version.
670     */
671    public function test_connection() {
672        /**
673         * Filters the successful response of the XMLRPC test_connection method
674         *
675         * @param string $response The response string.
676         */
677        return apply_filters( 'jetpack_xmlrpc_test_connection_response', 'success' );
678    }
679
680    /**
681     * Test the API user code.
682     *
683     * @param array $args arguments identifying the test site.
684     */
685    public function test_api_user_code( $args ) {
686        $client_id = (int) $args[0];
687        $user_id   = (int) $args[1];
688        $nonce     = (string) $args[2];
689        $verify    = (string) $args[3];
690
691        if ( ! $client_id || ! $user_id || ! strlen( $nonce ) || 32 !== strlen( $verify ) ) {
692            return false;
693        }
694
695        $user = get_user_by( 'id', $user_id );
696        if ( ! $user || is_wp_error( $user ) ) {
697            return false;
698        }
699
700        /* phpcs:ignore
701         debugging
702        error_log( "CLIENT: $client_id" );
703        error_log( "USER:   $user_id" );
704        error_log( "NONCE:  $nonce" );
705        error_log( "VERIFY: $verify" );
706        */
707
708        $jetpack_token = ( new Tokens() )->get_access_token( $user_id );
709
710        $api_user_code = get_user_meta( $user_id, "jetpack_json_api_$client_id", true );
711        if ( ! $api_user_code ) {
712            return false;
713        }
714
715        $hmac = hash_hmac(
716            'md5',
717            json_encode( // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode
718                (object) array(
719                    'client_id' => $client_id,
720                    'user_id'   => $user_id,
721                    'nonce'     => $nonce,
722                    'code'      => (string) $api_user_code,
723                ),
724                0 // phpcs:ignore Jetpack.Functions.JsonEncodeFlags.ZeroFound -- No `json_encode()` flags because this needs to match whatever is calculating the hash on the other end.
725            ),
726            $jetpack_token->secret
727        );
728
729        if ( ! hash_equals( $hmac, $verify ) ) {
730            return false;
731        }
732
733        return $user_id;
734    }
735
736    /**
737     * Unlink a user from WordPress.com
738     *
739     * When the request is done without any parameter, this XMLRPC callback gets an empty array as input.
740     *
741     * If $user_id is not provided, it will try to disconnect the current logged in user. This will fail if called by the Master User.
742     *
743     * If $user_id is is provided, it will try to disconnect the informed user, even if it's the Master User.
744     *
745     * @param mixed $user_id The user ID to disconnect from this site.
746     */
747    public function unlink_user( $user_id = array() ) {
748        $user_id = (int) $user_id;
749        if ( $user_id < 1 ) {
750            $user_id = null;
751        }
752        /**
753         * Fired when we want to log an event to the Jetpack event log.
754         *
755         * @since 1.7.0
756         * @since-jetpack 7.7.0
757         *
758         * @param string $code Unique name for the event.
759         * @param string $data Optional data about the event.
760         */
761        do_action( 'jetpack_event_log', 'unlink' );
762        return $this->connection->disconnect_user(
763            $user_id,
764            (bool) $user_id
765        );
766    }
767
768    /**
769     * Returns the home URL, site URL, and URL secret for the current site which can be used on the WPCOM side for
770     * IDC mitigation to decide whether sync should be allowed if the home and siteurl values differ between WPCOM
771     * and the remote Jetpack site.
772     *
773     * @since 1.56.0 Additional data may be added via filter `jetpack_connection_validate_urls_for_idc_mitigation_response`.
774     *
775     * @return array
776     */
777    public function validate_urls_for_idc_mitigation() {
778        $response = array(
779            'home'    => Urls::home_url(),
780            'siteurl' => Urls::site_url(),
781        );
782
783        /**
784         * Allows modifying the response.
785         *
786         * @since 1.56.0
787         *
788         * @param array $response
789         */
790        return apply_filters( 'jetpack_connection_validate_urls_for_idc_mitigation_response', $response );
791    }
792
793    /**
794     * Updates the attachment parent object.
795     *
796     * @param array $args attachment and parent identifiers.
797     */
798    public function update_attachment_parent( $args ) {
799        $attachment_id = (int) $args[0];
800        $parent_id     = (int) $args[1];
801
802        return wp_update_post(
803            array(
804                'ID'          => $attachment_id,
805                'post_parent' => $parent_id,
806            )
807        );
808    }
809
810    /**
811     * Deprecated: This method is no longer part of the Connection package and now lives on the Jetpack plugin.
812     *
813     * Disconnect this blog from the connected wordpress.com account
814     *
815     * @deprecated since 1.25.0
816     * @see Jetpack_XMLRPC_Methods::disconnect_blog() in the Jetpack plugin
817     *
818     * @return boolean
819     */
820    public function disconnect_blog() {
821        _deprecated_function( __METHOD__, '1.25.0', 'Jetpack_XMLRPC_Methods::disconnect_blog()' );
822        if ( class_exists( 'Jetpack_XMLRPC_Methods' ) ) {
823            return Jetpack_XMLRPC_Methods::disconnect_blog();
824        }
825        return false;
826    }
827
828    /**
829     * Deprecated: This method is no longer part of the Connection package and now lives on the Jetpack plugin.
830     *
831     * Serve a JSON API request.
832     *
833     * @deprecated since 1.25.0
834     * @see Jetpack_XMLRPC_Methods::json_api() in the Jetpack plugin
835     *
836     * @param array $args request arguments.
837     */
838    public function json_api( $args = array() ) {
839        _deprecated_function( __METHOD__, '1.25.0', 'Jetpack_XMLRPC_Methods::json_api()' );
840        if ( class_exists( 'Jetpack_XMLRPC_Methods' ) ) {
841            return Jetpack_XMLRPC_Methods::json_api( $args );
842        }
843        return array();
844    }
845}