Completed
Push — branch-4.9-built ( f63861...2af77d )
by
unknown
751:45 queued 732:48
created

Jetpack_XMLRPC_Server   C

Complexity

Total Complexity 76

Size/Duplication

Total Lines 488
Duplicated Lines 5.33 %

Coupling/Cohesion

Components 1
Dependencies 8

Importance

Changes 0
Metric Value
dl 26
loc 488
rs 5.488
c 0
b 0
f 0
wmc 76
lcom 1
cbo 8

18 Methods

Rating   Name   Duplication   Size   Complexity  
A xmlrpc_methods() 0 47 3
A bootstrap_xmlrpc_methods() 0 6 1
A authorize_xmlrpc_methods() 0 5 1
D remote_authorize() 0 35 9
A verify_registration() 0 3 1
C verify_action() 8 51 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 12 2
A unlink_user() 0 4 1
A sync_object() 0 11 1
A validate_urls_for_idc_mitigation() 0 6 1
A features_available() 9 9 2
A features_enabled() 9 9 2
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
	 * The current user
14
	 */
15
	public $user = null;
16
17
	/**
18
	 * Whitelist of the XML-RPC methods available to the Jetpack Server. If the
19
	 * user is not authenticated (->login()) then the methods are never added,
20
	 * so they will get a "does not exist" error.
21
	 */
22
	function xmlrpc_methods( $core_methods ) {
23
		$jetpack_methods = array(
24
			'jetpack.jsonAPI'      => array( $this, 'json_api' ),
25
			'jetpack.verifyAction' => array( $this, 'verify_action' ),
26
		);
27
28
		$this->user = $this->login();
29
30
		if ( $this->user ) {
31
			$jetpack_methods = array_merge( $jetpack_methods, array(
32
				'jetpack.testConnection'    => array( $this, 'test_connection' ),
33
				'jetpack.testAPIUserCode'   => array( $this, 'test_api_user_code' ),
34
				'jetpack.featuresAvailable' => array( $this, 'features_available' ),
35
				'jetpack.featuresEnabled'   => array( $this, 'features_enabled' ),
36
				'jetpack.disconnectBlog'    => array( $this, 'disconnect_blog' ),
37
				'jetpack.unlinkUser'        => array( $this, 'unlink_user' ),
38
				'jetpack.syncObject'        => array( $this, 'sync_object' ),
39
				'jetpack.idcUrlValidation'  => array( $this, 'validate_urls_for_idc_mitigation' ),
40
			) );
41
42
			if ( isset( $core_methods['metaWeblog.editPost'] ) ) {
43
				$jetpack_methods['metaWeblog.newMediaObject'] = $core_methods['metaWeblog.newMediaObject'];
44
				$jetpack_methods['jetpack.updateAttachmentParent'] = array( $this, 'update_attachment_parent' );
45
			}
46
47
			/**
48
			 * Filters the XML-RPC methods available to Jetpack for authenticated users.
49
			 *
50
			 * @since 1.1.0
51
			 *
52
			 * @param array $jetpack_methods XML-RPC methods available to the Jetpack Server.
53
			 * @param array $core_methods Available core XML-RPC methods.
54
			 * @param WP_User $user Information about a given WordPress user.
55
			 */
56
			$jetpack_methods = apply_filters( 'jetpack_xmlrpc_methods', $jetpack_methods, $core_methods, $this->user );
57
		}
58
59
		/**
60
		 * Filters the XML-RPC methods available to Jetpack for unauthenticated users.
61
		 *
62
		 * @since 3.0.0
63
		 *
64
		 * @param array $jetpack_methods XML-RPC methods available to the Jetpack Server.
65
		 * @param array $core_methods Available core XML-RPC methods.
66
		 */
67
		return apply_filters( 'jetpack_xmlrpc_unauthenticated_methods', $jetpack_methods, $core_methods );
68
	}
69
70
	/**
71
	 * Whitelist of the bootstrap XML-RPC methods
72
	 */
73
	function bootstrap_xmlrpc_methods() {
74
		return array(
75
			'jetpack.verifyRegistration' => array( $this, 'verify_registration' ),
76
			'jetpack.remoteAuthorize' => array( $this, 'remote_authorize' ),
77
		);
78
	}
