Completed
Push — add/sync-rest-2 ( dcf3c8...9df295 )
by
unknown
10:28
created

class.jetpack-xmlrpc-server.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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.getPost'           => array( $this, 'get_post' ),
32
				'jetpack.getPosts'          => array( $this, 'get_posts' ),
33
				'jetpack.getComment'        => array( $this, 'get_comment' ),
34
				'jetpack.getComments'       => array( $this, 'get_comments' ),
35
				'jetpack.disconnectBlog'    => array( $this, 'disconnect_blog' ),
36
				'jetpack.unlinkUser'        => array( $this, 'unlink_user' ),
37
			) );
38
39
			if ( isset( $core_methods['metaWeblog.editPost'] ) ) {
40
				$jetpack_methods['metaWeblog.newMediaObject'] = $core_methods['metaWeblog.newMediaObject'];
41
				$jetpack_methods['jetpack.updateAttachmentParent'] = array( $this, 'update_attachment_parent' );
42
			}
43
44
			/**
45
			 * Filters the XML-RPC methods available to Jetpack for authenticated users.
46
			 *
47
			 * @since 1.1.0
48
			 *
49
			 * @param array $jetpack_methods XML-RPC methods available to the Jetpack Server.
50
			 * @param array $core_methods Available core XML-RPC methods.
51
			 * @param WP_User $user Information about a given WordPress user.
52
			 */
53
			$jetpack_methods = apply_filters( 'jetpack_xmlrpc_methods', $jetpack_methods, $core_methods, $user );
54
		}
55
56
		/**
57
		 * Filters the XML-RPC methods available to Jetpack for unauthenticated users.
58
		 *
59
		 * @since 3.0.0
60
		 *
61
		 * @param array $jetpack_methods XML-RPC methods available to the Jetpack Server.
62
		 * @param array $core_methods Available core XML-RPC methods.
63
		 */
64
		return apply_filters( 'jetpack_xmlrpc_unauthenticated_methods', $jetpack_methods, $core_methods );
65
	}
66
67
	/**
68
	 * Whitelist of the bootstrap XML-RPC methods
69
	 */
70
	function bootstrap_xmlrpc_methods() {
71
		return array(
72
			'jetpack.verifyRegistration' => array( $this, 'verify_registration' ),
73
			'jetpack.remoteAuthorize' => array( $this, 'remote_authorize' ),
74
		);
75
	}
76
77
	function authorize_xmlrpc_methods() {
78
		return array( 'jetpack.remoteAuthorize' => array( $this, 'remote_authorize' ) );
79
	}
80
81
	function remote_authorize( $request ) {
82
		foreach( array( 'secret', 'state', 'redirect_uri', 'code' ) as $required ) {
83
			if ( ! isset( $request[ $required ] ) || empty( $request[ $required ] ) ) {
84
				return $this->error( new Jetpack_Error( 'missing_parameter', 'One or more parameters is missing from the request.', 400 ) );
85
			}
86
		}
87
88
		if ( ! get_user_by( 'id', $request['state'] ) ) {
89
			return $this->error( new Jetpack_Error( 'user_unknown', 'User not found.', 404 ) );
90
		}
91
92
		if ( Jetpack::is_active() && Jetpack::is_user_connected( $request['state'] ) ) {
93
			return $this->error( new Jetpack_Error( 'already_connected', 'User already connected.', 400 ) );
94
		}
95
96
		$verified = $this->verify_action( array( 'authorize', $request['secret'], $request['state'] ) );
97
98
		if ( is_a( $verified, 'IXR_Error' ) ) {
99
			return $verified;
100
		}
101
102
		wp_set_current_user( $request['state'] );
103
104
		$client_server = new Jetpack_Client_Server;
105
		$result = $client_server->authorize( $request );
106
107
		if ( is_wp_error( $result ) ) {
108
			return $this->error( $result );
109
		}
110
		
111
		return $result;
112
	}
113
114
	/**
115
	* Verifies that Jetpack.WordPress.com received a registration request from this site
116
	*/
117
	function verify_registration( $data ) {
118
		return $this->verify_action( array( 'register', $data[0], $data[1] ) );
119
	}
120
121
	/**
122
	 * @return WP_Error|string secret_2 on success, WP_Error( error_code => error_code, error_message => error description, error_data => status code ) on failure
123
	 *
124
	 * Possible error_codes:
125
	 *
126
	 * verify_secret_1_missing
127
	 * verify_secret_1_malformed
128
	 * verify_secrets_missing: No longer have verification secrets stored
129
	 * verify_secrets_mismatch: stored secret_1 does not match secret_1 sent by Jetpack.WordPress.com
130
	 *
131
	 * The 'authorize' and 'register' actions have additional error codes
132
	 *
133
	 * state_missing: a state ( user id ) was not supplied
134
	 * state_malformed: state is not the correct data type
135
	 * invalid_state: supplied state does not match the stored state
136
	 */
