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