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