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