Completed
Push — fix/jetpack-verify-action ( 758baf )
by
unknown
10:41
created

Jetpack_XMLRPC_Server   D

Complexity

Total Complexity 81

Size/Duplication

Total Lines 483
Duplicated Lines 7.04 %

Coupling/Cohesion

Components 1
Dependencies 7

Importance

Changes 6
Bugs 0 Features 0
Metric Value
wmc 81
c 6
b 0
f 0
lcom 1
cbo 7
dl 34
loc 483
rs 4.8717

20 Methods

Rating   Name   Duplication   Size   Complexity  
A xmlrpc_methods() 0 49 3
A bootstrap_xmlrpc_methods() 0 6 1
A authorize_xmlrpc_methods() 0 3 1
D remote_authorize() 0 32 9
A verify_registration() 0 3 1
C verify_action() 0 46 16
A login() 0 17 4
B error() 0 18 5
A test_connection() 0 3 1
D test_api_user_code() 0 42 9
A disconnect_blog() 0 6 1
A unlink_user() 0 4 1
A features_available() 9 9 2
A features_enabled() 9 9 2
A get_post() 0 10 2
A get_posts() 8 8 1
A get_comment() 0 18 4
A get_comments() 8 8 1
A update_attachment_parent() 0 9 1
D json_api() 0 94 16

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Jetpack_XMLRPC_Server often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Jetpack_XMLRPC_Server, and based on these observations, apply Extract Interface, too.

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 );
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...
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
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...
436
				$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...
437
				$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...
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 );
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...
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