137
	function verify_action( $params ) {
138
		$action = $params[0];
139
		$verify_secret = $params[1];
140
		$state = isset( $params[2] ) ? $params[2] : '';
141
142
		if ( empty( $verify_secret ) ) {
143
			return $this->error( new Jetpack_Error( 'verify_secret_1_missing', sprintf( 'The required "%s" parameter is missing.', 'secret_1' ), 400 ) );
144
		} else if ( ! is_string( $verify_secret ) ) {
145
			return $this->error( new Jetpack_Error( 'verify_secret_1_malformed', sprintf( 'The required "%s" parameter is malformed.', 'secret_1' ), 400 ) );
146
		}
147
148
		$secrets = Jetpack_Options::get_option( $action );
149
		if ( !$secrets || is_wp_error( $secrets ) ) {
150
			Jetpack_Options::delete_option( $action );
151
			return $this->error( new Jetpack_Error( 'verify_secrets_missing', 'Verification took too long', 400 ) );
152
		}
153
154
		@list( $secret_1, $secret_2, $secret_eol, $user_id ) = explode( ':', $secrets );
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
155
156
		if ( empty( $secret_1 ) || empty( $secret_2 ) || empty( $secret_eol ) || $secret_eol < time() ) {
157
			Jetpack_Options::delete_option( $action );
158
			return $this->error( new Jetpack_Error( 'verify_secrets_missing', 'Verification took too long', 400 ) );
159
		}
160
161
		if ( ! hash_equals( $verify_secret, $secret_1 ) ) {
162
			Jetpack_Options::delete_option( $action );
163
			return $this->error( new Jetpack_Error( 'verify_secrets_mismatch', 'Secret mismatch', 400 ) );
164
		}
165
166
		if ( in_array( $action, array( 'authorize', 'register' ) ) ) {
167
			// 'authorize' and 'register' actions require further testing
168
			if ( empty( $state ) ) {
169
				return $this->error( new Jetpack_Error( 'state_missing', sprintf( 'The required "%s" parameter is missing.', 'state' ), 400 ) );
170
			} else if ( ! ctype_digit( $state ) ) {
171
				return $this->error( new Jetpack_Error( 'state_malformed', sprintf( 'The required "%s" parameter is malformed.', 'state' ), 400 ) );
172
			}
173
			if ( empty( $user_id ) || $user_id !== $state ) {
174
				Jetpack_Options::delete_option( $action );
175
				return $this->error( new Jetpack_Error( 'invalid_state', 'State is invalid', 400 ) );
176
			}
177
		}
178
179
		Jetpack_Options::delete_option( $action );
180
181
		return $secret_2;
182
	}
183
184
	/**
185
	 * Wrapper for wp_authenticate( $username, $password );
186
	 *
187
	 * @return WP_User|IXR_Error
188
	 */
189
	function login() {
190
		Jetpack::init()->require_jetpack_authentication();
191
		$user = wp_authenticate( 'username', 'password' );
192
		if ( is_wp_error( $user ) ) {
193
			if ( 'authentication_failed' == $user->get_error_code() ) { // Generic error could mean most anything.
194
				$this->error = new Jetpack_Error( 'invalid_request', 'Invalid Request', 403 );
195
			} else {
196
				$this->error = $user;
197
			}
198
			return false;
199
		} else if ( !$user ) { // Shouldn't happen.
200
			$this->error = new Jetpack_Error( 'invalid_request', 'Invalid Request', 403 );
201
			return false;
202
		}
203
204
		return $user;
205
	}
206
207
	/**
208
	 * Returns the current error as an IXR_Error
209
	 *
210
	 * @return null|IXR_Error
211
	 */
212
	function error( $error = null ) {
213
		if ( !is_null( $error ) ) {
214
			$this->error = $error;
215
		}
216
217
		if ( is_wp_error( $this->error ) ) {
218
			$code = $this->error->get_error_data();
219
			if ( !$code ) {
220
				$code = -10520;
221
			}
222
			$message = sprintf( 'Jetpack: [%s] %s', $this->error->get_error_code(), $this->error->get_error_message() );
223
			return new IXR_Error( $code, $message );
224
		} else if ( is_a( $this->error, 'IXR_Error' ) ) {
225
			return $this->error;
226
		}
227
228
		return false;
229
	}
