Completed
Push — update/connect-url-authorize-s... ( 3a3ea9 )
by
unknown
10:56
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
		);
84
	}
85
86
	function remote_authorize( $request ) {
87
		foreach( array( 'secret', 'state', 'redirect_uri', 'code' ) as $required ) {
88
			if ( ! isset( $request[ $required ] ) || empty( $request[ $required ] ) ) {
89
				return $this->error( new Jetpack_Error( 'missing_parameter', 'One or more parameters is missing from the request.', 400 ) );
90
			}
91
		}
92
93
		if ( ! get_user_by( 'id', $request['state'] ) ) {
94
			return $this->error( new Jetpack_Error( 'user_unknown', 'User not found.', 404 ) );
95
		}
96
97
		if ( Jetpack::is_active() && Jetpack::is_user_connected( $request['state'] ) ) {
98
			return $this->error( new Jetpack_Error( 'already_connected', 'User already connected.', 400 ) );
99
		}
100
101
		$verified = $this->verify_action( array( 'authorize', $request['secret'], $request['state'] ) );
102
103
		if ( is_a( $verified, 'IXR_Error' ) ) {
104
			return $verified;
105
		}
106
107
		wp_set_current_user( $request['state'] );
108
109
		$client_server = new Jetpack_Client_Server;
110
		$result = $client_server->authorize( $request );
111
112
		if ( is_wp_error( $result ) ) {
113
			return $this->error( $result );
114
		}
115
116
		$response = array(
117
			'result' => $result,
118
		);
119
		return $response;
120
	}
121
122
	/**
123
	* Verifies that Jetpack.WordPress.com received a registration request from this site
124
	*/
125
	function verify_registration( $data ) {
126
		return $this->verify_action( array( 'register', $data[0], $data[1] ) );
127
	}
128
129
	/**
130
	 * @return WP_Error|string secret_2 on success, WP_Error( error_code => error_code, error_message => error description, error_data => status code ) on failure
131
	 *
132
	 * Possible error_codes:
133
	 *
134
	 * verify_secret_1_missing
135
	 * verify_secret_1_malformed
136
	 * verify_secrets_missing: verification secrets are not found in database
137
	 * verify_secrets_incomplete: verification secrets are only partially found in database
138
	 * verify_secrets_mismatch: stored secret_1 does not match secret_1 sent by Jetpack.WordPress.com
139
	 * state_missing: required parameter of state not found
140
	 * state_malformed: state is not a digit
141
	 * invalid_state: state in request does not match the stored state
142
	 *
143
	 * The 'authorize' and 'register' actions have additional error codes
144
	 *
145
	 * state_missing: a state ( user id ) was not supplied
146
	 * state_malformed: state is not the correct data type
147
	 * invalid_state: supplied state does not match the stored state
148
	 */
149
	function verify_action( $params ) {
150
		$action = $params[0];
151
		$verify_secret = $params[1];
152
		$state = isset( $params[2] ) ? $params[2] : '';
153
154
		if ( empty( $verify_secret ) ) {
155
			return $this->error( new Jetpack_Error( 'verify_secret_1_missing', sprintf( 'The required "%s" parameter is missing.', 'secret_1' ), 400 ) );
156
		} else if ( ! is_string( $verify_secret ) ) {
157
			return $this->error( new Jetpack_Error( 'verify_secret_1_malformed', sprintf( 'The required "%s" parameter is malformed.', 'secret_1' ), 400 ) );
158
		} else if ( empty( $state ) ) {
159
			return $this->error( new Jetpack_Error( 'state_missing', sprintf( 'The required "%s" parameter is missing.', 'state' ), 400 ) );
160
		} else if ( ! ctype_digit( $state ) ) {
161
			return $this->error( new Jetpack_Error( 'state_malformed', sprintf( 'The required "%s" parameter is malformed.', 'state' ), 400 ) );
162
		}
163
164
		$secret_name = 'jetpack_' . $action . '_' . $state;
165
		$secrets = get_transient( $secret_name );
166
167
		if ( ! $secrets || is_wp_error( $secrets ) ) {
168
			delete_transient( $secret_name );
169
			return $this->error( new Jetpack_Error( 'verify_secrets_missing', 'Verification secrets not found', 400 ) );
170
		}
171
172
		@list( $secret_1, $secret_2 ) = 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...
173
174
		if ( empty( $secret_1 ) || empty( $secret_2 ) ) {
175
			delete_transient( $secret_name );
176
			return $this->error( new Jetpack_Error( 'verify_secrets_incomplete', 'Verification secrets are incomplete', 400 ) );
177
		}
178
179
		if ( ! hash_equals( $verify_secret, $secret_1 ) ) {
180
			delete_transient( $secret_name );
181
			return $this->error( new Jetpack_Error( 'verify_secrets_mismatch', 'Secret mismatch', 400 ) );
182
		}
183
184
		delete_transient( $secret_name );
185
186
		return $secret_2;
187
	}
188
189
	/**
190
	 * Wrapper for wp_authenticate( $username, $password );
191
	 *
192
	 * @return WP_User|bool
193
	 */
194
	function login() {
195
		Jetpack::init()->require_jetpack_authentication();
196
		$user = wp_authenticate( 'username', 'password' );
197
		if ( is_wp_error( $user ) ) {
198
			if ( 'authentication_failed' == $user->get_error_code() ) { // Generic error could mean most anything.
199
				$this->error = new Jetpack_Error( 'invalid_request', 'Invalid Request', 403 );
200
			} else {
201
				$this->error = $user;
202
			}
203
			return false;
204
		} else if ( !$user ) { // Shouldn't happen.
205
			$this->error = new Jetpack_Error( 'invalid_request', 'Invalid Request', 403 );
206
			return false;
207
		}
208
209
		return $user;
210
	}
