Completed
Push — fix/untranslated-module-names ( 4226a9...c5f2cb )
by
unknown
11:13
created

class.jetpack-xmlrpc-server.php (2 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
		);
74
	}
75
76
	/**
77
	* Verifies that Jetpack.WordPress.com received a registration request from this site
78
	*/
79
	function verify_registration( $verify_secret ) {
80
		return $this->verify_action( array( 'register', $verify_secret ) );
81
	}
82
83
	/**
84
	 * @return WP_Error|string secret_2 on success, WP_Error( error_code => error_code, error_message => error description, error_data => status code ) on failure
85
	 *
86
	 * Possible error_codes:
87
	 *
88
	 * verify_secret_1_missing
89
	 * verify_secret_1_malformed
90
	 * verify_secrets_missing: No longer have verification secrets stored
91
	 * verify_secrets_mismatch: stored secret_1 does not match secret_1 sent by Jetpack.WordPress.com
92
	 */
93
	function verify_action( $params ) {
94
		$action = $params[0];
95
		$verify_secret = $params[1];
96
97
		if ( empty( $verify_secret ) ) {
98
			return $this->error( new Jetpack_Error( 'verify_secret_1_missing', sprintf( 'The required "%s" parameter is missing.', 'secret_1' ), 400 ) );
99
		} else if ( !is_string( $verify_secret ) ) {
100
			return $this->error( new Jetpack_Error( 'verify_secret_1_malformed', sprintf( 'The required "%s" parameter is malformed.', 'secret_1' ), 400 ) );
101
		}
102
103
		$secrets = Jetpack_Options::get_option( $action );
104
		if ( !$secrets || is_wp_error( $secrets ) ) {
105
			Jetpack_Options::delete_option( $action );
106
			return $this->error( new Jetpack_Error( 'verify_secrets_missing', 'Verification took too long', 400 ) );
107
		}
108
109
		@list( $secret_1, $secret_2, $secret_eol ) = explode( ':', $secrets );
110
		if ( empty( $secret_1 ) || empty( $secret_2 ) || empty( $secret_eol ) || $secret_eol < time() ) {
111
			Jetpack_Options::delete_option( $action );
112
			return $this->error( new Jetpack_Error( 'verify_secrets_missing', 'Verification took too long', 400 ) );
113
		}
114
115
		if ( ! hash_equals( $verify_secret, $secret_1 ) ) {
116
			Jetpack_Options::delete_option( $action );
117
			return $this->error( new Jetpack_Error( 'verify_secrets_mismatch', 'Secret mismatch', 400 ) );
118
		}
119
120
		Jetpack_Options::delete_option( $action );
121
122
		return $secret_2;
123
	}
124
125
	/**
126
	 * Wrapper for wp_authenticate( $username, $password );
127
	 *
128
	 * @return WP_User|IXR_Error
129
	 */
130
	function login() {
131
		Jetpack::init()->require_jetpack_authentication();
132
		$user = wp_authenticate( 'username', 'password' );
133
		if ( is_wp_error( $user ) ) {
134
			if ( 'authentication_failed' == $user->get_error_code() ) { // Generic error could mean most anything.
135
				$this->error = new Jetpack_Error( 'invalid_request', 'Invalid Request', 403 );
136
			} else {
137
				$this->error = $user;
138
			}
139
			return false;
140
		} else if ( !$user ) { // Shouldn't happen.
141
			$this->error = new Jetpack_Error( 'invalid_request', 'Invalid Request', 403 );
142
			return false;
143
		}
144
145
		return $user;
146
	}
147
148
	/**
149
	 * Returns the current error as an IXR_Error
150
	 *
151
	 * @return null|IXR_Error
152
	 */
153
	function error( $error = null ) {
154
		if ( !is_null( $error ) ) {
155
			$this->error = $error;
156
		}
157
158
		if ( is_wp_error( $this->error ) ) {
159
			$code = $this->error->get_error_data();
160
			if ( !$code ) {
161
				$code = -10520;
162
			}
163
			$message = sprintf( 'Jetpack: [%s] %s', $this->error->get_error_code(), $this->error->get_error_message() );
164
			return new IXR_Error( $code, $message );
165
		} else if ( is_a( $this->error, 'IXR_Error' ) ) {
166
			return $this->error;
167
		}
168
169
		return false;
170
	}
171
172
/* API Methods */
173
174
	/**
175
	 * Just authenticates with the given Jetpack credentials.
176
	 *
177
	 * @return bool|IXR_Error
178
	 */
179
	function test_connection() {
180
		return JETPACK__VERSION;
181
	}
