Completed
Push — add/react-videopress-settings ( 1a2dcb...a2519f )
by
unknown
275:56 queued 263:52
created

Jetpack_XMLRPC_Server::error()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 18
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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