Completed
Push — fix/inline-docs-410 ( f96891...63b75c )
by
unknown
43:24 queued 33:40
created

Jetpack_XMLRPC_Server::disconnect_blog()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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