79
80
	function authorize_xmlrpc_methods() {
81
		return array(
82
			'jetpack.remoteAuthorize' => array( $this, 'remote_authorize' ),
83
		);
84
	}
85
86
	function remote_authorize( $request ) {
87
		foreach( array( 'secret', 'state', 'redirect_uri', 'code' ) as $required ) {
88
			if ( ! isset( $request[ $required ] ) || empty( $request[ $required ] ) ) {
89
				return $this->error( new Jetpack_Error( 'missing_parameter', 'One or more parameters is missing from the request.', 400 ) );
90
			}
91
		}
92
93
		if ( ! get_user_by( 'id', $request['state'] ) ) {
94
			return $this->error( new Jetpack_Error( 'user_unknown', 'User not found.', 404 ) );
95
		}
96
97
		if ( Jetpack::is_active() && Jetpack::is_user_connected( $request['state'] ) ) {
98
			return $this->error( new Jetpack_Error( 'already_connected', 'User already connected.', 400 ) );
99
		}
100
101
		$verified = $this->verify_action( array( 'authorize', $request['secret'], $request['state'] ) );
102
103
		if ( is_a( $verified, 'IXR_Error' ) ) {
104
			return $verified;
105
		}
106
107
		wp_set_current_user( $request['state'] );
108
109
		$client_server = new Jetpack_Client_Server;
110
		$result = $client_server->authorize( $request );
111
112
		if ( is_wp_error( $result ) ) {
113
			return $this->error( $result );
114
		}
115
116
		$response = array(
117
			'result' => $result,
118
		);
119
		return $response;
120
	}
121
122
	/**
123
	* Verifies that Jetpack.WordPress.com received a registration request from this site
124
	*/
125
	function verify_registration( $data ) {
126
		return $this->verify_action( array( 'register', $data[0], $data[1] ) );
127
	}
128
129
	/**
130
	 * @return WP_Error|string secret_2 on success, WP_Error( error_code => error_code, error_message => error description, error_data => status code ) on failure
131
	 *
132
	 * Possible error_codes:
133
	 *
134
	 * verify_secret_1_missing
135
	 * verify_secret_1_malformed
136
	 * verify_secrets_missing: verification secrets are not found in database
137
	 * verify_secrets_incomplete: verification secrets are only partially found in database
138
	 * verify_secrets_expired: verification secrets have expired
139
	 * verify_secrets_mismatch: stored secret_1 does not match secret_1 sent by Jetpack.WordPress.com
140
	 * state_missing: required parameter of state not found
141
	 * state_malformed: state is not a digit
142
	 * invalid_state: state in request does not match the stored state
143
	 *
144
	 * The 'authorize' and 'register' actions have additional error codes
145
	 *
146
	 * state_missing: a state ( user id ) was not supplied
147
	 * state_malformed: state is not the correct data type
148
	 * invalid_state: supplied state does not match the stored state
149
	 */