211
212
	/**
213
	 * Returns the current error as an IXR_Error
214
	 *
215
	 * @return bool|IXR_Error
216
	 */
217
	function error( $error = null ) {
218
		if ( !is_null( $error ) ) {
219
			$this->error = $error;
220
		}
221
222
		if ( is_wp_error( $this->error ) ) {
223
			$code = $this->error->get_error_data();
224
			if ( !$code ) {
225
				$code = -10520;
226
			}
227
			$message = sprintf( 'Jetpack: [%s] %s', $this->error->get_error_code(), $this->error->get_error_message() );
228
			return new IXR_Error( $code, $message );
229
		} else if ( is_a( $this->error, 'IXR_Error' ) ) {
230
			return $this->error;
231
		}
232
233
		return false;
234
	}
235
236
/* API Methods */
237
238
	/**
239
	 * Just authenticates with the given Jetpack credentials.
240
	 *
241
	 * @return string The current Jetpack version number
242
	 */
243
	function test_connection() {
244
		return JETPACK__VERSION;
245
	}
246
247
	function test_api_user_code( $args ) {
248
		$client_id = (int) $args[0];
249
		$user_id   = (int) $args[1];
250
		$nonce     = (string) $args[2];
251
		$verify    = (string) $args[3];
252
253
		if ( !$client_id || !$user_id || !strlen( $nonce ) || 32 !== strlen( $verify ) ) {
254
			return false;
255
		}
256
257
		$user = get_user_by( 'id', $user_id );
258
		if ( !$user || is_wp_error( $user ) ) {
259
			return false;
260
		}
261
262
		/* debugging
263
		error_log( "CLIENT: $client_id" );
264
		error_log( "USER:   $user_id" );
265
		error_log( "NONCE:  $nonce" );
266
		error_log( "VERIFY: $verify" );
267
		*/
268
269
		$jetpack_token = Jetpack_Data::get_access_token( $user_id );
270
271
		$api_user_code = get_user_meta( $user_id, "jetpack_json_api_$client_id", true );
272
		if ( !$api_user_code ) {
273
			return false;
274
		}
275
276
		$hmac = hash_hmac( 'md5', json_encode( (object) array(
277
			'client_id' => (int) $client_id,
278
			'user_id'   => (int) $user_id,
279
			'nonce'     => (string) $nonce,
280
			'code'      => (string) $api_user_code,
281
		) ), $jetpack_token->secret );
282
283
		if ( ! hash_equals( $hmac, $verify ) ) {
284
			return false;
285
		}
286
287
		return $user_id;
288
	}
289
290
	/**
291
	* Disconnect this blog from the connected wordpress.com account
292
	* @return boolean
293
	*/
294
	function disconnect_blog() {
295
296
		// For tracking
297
		if ( ! empty( $this->user->ID ) ) {
298
			wp_set_current_user( $this->user->ID );
299
		}
300
301
		Jetpack::log( 'disconnect' );
302
		Jetpack::disconnect();
303
304
		return true;
305
	}
306
307
	/**
308
	 * Unlink a user from WordPress.com
309
	 *
310
	 * This will fail if called by the Master User.
311
	 */
312
	function unlink_user() {
313
		Jetpack::log( 'unlink' );
314
		return Jetpack::unlink_user();
315
	}
316
317
	/**
318
	 * Returns any object that is able to be synced
319
	 */
320
	function sync_object( $args ) {
321
		// e.g. posts, post, 5
322
		list( $module_name, $object_type, $id ) = $args;
323
		require_once dirname( __FILE__ ) . '/sync/class.jetpack-sync-modules.php';
324
		require_once dirname( __FILE__ ) . '/sync/class.jetpack-sync-sender.php';
325
326
		$sync_module = Jetpack_Sync_Modules::get_module( $module_name );
327
		$codec = Jetpack_Sync_Sender::get_instance()->get_codec();
328
329
		return $codec->encode( $sync_module->get_object_by_id( $object_type, $id ) );
330
	}
331
332
	/**
333
	 * Returns the home URL and site URL for the current site which can be used on the WPCOM side for
334
	 * IDC mitigation to decide whether sync should be allowed if the home and siteurl values differ between WPCOM
335
	 * and the remote Jetpack site.
336
	 *
337
	 * @return array
338
	 */
339
	function validate_urls_for_idc_mitigation() {
340
		return array(
341
			'home'    => get_home_url(),
342
			'siteurl' => get_site_url(),
343
		);
344
	}
345
346
	/**
347
	 * Returns what features are available. Uses the slug of the module files.
348
	 *
349
	 * @return array
350
	 */
351 View Code Duplication
	function features_available() {
352
		$raw_modules = Jetpack::get_available_modules();
353
		$modules = array();
354
		foreach ( $raw_modules as $module ) {
355
			$modules[] = Jetpack::get_module_slug( $module );
356
		}
357
358
		return $modules;
359
	}
360
361
	/**
362
	 * Returns what features are enabled. Uses the slug of the modules files.
363
	 *
364
	 * @return array
365
	 */
366 View Code Duplication
	function features_enabled() {
367
		$raw_modules = Jetpack::get_active_modules();
368
		$modules = array();
369
		foreach ( $raw_modules as $module ) {
370
			$modules[] = Jetpack::get_module_slug( $module );
371
		}
372
373
		return $modules;
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
				$locale_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