182
183
	function test_api_user_code( $args ) {
184
		$client_id = (int) $args[0];
185
		$user_id   = (int) $args[1];
186
		$nonce     = (string) $args[2];
187
		$verify    = (string) $args[3];
188
189
		if ( !$client_id || !$user_id || !strlen( $nonce ) || 32 !== strlen( $verify ) ) {
190
			return false;
191
		}
192
193
		$user = get_user_by( 'id', $user_id );
194
		if ( !$user || is_wp_error( $user ) ) {
195
			return false;
196
		}
197
198
		/* debugging
0 ignored issues
show
Unused Code Comprehensibility introduced by
48% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
199
		error_log( "CLIENT: $client_id" );
200
		error_log( "USER:   $user_id" );
201
		error_log( "NONCE:  $nonce" );
202
		error_log( "VERIFY: $verify" );
203
		*/
204
205
		$jetpack_token = Jetpack_Data::get_access_token( JETPACK_MASTER_USER );
206
207
		$api_user_code = get_user_meta( $user_id, "jetpack_json_api_$client_id", true );
208
		if ( !$api_user_code ) {
209
			return false;
210
		}
211
212
		$hmac = hash_hmac( 'md5', json_encode( (object) array(
213
			'client_id' => (int) $client_id,
214
			'user_id'   => (int) $user_id,
215
			'nonce'     => (string) $nonce,
216
			'code'      => (string) $api_user_code,
217
		) ), $jetpack_token->secret );
218
219
		if ( $hmac !== $verify ) {
220
			return false;
221
		}
222
223
		return $user_id;
224
	}
225
226
	/**
227
	* Disconnect this blog from the connected wordpress.com account
228
	* @return boolean
229
	*/
230
	function disconnect_blog() {
231
		Jetpack::log( 'disconnect' );
232
		Jetpack::disconnect();
233
234
		return true;
235
	}
236
237
	/**
238
	 * Unlink a user from WordPress.com
239
	 *
240
	 * This will fail if called by the Master User.
241
	 */
242
	function unlink_user() {
243
		Jetpack::log( 'unlink' );
244
		return Jetpack::unlink_user();
245
	}
246
247
	/**
248
	 * Returns what features are available. Uses the slug of the module files.
249
	 *
250
	 * @return array|IXR_Error
251
	 */
252 View Code Duplication
	function features_available() {
253
		$raw_modules = Jetpack::get_available_modules();
254
		$modules = array();
255
		foreach ( $raw_modules as $module ) {
256
			$modules[] = Jetpack::get_module_slug( $module );
257
		}
258
259
		return $modules;
260
	}
261
262
	/**
263
	 * Returns what features are enabled. Uses the slug of the modules files.
264
	 *
265
	 * @return array|IXR_Error
266
	 */
267 View Code Duplication
	function features_enabled() {
268
		$raw_modules = Jetpack::get_active_modules();
269
		$modules = array();
270
		foreach ( $raw_modules as $module ) {
271
			$modules[] = Jetpack::get_module_slug( $module );
272
		}
273
274
		return $modules;
275
	}
276
277
	function get_post( $id ) {
278
		if ( !$id = (int) $id ) {
279
			return false;
280
		}
281
282
		$jetpack = Jetpack::init();
283
284
		$post = $jetpack->sync->get_post( $id );
285
		return $post;
286
	}
287
288 View Code Duplication
	function get_posts( $args ) {
289
		list( $post_ids ) = $args;
290
		$post_ids = array_map( 'intval', (array) $post_ids );
291
		$jp = Jetpack::init();
292
		$sync_data = $jp->sync->get_content( array( 'posts' => $post_ids ) );
293
294
		return $sync_data;
295
	}
296
297
	function get_comment( $id ) {
298
		if ( !$id = (int) $id ) {
299
			return false;
300
		}
301
302
		$jetpack = Jetpack::init();
303
304
		$comment = $jetpack->sync->get_comment( $id );
305
		if ( !is_array( $comment ) )
306
			return false;
307
308
		$post = $jetpack->sync->get_post( $comment['comment_post_ID'] );
309
		if ( !$post ) {
310
			return false;
311
		}
312
313
		return $comment;
314
	}
315
316 View Code Duplication
	function get_comments( $args ) {
317
		list( $comment_ids ) = $args;
318
		$comment_ids = array_map( 'intval', (array) $comment_ids );
319
		$jp = Jetpack::init();
320
		$sync_data = $jp->sync->get_content( array( 'comments' => $comment_ids ) );
321
322
		return $sync_data;
323
	}