150
	function verify_action( $params ) {
151
		$action = $params[0];
152
		$verify_secret = $params[1];
153
		$state = isset( $params[2] ) ? $params[2] : '';
154
155
		if ( empty( $verify_secret ) ) {
156
			return $this->error( new Jetpack_Error( 'verify_secret_1_missing', sprintf( 'The required "%s" parameter is missing.', 'secret_1' ), 400 ) );
157
		} else if ( ! is_string( $verify_secret ) ) {
158
			return $this->error( new Jetpack_Error( 'verify_secret_1_malformed', sprintf( 'The required "%s" parameter is malformed.', 'secret_1' ), 400 ) );
159
		}
160
161
		$secrets = Jetpack_Options::get_option( $action );
162
		if ( ! $secrets || is_wp_error( $secrets ) ) {
163
			Jetpack_Options::delete_option( $action );
164
			return $this->error( new Jetpack_Error( 'verify_secrets_missing', 'Verification secrets not found', 400 ) );
165
		}
166
167
		@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...
168
169 View Code Duplication
		if ( empty( $secret_1 ) || empty( $secret_2 ) || empty( $secret_eol ) ) {
170
			Jetpack_Options::delete_option( $action );
171
			return $this->error( new Jetpack_Error( 'verify_secrets_incomplete', 'Verification secrets are incomplete', 400 ) );
172
		}
173
174
		if ( $secret_eol < time() ) {
175
			Jetpack_Options::delete_option( $action );
176
			return $this->error( new Jetpack_Error( 'verify_secrets_expired', 'Verification took too long', 400 ) );
177
		}
178
179
		if ( ! hash_equals( $verify_secret, $secret_1 ) ) {
180
			Jetpack_Options::delete_option( $action );
181
			return $this->error( new Jetpack_Error( 'verify_secrets_mismatch', 'Secret mismatch', 400 ) );
182
		}
183
184
		if ( in_array( $action, array( 'authorize', 'register' ) ) ) {
185
			// 'authorize' and 'register' actions require further testing
186
			if ( empty( $state ) ) {
187
				return $this->error( new Jetpack_Error( 'state_missing', sprintf( 'The required "%s" parameter is missing.', 'state' ), 400 ) );
188
			} else if ( ! ctype_digit( $state ) ) {
189
				return $this->error( new Jetpack_Error( 'state_malformed', sprintf( 'The required "%s" parameter is malformed.', 'state' ), 400 ) );
190
			}
191 View Code Duplication
			if ( empty( $user_id ) || $user_id !== $state ) {
192
				Jetpack_Options::delete_option( $action );
193
				return $this->error( new Jetpack_Error( 'invalid_state', 'State is invalid', 400 ) );
194
			}
195
		}
196
197
		Jetpack_Options::delete_option( $action );
198
199
		return $secret_2;
200
	}
201
202
	/**
203
	 * Wrapper for wp_authenticate( $username, $password );
204
	 *
205
	 * @return WP_User|bool
206
	 */
207
	function login() {
208
		Jetpack::init()->require_jetpack_authentication();
209
		$user = wp_authenticate( 'username', 'password' );
210
		if ( is_wp_error( $user ) ) {
211
			if ( 'authentication_failed' == $user->get_error_code() ) { // Generic error could mean most anything.
212
				$this->error = new Jetpack_Error( 'invalid_request', 'Invalid Request', 403 );
213
			} else {
214
				$this->error = $user;
215
			}
216
			return false;
217
		} else if ( !$user ) { // Shouldn't happen.
218
			$this->error = new Jetpack_Error( 'invalid_request', 'Invalid Request', 403 );
219
			return false;
220
		}
221
222
		return $user;
223
	}
224
225
	/**
226
	 * Returns the current error as an IXR_Error
227
	 *
228
	 * @return bool|IXR_Error
229
	 */
230
	function error( $error = null ) {
231
		if ( !is_null( $error ) ) {
232
			$this->error = $error;
233
		}
234
235
		if ( is_wp_error( $this->error ) ) {
236
			$code = $this->error->get_error_data();
237
			if ( !$code ) {
238
				$code = -10520;
239
			}
240
			$message = sprintf( 'Jetpack: [%s] %s', $this->error->get_error_code(), $this->error->get_error_message() );
241
			return new IXR_Error( $code, $message );
242
		} else if ( is_a( $this->error, 'IXR_Error' ) ) {
243
			return $this->error;
244
		}
245
246
		return false;
247
	}
248
249
/* API Methods */
250
251
	/**
252
	 * Just authenticates with the given Jetpack credentials.
253
	 *
254
	 * @return string The current Jetpack version number
255
	 */
256
	function test_connection() {
257
		return JETPACK__VERSION;
258
	}
