Completed
Push — branch-4.2 ( d4d656...d606d4 )
by
unknown
203:58 queued 194:03
created

Jetpack_XMLRPC_Server::verify_registration()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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