324
325
	function update_attachment_parent( $args ) {
326
		$attachment_id = (int) $args[0];
327
		$parent_id     = (int) $args[1];
328
329
		return wp_update_post( array(
330
			'ID'          => $attachment_id,
331
			'post_parent' => $parent_id,
332
		) );
333
	}
334
335
	function json_api( $args = array() ) {
336
		$json_api_args = $args[0];
337
		$verify_api_user_args = $args[1];
338
339
		$method       = (string) $json_api_args[0];
340
		$url          = (string) $json_api_args[1];
341
		$post_body    = is_null( $json_api_args[2] ) ? null : (string) $json_api_args[2];
342
		$user_details = (array) $json_api_args[4];
343
		$locale       = (string) $json_api_args[5];
344
345
		if ( !$verify_api_user_args ) {
346
			$user_id = 0;
347
		} elseif ( 'internal' === $verify_api_user_args[0] ) {
348
			$user_id = (int) $verify_api_user_args[1];
349
			if ( $user_id ) {
350
				$user = get_user_by( 'id', $user_id );
351
				if ( !$user || is_wp_error( $user ) ) {
352
					return false;
353
				}
354
			}
355
		} else {
356
			$user_id = call_user_func( array( $this, 'test_api_user_code' ), $verify_api_user_args );
357
			if ( !$user_id ) {
358
				return false;
359
			}
360
		}
361
362
		/* debugging
0 ignored issues
show
Unused Code Comprehensibility introduced by
47% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
363
		error_log( "-- begin json api via jetpack debugging -- " );
364
		error_log( "METHOD: $method" );
365
		error_log( "URL: $url" );
366
		error_log( "POST BODY: $post_body" );
367
		error_log( "VERIFY_ARGS: " . print_r( $verify_api_user_args, 1 ) );
368
		error_log( "VERIFIED USER_ID: " . (int) $user_id );
369
		error_log( "-- end json api via jetpack debugging -- " );
370
		*/
371
372
		if ( 'en' !== $locale ) {
373
			// .org mo files are named slightly different from .com, and all we have is this the locale -- try to guess them.
374
			$new_locale = $locale;
375
			if ( strpos( $locale, '-' ) !== false ) {
376
				$pieces = explode( '-', $locale );
377
				$new_locale = $locale_pieces[0];
378
				$new_locale .= ( ! empty( $locale_pieces[1] ) ) ? '_' . strtoupper( $locale_pieces[1] ) : '';
379
			} else {
380
				// .com might pass 'fr' because thats what our language files are named as, where core seems
381
				// to do fr_FR - so try that if we don't think we can load the file.
382
				if ( ! file_exists( WP_LANG_DIR . '/' . $locale . '.mo' ) ) {
383
					$new_locale =  $locale . '_' . strtoupper( $locale );
384
				}
385
			}
386
387
			if ( file_exists( WP_LANG_DIR . '/' . $new_locale . '.mo' ) ) {
388
				unload_textdomain( 'default' );
389
				load_textdomain( 'default', WP_LANG_DIR . '/' . $new_locale . '.mo' );
390
			}
391
		}
392
393
		$old_user = wp_get_current_user();
394
		wp_set_current_user( $user_id );
395
396
		$token = Jetpack_Data::get_access_token( get_current_user_id() );
397
		if ( !$token || is_wp_error( $token ) ) {
398
			return false;
399
		}
400
401
		define( 'REST_API_REQUEST', true );
402
		define( 'WPCOM_JSON_API__BASE', 'public-api.wordpress.com/rest/v1' );
403
404
		// needed?
405
		require_once ABSPATH . 'wp-admin/includes/admin.php';
406
407
		require_once JETPACK__PLUGIN_DIR . 'class.json-api.php';
408
		$api = WPCOM_JSON_API::init( $method, $url, $post_body );
409
		$api->token_details['user'] = $user_details;
410
		require_once JETPACK__PLUGIN_DIR . 'class.json-api-endpoints.php';
411
412
		$display_errors = ini_set( 'display_errors', 0 );
413
		ob_start();
414
		$content_type = $api->serve( false );
415
		$output = ob_get_clean();
416
		ini_set( 'display_errors', $display_errors );
417
418
		$nonce = wp_generate_password( 10, false );
419
		$hmac  = hash_hmac( 'md5', $nonce . $output, $token->secret );
420
421
		wp_set_current_user( isset( $old_user->ID ) ? $old_user->ID : 0 );
422
423
		return array(
424
			(string) $output,
425
			(string) $nonce,
426
			(string) $hmac,
427
		);
428
	}
429
}
430