230
231
/* API Methods */
232
233
	/**
234
	 * Just authenticates with the given Jetpack credentials.
235
	 *
236
	 * @return bool|IXR_Error
237
	 */
238
	function test_connection() {
239
		return JETPACK__VERSION;
240
	}
241
242
	function test_api_user_code( $args ) {
243
		$client_id = (int) $args[0];
244
		$user_id   = (int) $args[1];
245
		$nonce     = (string) $args[2];
246
		$verify    = (string) $args[3];
247
248
		if ( !$client_id || !$user_id || !strlen( $nonce ) || 32 !== strlen( $verify ) ) {
249
			return false;
250
		}
251
252
		$user = get_user_by( 'id', $user_id );
253
		if ( !$user || is_wp_error( $user ) ) {
254
			return false;
255
		}
256
257
		/* debugging
258
		error_log( "CLIENT: $client_id" );
259
		error_log( "USER:   $user_id" );
260
		error_log( "NONCE:  $nonce" );
261
		error_log( "VERIFY: $verify" );
262
		*/
263
264
		$jetpack_token = Jetpack_Data::get_access_token( JETPACK_MASTER_USER );
265
266
		$api_user_code = get_user_meta( $user_id, "jetpack_json_api_$client_id", true );
267
		if ( !$api_user_code ) {
268
			return false;
269
		}
270
271
		$hmac = hash_hmac( 'md5', json_encode( (object) array(
272
			'client_id' => (int) $client_id,
273
			'user_id'   => (int) $user_id,
274
			'nonce'     => (string) $nonce,
275
			'code'      => (string) $api_user_code,
276
		) ), $jetpack_token->secret );
277
278
		if ( $hmac !== $verify ) {
279
			return false;
280
		}
281
282
		return $user_id;
283
	}
284
285
	/**
286
	* Disconnect this blog from the connected wordpress.com account
287
	* @return boolean
288
	*/
289
	function disconnect_blog() {
290
		Jetpack::log( 'disconnect' );
291
		Jetpack::disconnect();
292
293
		return true;
294
	}
295
296
	/**
297
	 * Unlink a user from WordPress.com
298
	 *
299
	 * This will fail if called by the Master User.
300
	 */
301
	function unlink_user() {
302
		Jetpack::log( 'unlink' );
303
		return Jetpack::unlink_user();
304
	}
305
306
	/**
307
	 * Returns what features are available. Uses the slug of the module files.
308
	 *
309
	 * @return array|IXR_Error
310
	 */
311 View Code Duplication
	function features_available() {
312
		$raw_modules = Jetpack::get_available_modules();
313
		$modules = array();
314
		foreach ( $raw_modules as $module ) {
315
			$modules[] = Jetpack::get_module_slug( $module );
316
		}
317
318
		return $modules;
319
	}
320
321
	/**
322
	 * Returns what features are enabled. Uses the slug of the modules files.
323
	 *
324
	 * @return array|IXR_Error
325
	 */
326 View Code Duplication
	function features_enabled() {
327
		$raw_modules = Jetpack::get_active_modules();
328
		$modules = array();
329
		foreach ( $raw_modules as $module ) {
330
			$modules[] = Jetpack::get_module_slug( $module );
331
		}
332
333
		return $modules;
334
	}
335
336
	function get_post( $id ) {
337
		if ( !$id = (int) $id ) {
338
			return false;
339
		}
340
341
		$post = Jetpack_Sync_Posts::get_post( $id );
342
		return $post;
343
	}
344
345
	function get_posts( $args ) {
346
		list( $post_ids ) = $args;
347
		$post_ids = array_map( 'intval', (array) $post_ids );
348
		$sync_data = array( 'posts' => array_map( array( 'Jetpack_Sync_Posts', 'get_post' ), $post_ids ) );
349
		return $sync_data;
350
	}
351
352
	function get_comment( $id ) {
353
		if ( !$id = (int) $id ) {
354
			return false;
355
		}
356
357
		$comment = Jetpack_Sync_Comments::get_comment( $id );
358
		if ( !is_array( $comment ) )
359
			return false;
360
361
		$post = Jetpack_Sync_Posts::get_post( $comment['comment_post_ID'] );
362
		if ( !$post ) {
363
			return false;
364
		}
365
366
		return $comment;
367
	}
368
369
	function get_comments( $args ) {
370
		list( $comment_ids ) = $args;
371
		$comment_ids = array_map( 'intval', (array) $comment_ids );
372
		$sync_data = array( 'comments' => array_map( array( 'Jetpack_Sync_Comments' ), 'get_comment', $comment_ids ) );
373
		return $sync_data;
374
	}