259
260
	function test_api_user_code( $args ) {
261
		$client_id = (int) $args[0];
262
		$user_id   = (int) $args[1];
263
		$nonce     = (string) $args[2];
264
		$verify    = (string) $args[3];
265
266
		if ( !$client_id || !$user_id || !strlen( $nonce ) || 32 !== strlen( $verify ) ) {
267
			return false;
268
		}
269
270
		$user = get_user_by( 'id', $user_id );
271
		if ( !$user || is_wp_error( $user ) ) {
272
			return false;
273
		}
274
275
		/* debugging
276
		error_log( "CLIENT: $client_id" );
277
		error_log( "USER:   $user_id" );
278
		error_log( "NONCE:  $nonce" );
279
		error_log( "VERIFY: $verify" );
280
		*/
281
282
		$jetpack_token = Jetpack_Data::get_access_token( $user_id );
0 ignored issues
show
Documentation introduced by
$user_id is of type integer, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
283
284
		$api_user_code = get_user_meta( $user_id, "jetpack_json_api_$client_id", true );
285
		if ( !$api_user_code ) {
286
			return false;
287
		}
288
289
		$hmac = hash_hmac( 'md5', json_encode( (object) array(
290
			'client_id' => (int) $client_id,
291
			'user_id'   => (int) $user_id,
292
			'nonce'     => (string) $nonce,
293
			'code'      => (string) $api_user_code,
294
		) ), $jetpack_token->secret );
295
296
		if ( ! hash_equals( $hmac, $verify ) ) {
297
			return false;
298
		}
299
300
		return $user_id;
301
	}
302
303
	/**
304
	* Disconnect this blog from the connected wordpress.com account
305
	* @return boolean
306
	*/
307
	function disconnect_blog() {
308
309
		// For tracking
310
		if ( ! empty( $this->user->ID ) ) {
311
			wp_set_current_user( $this->user->ID );
312
		}
313
314
		Jetpack::log( 'disconnect' );
315
		Jetpack::disconnect();
316
317
		return true;
318
	}
319
320
	/**
321
	 * Unlink a user from WordPress.com
322
	 *
323
	 * This will fail if called by the Master User.
324
	 */
325
	function unlink_user() {
326
		Jetpack::log( 'unlink' );
327
		return Jetpack::unlink_user();
328
	}
329
330
	/**
331
	 * Returns any object that is able to be synced
332
	 */
333
	function sync_object( $args ) {
334
		// e.g. posts, post, 5
335
		list( $module_name, $object_type, $id ) = $args;
336
		require_once dirname( __FILE__ ) . '/sync/class.jetpack-sync-modules.php';
337
		require_once dirname( __FILE__ ) . '/sync/class.jetpack-sync-sender.php';
338
339
		$sync_module = Jetpack_Sync_Modules::get_module( $module_name );
340
		$codec = Jetpack_Sync_Sender::get_instance()->get_codec();
341
342
		return $codec->encode( $sync_module->get_object_by_id( $object_type, $id ) );
343
	}
344
345
	/**
346
	 * Returns the home URL and site URL for the current site which can be used on the WPCOM side for
347
	 * IDC mitigation to decide whether sync should be allowed if the home and siteurl values differ between WPCOM
348
	 * and the remote Jetpack site.
349
	 *
350
	 * @return array
351
	 */
352
	function validate_urls_for_idc_mitigation() {
353
		return array(
354
			'home'    => get_home_url(),
355
			'siteurl' => get_site_url(),
356
		);
357
	}
358
359
	/**
360
	 * Returns what features are available. Uses the slug of the module files.
361
	 *
362
	 * @return array
363
	 */
