Completed
Push — update/json-api-undefined-piec... ( 5a3a27 )
by
unknown
27:43 queued 17:48
created

class.jetpack-xmlrpc-server.php (3 issues)

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 );
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
		$jetpack = Jetpack::init();
342
343
		$post = $jetpack->sync->get_post( $id );
344
		return $post;
345
	}
346
347 View Code Duplication
	function get_posts( $args ) {
348
		list( $post_ids ) = $args;
349
		$post_ids = array_map( 'intval', (array) $post_ids );
350
		$jp = Jetpack::init();
351
		$sync_data = $jp->sync->get_content( array( 'posts' => $post_ids ) );
352
353
		return $sync_data;
354
	}
355
356
	function get_comment( $id ) {
357
		if ( !$id = (int) $id ) {
358
			return false;
359
		}
360
361
		$jetpack = Jetpack::init();
362
363
		$comment = $jetpack->sync->get_comment( $id );
364
		if ( !is_array( $comment ) )
365
			return false;
366
367
		$post = $jetpack->sync->get_post( $comment['comment_post_ID'] );
368
		if ( !$post ) {
369
			return false;
370
		}
371
372
		return $comment;
373
	}
374
375 View Code Duplication
	function get_comments( $args ) {
376
		list( $comment_ids ) = $args;
377
		$comment_ids = array_map( 'intval', (array) $comment_ids );
378
		$jp = Jetpack::init();
379
		$sync_data = $jp->sync->get_content( array( 'comments' => $comment_ids ) );
380
381
		return $sync_data;
382
	}
383
384
	function update_attachment_parent( $args ) {
385
		$attachment_id = (int) $args[0];
386
		$parent_id     = (int) $args[1];
387
388
		return wp_update_post( array(
389
			'ID'          => $attachment_id,
390
			'post_parent' => $parent_id,
391
		) );
392
	}
393
394
	function json_api( $args = array() ) {
395
		$json_api_args = $args[0];
396
		$verify_api_user_args = $args[1];
397
398
		$method       = (string) $json_api_args[0];
399
		$url          = (string) $json_api_args[1];
400
		$post_body    = is_null( $json_api_args[2] ) ? null : (string) $json_api_args[2];
401
		$user_details = (array) $json_api_args[4];
402
		$locale       = (string) $json_api_args[5];
403
404
		if ( !$verify_api_user_args ) {
405
			$user_id = 0;
406
		} elseif ( 'internal' === $verify_api_user_args[0] ) {
407
			$user_id = (int) $verify_api_user_args[1];
408
			if ( $user_id ) {
409
				$user = get_user_by( 'id', $user_id );
410
				if ( !$user || is_wp_error( $user ) ) {
411
					return false;
412
				}
413
			}
414
		} else {
415
			$user_id = call_user_func( array( $this, 'test_api_user_code' ), $verify_api_user_args );
416
			if ( !$user_id ) {
417
				return false;
418
			}
419
		}
420
421
		/* debugging
422
		error_log( "-- begin json api via jetpack debugging -- " );
423
		error_log( "METHOD: $method" );
424
		error_log( "URL: $url" );
425
		error_log( "POST BODY: $post_body" );
426
		error_log( "VERIFY_ARGS: " . print_r( $verify_api_user_args, 1 ) );
427
		error_log( "VERIFIED USER_ID: " . (int) $user_id );
428
		error_log( "-- end json api via jetpack debugging -- " );
429
		*/
430
431
		if ( 'en' !== $locale ) {
432
			// .org mo files are named slightly different from .com, and all we have is this the locale -- try to guess them.
433
			$new_locale = $locale;
434
			if ( strpos( $locale, '-' ) !== false ) {
435
				$pieces = explode( '-', $locale );
0 ignored issues
show
$pieces is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
436
				$new_locale = $locale_pieces[0];
0 ignored issues
show
The variable $locale_pieces does not exist. Did you mean $locale?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
437
				$new_locale .= ( ! empty( $locale_pieces[1] ) ) ? '_' . strtoupper( $locale_pieces[1] ) : '';
0 ignored issues
show
The variable $locale_pieces does not exist. Did you mean $locale?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
438
			} else {
439
				// .com might pass 'fr' because thats what our language files are named as, where core seems
440
				// to do fr_FR - so try that if we don't think we can load the file.
441
				if ( ! file_exists( WP_LANG_DIR . '/' . $locale . '.mo' ) ) {
442
					$new_locale =  $locale . '_' . strtoupper( $locale );
443
				}
444
			}
445
446
			if ( file_exists( WP_LANG_DIR . '/' . $new_locale . '.mo' ) ) {
447
				unload_textdomain( 'default' );
448
				load_textdomain( 'default', WP_LANG_DIR . '/' . $new_locale . '.mo' );
449
			}
450
		}
451
452
		$old_user = wp_get_current_user();
453
		wp_set_current_user( $user_id );
454
455
		$token = Jetpack_Data::get_access_token( get_current_user_id() );
456
		if ( !$token || is_wp_error( $token ) ) {
457
			return false;
458
		}
459
460
		define( 'REST_API_REQUEST', true );
461
		define( 'WPCOM_JSON_API__BASE', 'public-api.wordpress.com/rest/v1' );
462
463
		// needed?
464
		require_once ABSPATH . 'wp-admin/includes/admin.php';
465
466
		require_once JETPACK__PLUGIN_DIR . 'class.json-api.php';
467
		$api = WPCOM_JSON_API::init( $method, $url, $post_body );
468
		$api->token_details['user'] = $user_details;
469
		require_once JETPACK__PLUGIN_DIR . 'class.json-api-endpoints.php';
470
471
		$display_errors = ini_set( 'display_errors', 0 );
472
		ob_start();
473
		$content_type = $api->serve( false );
474
		$output = ob_get_clean();
475
		ini_set( 'display_errors', $display_errors );
476
477
		$nonce = wp_generate_password( 10, false );
478
		$hmac  = hash_hmac( 'md5', $nonce . $output, $token->secret );
479
480
		wp_set_current_user( isset( $old_user->ID ) ? $old_user->ID : 0 );
481
482
		return array(
483
			(string) $output,
484
			(string) $nonce,
485
			(string) $hmac,
486
		);
487
	}
488
}
489