375
376
	function update_attachment_parent( $args ) {
377
		$attachment_id = (int) $args[0];
378
		$parent_id     = (int) $args[1];
379
380
		return wp_update_post( array(
381
			'ID'          => $attachment_id,
382
			'post_parent' => $parent_id,
383
		) );
384
	}
385
386
	function json_api( $args = array() ) {
387
		$json_api_args = $args[0];
388
		$verify_api_user_args = $args[1];
389
390
		$method       = (string) $json_api_args[0];
391
		$url          = (string) $json_api_args[1];
392
		$post_body    = is_null( $json_api_args[2] ) ? null : (string) $json_api_args[2];
393
		$user_details = (array) $json_api_args[4];
394
		$locale       = (string) $json_api_args[5];
395
396
		if ( !$verify_api_user_args ) {
397
			$user_id = 0;
398
		} elseif ( 'internal' === $verify_api_user_args[0] ) {
399
			$user_id = (int) $verify_api_user_args[1];
400
			if ( $user_id ) {
401
				$user = get_user_by( 'id', $user_id );
402
				if ( !$user || is_wp_error( $user ) ) {
403
					return false;
404
				}
405
			}
406
		} else {
407
			$user_id = call_user_func( array( $this, 'test_api_user_code' ), $verify_api_user_args );
408
			if ( !$user_id ) {
409
				return false;
410
			}
411
		}
412
413
		/* debugging
414
		error_log( "-- begin json api via jetpack debugging -- " );
415
		error_log( "METHOD: $method" );
416
		error_log( "URL: $url" );
417
		error_log( "POST BODY: $post_body" );
418
		error_log( "VERIFY_ARGS: " . print_r( $verify_api_user_args, 1 ) );
419
		error_log( "VERIFIED USER_ID: " . (int) $user_id );
420
		error_log( "-- end json api via jetpack debugging -- " );
421
		*/
422
423
		if ( 'en' !== $locale ) {
424
			// .org mo files are named slightly different from .com, and all we have is this the locale -- try to guess them.
425
			$new_locale = $locale;
426
			if ( strpos( $locale, '-' ) !== false ) {
427
				$pieces = explode( '-', $locale );
428
				$new_locale = $locale_pieces[0];
429
				$new_locale .= ( ! empty( $locale_pieces[1] ) ) ? '_' . strtoupper( $locale_pieces[1] ) : '';
430
			} else {
431
				// .com might pass 'fr' because thats what our language files are named as, where core seems
432
				// to do fr_FR - so try that if we don't think we can load the file.
433
				if ( ! file_exists( WP_LANG_DIR . '/' . $locale . '.mo' ) ) {
434
					$new_locale =  $locale . '_' . strtoupper( $locale );
435
				}
436
			}
437
438
			if ( file_exists( WP_LANG_DIR . '/' . $new_locale . '.mo' ) ) {
439
				unload_textdomain( 'default' );
440
				load_textdomain( 'default', WP_LANG_DIR . '/' . $new_locale . '.mo' );
441
			}
442
		}
443
444
		$old_user = wp_get_current_user();
445
		wp_set_current_user( $user_id );
446
447
		$token = Jetpack_Data::get_access_token( get_current_user_id() );
448
		if ( !$token || is_wp_error( $token ) ) {
449
			return false;
450
		}
451
452
		define( 'REST_API_REQUEST', true );
453
		define( 'WPCOM_JSON_API__BASE', 'public-api.wordpress.com/rest/v1' );
454
455
		// needed?
456
		require_once ABSPATH . 'wp-admin/includes/admin.php';
457
458
		require_once JETPACK__PLUGIN_DIR . 'class.json-api.php';
459
		$api = WPCOM_JSON_API::init( $method, $url, $post_body );
460
		$api->token_details['user'] = $user_details;
461
		require_once JETPACK__PLUGIN_DIR . 'class.json-api-endpoints.php';
462
463
		$display_errors = ini_set( 'display_errors', 0 );
464
		ob_start();
465
		$content_type = $api->serve( false );
466
		$output = ob_get_clean();
467
		ini_set( 'display_errors', $display_errors );
468
469
		$nonce = wp_generate_password( 10, false );
470
		$hmac  = hash_hmac( 'md5', $nonce . $output, $token->secret );
471
472
		wp_set_current_user( isset( $old_user->ID ) ? $old_user->ID : 0 );
473
474
		return array(
475
			(string) $output,
476
			(string) $nonce,
477
			(string) $hmac,
478
		);
479
	}
480
}
481