364 View Code Duplication
	function features_available() {
365
		$raw_modules = Jetpack::get_available_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
	/**
375
	 * Returns what features are enabled. Uses the slug of the modules files.
376
	 *
377
	 * @return array
378
	 */
379 View Code Duplication
	function features_enabled() {
380
		$raw_modules = Jetpack::get_active_modules();
381
		$modules = array();
382
		foreach ( $raw_modules as $module ) {
383
			$modules[] = Jetpack::get_module_slug( $module );
384
		}
385
386
		return $modules;
387
	}
388
389
	function update_attachment_parent( $args ) {
390
		$attachment_id = (int) $args[0];
391
		$parent_id     = (int) $args[1];
392
393
		return wp_update_post( array(
394
			'ID'          => $attachment_id,
395
			'post_parent' => $parent_id,
396
		) );
397
	}
398
399
	function json_api( $args = array() ) {
400
		$json_api_args = $args[0];
401
		$verify_api_user_args = $args[1];
402
403
		$method       = (string) $json_api_args[0];
404
		$url          = (string) $json_api_args[1];
405
		$post_body    = is_null( $json_api_args[2] ) ? null : (string) $json_api_args[2];
406
		$user_details = (array) $json_api_args[4];
407
		$locale       = (string) $json_api_args[5];
408
409
		if ( !$verify_api_user_args ) {
410
			$user_id = 0;
411
		} elseif ( 'internal' === $verify_api_user_args[0] ) {
412
			$user_id = (int) $verify_api_user_args[1];
413
			if ( $user_id ) {
414
				$user = get_user_by( 'id', $user_id );
415
				if ( !$user || is_wp_error( $user ) ) {
416
					return false;
417
				}
418
			}
419
		} else {
420
			$user_id = call_user_func( array( $this, 'test_api_user_code' ), $verify_api_user_args );
421
			if ( !$user_id ) {
422
				return false;
423
			}
424
		}
425
426
		/* debugging
427
		error_log( "-- begin json api via jetpack debugging -- " );
428
		error_log( "METHOD: $method" );
429
		error_log( "URL: $url" );
430
		error_log( "POST BODY: $post_body" );
431
		error_log( "VERIFY_ARGS: " . print_r( $verify_api_user_args, 1 ) );
432
		error_log( "VERIFIED USER_ID: " . (int) $user_id );
433
		error_log( "-- end json api via jetpack debugging -- " );
434
		*/
435
436
		if ( 'en' !== $locale ) {
437
			// .org mo files are named slightly different from .com, and all we have is this the locale -- try to guess them.
438
			$new_locale = $locale;
439
			if ( strpos( $locale, '-' ) !== false ) {
440
				$locale_pieces = explode( '-', $locale );
441
				$new_locale = $locale_pieces[0];
442
				$new_locale .= ( ! empty( $locale_pieces[1] ) ) ? '_' . strtoupper( $locale_pieces[1] ) : '';
443
			} else {
444
				// .com might pass 'fr' because thats what our language files are named as, where core seems
445
				// to do fr_FR - so try that if we don't think we can load the file.
446
				if ( ! file_exists( WP_LANG_DIR . '/' . $locale . '.mo' ) ) {
447
					$new_locale =  $locale . '_' . strtoupper( $locale );
448
				}
449
			}
450
451
			if ( file_exists( WP_LANG_DIR . '/' . $new_locale . '.mo' ) ) {
452
				unload_textdomain( 'default' );
453
				load_textdomain( 'default', WP_LANG_DIR . '/' . $new_locale . '.mo' );
454
			}
455
		}
456
457
		$old_user = wp_get_current_user();
458
		wp_set_current_user( $user_id );
459
460
		$token = Jetpack_Data::get_access_token( get_current_user_id() );
461
		if ( !$token || is_wp_error( $token ) ) {
462
			return false;
463
		}
464
465
		define( 'REST_API_REQUEST', true );
466
		define( 'WPCOM_JSON_API__BASE', 'public-api.wordpress.com/rest/v1' );
467
468
		// needed?
469
		require_once ABSPATH . 'wp-admin/includes/admin.php';
470
471
		require_once JETPACK__PLUGIN_DIR . 'class.json-api.php';
472
		$api = WPCOM_JSON_API::init( $method, $url, $post_body );
473
		$api->token_details['user'] = $user_details;
474
		require_once JETPACK__PLUGIN_DIR . 'class.json-api-endpoints.php';
475
476
		$display_errors = ini_set( 'display_errors', 0 );
477
		ob_start();
478
		$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...
479
		$output = ob_get_clean();
480
		ini_set( 'display_errors', $display_errors );
481
482
		$nonce = wp_generate_password( 10, false );
483
		$hmac  = hash_hmac( 'md5', $nonce . $output, $token->secret );
484
485
		wp_set_current_user( isset( $old_user->ID ) ? $old_user->ID : 0 );
486
487
		return array(
488
			(string) $output,
489
			(string) $nonce,
490
			(string) $hmac,
491
		);
492
	}
493
}
494