PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 4.3.5
Jetpack – WP Security, Backup, Speed, & Growth v4.3.5
16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 14.3.1 All 501 releases
jetpack / class.jetpack-xmlrpc-server.php
class.jetpack-xmlrpc-server.php
504 lines 16.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Just a sack of functions. Not actually an IXR_Server
5 */
6 class Jetpack_XMLRPC_Server {
7 /**
8 * The current error object
9 */
10 public $error = null;
11
12 /**
13 * Whitelist of the XML-RPC methods available to the Jetpack Server. If the
14 * user is not authenticated (->login()) then the methods are never added,
15 * so they will get a "does not exist" error.
16 */
17 function xmlrpc_methods( $core_methods ) {
18 $jetpack_methods = array(
19 'jetpack.jsonAPI' => array( $this, 'json_api' ),
20 'jetpack.verifyAction' => array( $this, 'verify_action' ),
21 );
22
23 $user = $this->login();
24
25 if ( $user ) {
26 $jetpack_methods = array_merge( $jetpack_methods, array(
27 'jetpack.testConnection' => array( $this, 'test_connection' ),
28 'jetpack.testAPIUserCode' => array( $this, 'test_api_user_code' ),
29 'jetpack.featuresAvailable' => array( $this, 'features_available' ),
30 'jetpack.featuresEnabled' => array( $this, 'features_enabled' ),
31 'jetpack.disconnectBlog' => array( $this, 'disconnect_blog' ),
32 'jetpack.unlinkUser' => array( $this, 'unlink_user' ),
33 'jetpack.syncObject' => array( $this, 'sync_object' ),
34 'jetpack.idcUrlValidation' => array( $this, 'validate_urls_for_idc_mitigation' ),
35 ) );
36
37 if ( isset( $core_methods['metaWeblog.editPost'] ) ) {
38 $jetpack_methods['metaWeblog.newMediaObject'] = $core_methods['metaWeblog.newMediaObject'];
39 $jetpack_methods['jetpack.updateAttachmentParent'] = array( $this, 'update_attachment_parent' );
40 }
41
42 /**
43 * Filters the XML-RPC methods available to Jetpack for authenticated users.
44 *
45 * @since 1.1.0
46 *
47 * @param array $jetpack_methods XML-RPC methods available to the Jetpack Server.
48 * @param array $core_methods Available core XML-RPC methods.
49 * @param WP_User $user Information about a given WordPress user.
50 */
51 $jetpack_methods = apply_filters( 'jetpack_xmlrpc_methods', $jetpack_methods, $core_methods, $user );
52 }
53
54 /**
55 * Filters the XML-RPC methods available to Jetpack for unauthenticated users.
56 *
57 * @since 3.0.0
58 *
59 * @param array $jetpack_methods XML-RPC methods available to the Jetpack Server.
60 * @param array $core_methods Available core XML-RPC methods.
61 */
62 return apply_filters( 'jetpack_xmlrpc_unauthenticated_methods', $jetpack_methods, $core_methods );
63 }
64
65 /**
66 * Whitelist of the bootstrap XML-RPC methods
67 */
68 function bootstrap_xmlrpc_methods() {
69 return array(
70 'jetpack.verifyRegistration' => array( $this, 'verify_registration' ),
71 'jetpack.remoteAuthorize' => array( $this, 'remote_authorize' ),
72 );
73 }
74
75 function authorize_xmlrpc_methods() {
76 return array(
77 'jetpack.remoteAuthorize' => array( $this, 'remote_authorize' ),
78 'jetpack.activateManage' => array( $this, 'activate_manage' ),
79 );
80 }
81
82 function activate_manage( $request ) {
83 foreach( array( 'secret', 'state' ) as $required ) {
84 if ( ! isset( $request[ $required ] ) || empty( $request[ $required ] ) ) {
85 return $this->error( new Jetpack_Error( 'missing_parameter', 'One or more parameters is missing from the request.', 400 ) );
86 }
87 }
88 $verified = $this->verify_action( array( 'activate_manage', $request['secret'], $request['state'] ) );
89 if ( is_a( $verified, 'IXR_Error' ) ) {
90 return $verified;
91 }
92 $activated = Jetpack::activate_module( 'manage', false, false );
93 if ( false === $activated || ! Jetpack::is_module_active( 'manage' ) ) {
94 return $this->error( new Jetpack_Error( 'activation_error', 'There was an error while activating the module.', 500 ) );
95 }
96 return 'active';
97 }
98
99 function remote_authorize( $request ) {
100 foreach( array( 'secret', 'state', 'redirect_uri', 'code' ) as $required ) {
101 if ( ! isset( $request[ $required ] ) || empty( $request[ $required ] ) ) {
102 return $this->error( new Jetpack_Error( 'missing_parameter', 'One or more parameters is missing from the request.', 400 ) );
103 }
104 }
105
106 if ( ! get_user_by( 'id', $request['state'] ) ) {
107 return $this->error( new Jetpack_Error( 'user_unknown', 'User not found.', 404 ) );
108 }
109
110 if ( Jetpack::is_active() && Jetpack::is_user_connected( $request['state'] ) ) {
111 return $this->error( new Jetpack_Error( 'already_connected', 'User already connected.', 400 ) );
112 }
113
114 $verified = $this->verify_action( array( 'authorize', $request['secret'], $request['state'] ) );
115
116 if ( is_a( $verified, 'IXR_Error' ) ) {
117 return $verified;
118 }
119
120 wp_set_current_user( $request['state'] );
121
122 $client_server = new Jetpack_Client_Server;
123 $result = $client_server->authorize( $request );
124
125 if ( is_wp_error( $result ) ) {
126 return $this->error( $result );
127 }
128 // Creates a new secret, allowing someone to activate the manage module for up to 1 day after authorization.
129 $secrets = Jetpack::init()->generate_secrets( 'activate_manage', DAY_IN_SECONDS );
130 @list( $secret ) = explode( ':', $secrets );
131 $response = array(
132 'result' => $result,
133 'activate_manage' => $secret,
134 );
135 return $response;
136 }
137
138 /**
139 * Verifies that Jetpack.WordPress.com received a registration request from this site
140 */
141 function verify_registration( $data ) {
142 return $this->verify_action( array( 'register', $data[0], $data[1] ) );
143 }
144
145 /**
146 * @return WP_Error|string secret_2 on success, WP_Error( error_code => error_code, error_message => error description, error_data => status code ) on failure
147 *
148 * Possible error_codes:
149 *
150 * verify_secret_1_missing
151 * verify_secret_1_malformed
152 * verify_secrets_missing: verification secrets are not found in database
153 * verify_secrets_incomplete: verification secrets are only partially found in database
154 * verify_secrets_expired: verification secrets have expired
155 * verify_secrets_mismatch: stored secret_1 does not match secret_1 sent by Jetpack.WordPress.com
156 * state_missing: required parameter of state not found
157 * state_malformed: state is not a digit
158 * invalid_state: state in request does not match the stored state
159 *
160 * The 'authorize' and 'register' actions have additional error codes
161 *
162 * state_missing: a state ( user id ) was not supplied
163 * state_malformed: state is not the correct data type
164 * invalid_state: supplied state does not match the stored state
165 */
166 function verify_action( $params ) {
167 $action = $params[0];
168 $verify_secret = $params[1];
169 $state = isset( $params[2] ) ? $params[2] : '';
170
171 if ( empty( $verify_secret ) ) {
172 return $this->error( new Jetpack_Error( 'verify_secret_1_missing', sprintf( 'The required "%s" parameter is missing.', 'secret_1' ), 400 ) );
173 } else if ( ! is_string( $verify_secret ) ) {
174 return $this->error( new Jetpack_Error( 'verify_secret_1_malformed', sprintf( 'The required "%s" parameter is malformed.', 'secret_1' ), 400 ) );
175 }
176
177 $secrets = Jetpack_Options::get_option( $action );
178 if ( ! $secrets || is_wp_error( $secrets ) ) {
179 Jetpack_Options::delete_option( $action );
180 return $this->error( new Jetpack_Error( 'verify_secrets_missing', 'Verification secrets not found', 400 ) );
181 }
182
183 @list( $secret_1, $secret_2, $secret_eol, $user_id ) = explode( ':', $secrets );
184
185 if ( empty( $secret_1 ) || empty( $secret_2 ) || empty( $secret_eol ) ) {
186 Jetpack_Options::delete_option( $action );
187 return $this->error( new Jetpack_Error( 'verify_secrets_incomplete', 'Verification secrets are incomplete', 400 ) );
188 }
189
190 if ( $secret_eol < time() ) {
191 Jetpack_Options::delete_option( $action );
192 return $this->error( new Jetpack_Error( 'verify_secrets_expired', 'Verification took too long', 400 ) );
193 }
194
195 if ( ! hash_equals( $verify_secret, $secret_1 ) ) {
196 Jetpack_Options::delete_option( $action );
197 return $this->error( new Jetpack_Error( 'verify_secrets_mismatch', 'Secret mismatch', 400 ) );
198 }
199
200 if ( in_array( $action, array( 'authorize', 'register' ) ) ) {
201 // 'authorize' and 'register' actions require further testing
202 if ( empty( $state ) ) {
203 return $this->error( new Jetpack_Error( 'state_missing', sprintf( 'The required "%s" parameter is missing.', 'state' ), 400 ) );
204 } else if ( ! ctype_digit( $state ) ) {
205 return $this->error( new Jetpack_Error( 'state_malformed', sprintf( 'The required "%s" parameter is malformed.', 'state' ), 400 ) );
206 }
207 if ( empty( $user_id ) || $user_id !== $state ) {
208 Jetpack_Options::delete_option( $action );
209 return $this->error( new Jetpack_Error( 'invalid_state', 'State is invalid', 400 ) );
210 }
211 }
212
213 Jetpack_Options::delete_option( $action );
214
215 return $secret_2;
216 }
217
218 /**
219 * Wrapper for wp_authenticate( $username, $password );
220 *
221 * @return WP_User|bool
222 */
223 function login() {
224 Jetpack::init()->require_jetpack_authentication();
225 $user = wp_authenticate( 'username', 'password' );
226 if ( is_wp_error( $user ) ) {
227 if ( 'authentication_failed' == $user->get_error_code() ) { // Generic error could mean most anything.
228 $this->error = new Jetpack_Error( 'invalid_request', 'Invalid Request', 403 );
229 } else {
230 $this->error = $user;
231 }
232 return false;
233 } else if ( !$user ) { // Shouldn't happen.
234 $this->error = new Jetpack_Error( 'invalid_request', 'Invalid Request', 403 );
235 return false;
236 }
237
238 return $user;
239 }
240
241 /**
242 * Returns the current error as an IXR_Error
243 *
244 * @return bool|IXR_Error
245 */
246 function error( $error = null ) {
247 if ( !is_null( $error ) ) {
248 $this->error = $error;
249 }
250
251 if ( is_wp_error( $this->error ) ) {
252 $code = $this->error->get_error_data();
253 if ( !$code ) {
254 $code = -10520;
255 }
256 $message = sprintf( 'Jetpack: [%s] %s', $this->error->get_error_code(), $this->error->get_error_message() );
257 return new IXR_Error( $code, $message );
258 } else if ( is_a( $this->error, 'IXR_Error' ) ) {
259 return $this->error;
260 }
261
262 return false;
263 }
264
265 /* API Methods */
266
267 /**
268 * Just authenticates with the given Jetpack credentials.
269 *
270 * @return string The current Jetpack version number
271 */
272 function test_connection() {
273 return JETPACK__VERSION;
274 }
275
276 function test_api_user_code( $args ) {
277 $client_id = (int) $args[0];
278 $user_id = (int) $args[1];
279 $nonce = (string) $args[2];
280 $verify = (string) $args[3];
281
282 if ( !$client_id || !$user_id || !strlen( $nonce ) || 32 !== strlen( $verify ) ) {
283 return false;
284 }
285
286 $user = get_user_by( 'id', $user_id );
287 if ( !$user || is_wp_error( $user ) ) {
288 return false;
289 }
290
291 /* debugging
292 error_log( "CLIENT: $client_id" );
293 error_log( "USER: $user_id" );
294 error_log( "NONCE: $nonce" );
295 error_log( "VERIFY: $verify" );
296 */
297
298 $jetpack_token = Jetpack_Data::get_access_token( JETPACK_MASTER_USER );
299
300 $api_user_code = get_user_meta( $user_id, "jetpack_json_api_$client_id", true );
301 if ( !$api_user_code ) {
302 return false;
303 }
304
305 $hmac = hash_hmac( 'md5', json_encode( (object) array(
306 'client_id' => (int) $client_id,
307 'user_id' => (int) $user_id,
308 'nonce' => (string) $nonce,
309 'code' => (string) $api_user_code,
310 ) ), $jetpack_token->secret );
311
312 if ( ! hash_equals( $hmac, $verify ) ) {
313 return false;
314 }
315
316 return $user_id;
317 }
318
319 /**
320 * Disconnect this blog from the connected wordpress.com account
321 * @return boolean
322 */
323 function disconnect_blog() {
324 Jetpack::log( 'disconnect' );
325 Jetpack::disconnect();
326
327 return true;
328 }
329
330 /**
331 * Unlink a user from WordPress.com
332 *
333 * This will fail if called by the Master User.
334 */
335 function unlink_user() {
336 Jetpack::log( 'unlink' );
337 return Jetpack::unlink_user();
338 }
339
340 /**
341 * Returns any object that is able to be synced
342 */
343 function sync_object( $args ) {
344 // e.g. posts, post, 5
345 list( $module_name, $object_type, $id ) = $args;
346 require_once dirname( __FILE__ ) . '/sync/class.jetpack-sync-modules.php';
347 require_once dirname( __FILE__ ) . '/sync/class.jetpack-sync-sender.php';
348
349 $sync_module = Jetpack_Sync_Modules::get_module( $module_name );
350 $codec = Jetpack_Sync_Sender::get_instance()->get_codec();
351
352 return $codec->encode( $sync_module->get_object_by_id( $object_type, $id ) );
353 }
354
355 /**
356 * Returns the home URL and site URL for the current site which can be used on the WPCOM side for
357 * IDC mitigation to decide whether sync should be allowed if the home and siteurl values differ between WPCOM
358 * and the remote Jetpack site.
359 *
360 * @return array
361 */
362 function validate_urls_for_idc_mitigation() {
363 return array(
364 'home' => get_home_url(),
365 'siteurl' => get_site_url(),
366 );
367 }
368
369 /**
370 * Returns what features are available. Uses the slug of the module files.
371 *
372 * @return array
373 */
374 function features_available() {
375 $raw_modules = Jetpack::get_available_modules();
376 $modules = array();
377 foreach ( $raw_modules as $module ) {
378 $modules[] = Jetpack::get_module_slug( $module );
379 }
380
381 return $modules;
382 }
383
384 /**
385 * Returns what features are enabled. Uses the slug of the modules files.
386 *
387 * @return array
388 */
389 function features_enabled() {
390 $raw_modules = Jetpack::get_active_modules();
391 $modules = array();
392 foreach ( $raw_modules as $module ) {
393 $modules[] = Jetpack::get_module_slug( $module );
394 }
395
396 return $modules;
397 }
398
399 function update_attachment_parent( $args ) {
400 $attachment_id = (int) $args[0];
401 $parent_id = (int) $args[1];
402
403 return wp_update_post( array(
404 'ID' => $attachment_id,
405 'post_parent' => $parent_id,
406 ) );
407 }
408
409 function json_api( $args = array() ) {
410 $json_api_args = $args[0];
411 $verify_api_user_args = $args[1];
412
413 $method = (string) $json_api_args[0];
414 $url = (string) $json_api_args[1];
415 $post_body = is_null( $json_api_args[2] ) ? null : (string) $json_api_args[2];
416 $user_details = (array) $json_api_args[4];
417 $locale = (string) $json_api_args[5];
418
419 if ( !$verify_api_user_args ) {
420 $user_id = 0;
421 } elseif ( 'internal' === $verify_api_user_args[0] ) {
422 $user_id = (int) $verify_api_user_args[1];
423 if ( $user_id ) {
424 $user = get_user_by( 'id', $user_id );
425 if ( !$user || is_wp_error( $user ) ) {
426 return false;
427 }
428 }
429 } else {
430 $user_id = call_user_func( array( $this, 'test_api_user_code' ), $verify_api_user_args );
431 if ( !$user_id ) {
432 return false;
433 }
434 }
435
436 /* debugging
437 error_log( "-- begin json api via jetpack debugging -- " );
438 error_log( "METHOD: $method" );
439 error_log( "URL: $url" );
440 error_log( "POST BODY: $post_body" );
441 error_log( "VERIFY_ARGS: " . print_r( $verify_api_user_args, 1 ) );
442 error_log( "VERIFIED USER_ID: " . (int) $user_id );
443 error_log( "-- end json api via jetpack debugging -- " );
444 */
445
446 if ( 'en' !== $locale ) {
447 // .org mo files are named slightly different from .com, and all we have is this the locale -- try to guess them.
448 $new_locale = $locale;
449 if ( strpos( $locale, '-' ) !== false ) {
450 $locale_pieces = explode( '-', $locale );
451 $new_locale = $locale_pieces[0];
452 $new_locale .= ( ! empty( $locale_pieces[1] ) ) ? '_' . strtoupper( $locale_pieces[1] ) : '';
453 } else {
454 // .com might pass 'fr' because thats what our language files are named as, where core seems
455 // to do fr_FR - so try that if we don't think we can load the file.
456 if ( ! file_exists( WP_LANG_DIR . '/' . $locale . '.mo' ) ) {
457 $new_locale = $locale . '_' . strtoupper( $locale );
458 }
459 }
460
461 if ( file_exists( WP_LANG_DIR . '/' . $new_locale . '.mo' ) ) {
462 unload_textdomain( 'default' );
463 load_textdomain( 'default', WP_LANG_DIR . '/' . $new_locale . '.mo' );
464 }
465 }
466
467 $old_user = wp_get_current_user();
468 wp_set_current_user( $user_id );
469
470 $token = Jetpack_Data::get_access_token( get_current_user_id() );
471 if ( !$token || is_wp_error( $token ) ) {
472 return false;
473 }
474
475 define( 'REST_API_REQUEST', true );
476 define( 'WPCOM_JSON_API__BASE', 'public-api.wordpress.com/rest/v1' );
477
478 // needed?
479 require_once ABSPATH . 'wp-admin/includes/admin.php';
480
481 require_once JETPACK__PLUGIN_DIR . 'class.json-api.php';
482 $api = WPCOM_JSON_API::init( $method, $url, $post_body );
483 $api->token_details['user'] = $user_details;
484 require_once JETPACK__PLUGIN_DIR . 'class.json-api-endpoints.php';
485
486 $display_errors = ini_set( 'display_errors', 0 );
487 ob_start();
488 $content_type = $api->serve( false );
489 $output = ob_get_clean();
490 ini_set( 'display_errors', $display_errors );
491
492 $nonce = wp_generate_password( 10, false );
493 $hmac = hash_hmac( 'md5', $nonce . $output, $token->secret );
494
495 wp_set_current_user( isset( $old_user->ID ) ? $old_user->ID : 0 );
496
497 return array(
498 (string) $output,
499 (string) $nonce,
500 (string) $hmac,
501 );
502 }
503 }
504