Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
50.45% |
168 / 333 |
|
19.05% |
4 / 21 |
CRAP | |
0.00% |
0 / 1 |
| Jetpack_XMLRPC_Server | |
50.45% |
168 / 333 |
|
19.05% |
4 / 21 |
985.74 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| xmlrpc_methods | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
12 | |||
| bootstrap_xmlrpc_methods | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
| authorize_xmlrpc_methods | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
| provision_xmlrpc_methods | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
| get_user | |
94.29% |
33 / 35 |
|
0.00% |
0 / 1 |
6.01 | |||
| remote_authorize | |
65.22% |
15 / 23 |
|
0.00% |
0 / 1 |
12.41 | |||
| remote_register | |
57.14% |
32 / 56 |
|
0.00% |
0 / 1 |
26.30 | |||
| remote_already_registered | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
| remote_provision | |
22.22% |
6 / 27 |
|
0.00% |
0 / 1 |
16.76 | |||
| remote_connect | |
100.00% |
51 / 51 |
|
100.00% |
1 / 1 |
9 | |||
| fetch_and_verify_local_user | |
27.27% |
3 / 11 |
|
0.00% |
0 / 1 |
3.54 | |||
| get_user_by_anything | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
| verify_action | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
2.01 | |||
| login | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 | |||
| error | |
66.67% |
10 / 15 |
|
0.00% |
0 / 1 |
8.81 | |||
| test_connection | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| test_api_user_code | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
90 | |||
| unlink_user | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
| validate_urls_for_idc_mitigation | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
| update_attachment_parent | |
0.00% |
0 / 8 |
|
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 | |
| 8 | use Automattic\Jetpack\Connection\Client; |
| 9 | use Automattic\Jetpack\Connection\Manager as Connection_Manager; |
| 10 | use Automattic\Jetpack\Connection\Secrets; |
| 11 | use Automattic\Jetpack\Connection\Tokens; |
| 12 | use Automattic\Jetpack\Connection\Urls; |
| 13 | use Automattic\Jetpack\Constants; |
| 14 | use Automattic\Jetpack\Roles; |
| 15 | |
| 16 | /** |
| 17 | * Just a sack of functions. Not actually an IXR_Server |
| 18 | */ |
| 19 | class 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 | } |