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 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 |
||
7 | class Jetpack_XMLRPC_Server { |
||
8 | /** |
||
9 | * The current error object |
||
10 | */ |
||
11 | public $error = null; |
||
12 | |||
13 | /** |
||
14 | * The current user |
||
15 | */ |
||
16 | public $user = null; |
||
17 | |||
18 | private $tracking; |
||
19 | |||
20 | function __construct() { |
||
21 | $this->tracking = new Tracking(); |
||
22 | } |
||
23 | |||
24 | /** |
||
25 | * Whitelist of the XML-RPC methods available to the Jetpack Server. If the |
||
26 | * user is not authenticated (->login()) then the methods are never added, |
||
27 | * so they will get a "does not exist" error. |
||
28 | */ |
||
29 | function xmlrpc_methods( $core_methods ) { |
||
30 | $jetpack_methods = array( |
||
31 | 'jetpack.jsonAPI' => array( $this, 'json_api' ), |
||
32 | 'jetpack.verifyAction' => array( $this, 'verify_action' ), |
||
33 | 'jetpack.remoteRegister' => array( $this, 'remote_register' ), |
||
34 | 'jetpack.remoteProvision' => array( $this, 'remote_provision' ), |
||
35 | ); |
||
36 | |||
37 | $this->user = $this->login(); |
||
38 | |||
39 | if ( $this->user ) { |
||
40 | $jetpack_methods = array_merge( $jetpack_methods, array( |
||
41 | 'jetpack.testConnection' => array( $this, 'test_connection' ), |
||
42 | 'jetpack.testAPIUserCode' => array( $this, 'test_api_user_code' ), |
||
43 | 'jetpack.featuresAvailable' => array( $this, 'features_available' ), |
||
44 | 'jetpack.featuresEnabled' => array( $this, 'features_enabled' ), |
||
45 | 'jetpack.disconnectBlog' => array( $this, 'disconnect_blog' ), |
||
46 | 'jetpack.unlinkUser' => array( $this, 'unlink_user' ), |
||
47 | 'jetpack.syncObject' => array( $this, 'sync_object' ), |
||
48 | 'jetpack.idcUrlValidation' => array( $this, 'validate_urls_for_idc_mitigation' ), |
||
49 | ) ); |
||
50 | |||
51 | if ( isset( $core_methods['metaWeblog.editPost'] ) ) { |
||
52 | $jetpack_methods['metaWeblog.newMediaObject'] = $core_methods['metaWeblog.newMediaObject']; |
||
53 | $jetpack_methods['jetpack.updateAttachmentParent'] = array( $this, 'update_attachment_parent' ); |
||
54 | } |
||
55 | |||
56 | /** |
||
57 | * Filters the XML-RPC methods available to Jetpack for authenticated users. |
||
58 | * |
||
59 | * @since 1.1.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 | * @param WP_User $user Information about a given WordPress user. |
||
64 | */ |
||
65 | $jetpack_methods = apply_filters( 'jetpack_xmlrpc_methods', $jetpack_methods, $core_methods, $this->user ); |
||
66 | } |
||
67 | |||
68 | /** |
||
69 | * Filters the XML-RPC methods available to Jetpack for unauthenticated users. |
||
70 | * |
||
71 | * @since 3.0.0 |
||
72 | * |
||
73 | * @param array $jetpack_methods XML-RPC methods available to the Jetpack Server. |
||
74 | * @param array $core_methods Available core XML-RPC methods. |
||
75 | */ |
||
76 | return apply_filters( 'jetpack_xmlrpc_unauthenticated_methods', $jetpack_methods, $core_methods ); |
||
77 | } |
||
78 | |||
79 | /** |
||
80 | * Whitelist of the bootstrap XML-RPC methods |
||
81 | */ |
||
82 | function bootstrap_xmlrpc_methods() { |
||
83 | return array( |
||
84 | 'jetpack.remoteAuthorize' => array( $this, 'remote_authorize' ), |
||
85 | 'jetpack.remoteRegister' => array( $this, 'remote_register' ), |
||
86 | ); |
||
87 | } |
||
88 | |||
89 | function authorize_xmlrpc_methods() { |
||
90 | return array( |
||
91 | 'jetpack.remoteAuthorize' => array( $this, 'remote_authorize' ), |
||
92 | ); |
||
93 | } |
||
94 | |||
95 | function provision_xmlrpc_methods() { |
||
96 | return array( |
||
97 | 'jetpack.remoteRegister' => array( $this, 'remote_register' ), |
||
98 | 'jetpack.remoteProvision' => array( $this, 'remote_provision' ), |
||
99 | 'jetpack.remoteConnect' => array( $this, 'remote_connect' ), |
||
100 | ); |
||
101 | } |
||
102 | |||
103 | function remote_authorize( $request ) { |
||
104 | $user = get_user_by( 'id', $request['state'] ); |
||
105 | $this->tracking->record_user_event( 'jpc_remote_authorize_begin', array(), $user ); |
||
106 | |||
107 | foreach( array( 'secret', 'state', 'redirect_uri', 'code' ) as $required ) { |
||
108 | if ( ! isset( $request[ $required ] ) || empty( $request[ $required ] ) ) { |
||
109 | return $this->error( new Jetpack_Error( 'missing_parameter', 'One or more parameters is missing from the request.', 400 ), 'jpc_remote_authorize_fail' ); |
||
110 | } |
||
111 | } |
||
112 | |||
113 | if ( ! $user ) { |
||
114 | return $this->error( new Jetpack_Error( 'user_unknown', 'User not found.', 404 ), 'jpc_remote_authorize_fail' ); |
||
115 | } |
||
116 | |||
117 | if ( Jetpack::is_active() && Jetpack::is_user_connected( $request['state'] ) ) { |
||
118 | return $this->error( new Jetpack_Error( 'already_connected', 'User already connected.', 400 ), 'jpc_remote_authorize_fail' ); |
||
119 | } |
||
120 | |||
121 | $verified = $this->verify_action( array( 'authorize', $request['secret'], $request['state'] ) ); |
||
122 | |||
123 | if ( is_a( $verified, 'IXR_Error' ) ) { |
||
124 | return $this->error( $verified, 'jpc_remote_authorize_fail' ); |
||
125 | } |
||
126 | |||
127 | wp_set_current_user( $request['state'] ); |
||
128 | |||
129 | $client_server = new Jetpack_Client_Server; |
||
130 | $result = $client_server->authorize( $request ); |
||
131 | |||
132 | if ( is_wp_error( $result ) ) { |
||
133 | return $this->error( $result, 'jpc_remote_authorize_fail' ); |
||
134 | } |
||
135 | |||
136 | $this->tracking->record_user_event( 'jpc_remote_authorize_success' ); |
||
137 | |||
138 | return array( |
||
139 | 'result' => $result, |
||
140 | ); |
||
141 | } |
||
142 | |||
143 | /** |
||
144 | * This XML-RPC method is called from the /jpphp/provision endpoint on WPCOM in order to |
||
145 | * register this site so that a plan can be provisioned. |
||
146 | * |
||
147 | * @param array $request An array containing at minimum nonce and local_user keys. |
||
148 | * |
||
149 | * @return WP_Error|array |
||
150 | */ |
||
151 | public function remote_register( $request ) { |
||
152 | $this->tracking->record_user_event( 'jpc_remote_register_begin', array() ); |
||
153 | |||
154 | $user = $this->fetch_and_verify_local_user( $request ); |
||
155 | |||
156 | if ( ! $user ) { |
||
157 | return $this->error( new WP_Error( 'input_error', __( 'Valid user is required', 'jetpack' ), 400 ), 'jpc_remote_register_fail' ); |
||
158 | } |
||
159 | |||
160 | if ( is_wp_error( $user ) || is_a( $user, 'IXR_Error' ) ) { |
||
161 | return $this->error( $user, 'jpc_remote_register_fail' ); |
||
162 | } |
||
163 | |||
164 | if ( empty( $request['nonce'] ) ) { |
||
165 | return $this->error( |
||
166 | new Jetpack_Error( |
||
167 | 'nonce_missing', |
||
168 | __( 'The required "nonce" parameter is missing.', 'jetpack' ), |
||
169 | 400 |
||
170 | ), |
||
171 | 'jpc_remote_register_fail' |
||
172 | ); |
||
173 | } |
||
174 | |||
175 | $nonce = sanitize_text_field( $request['nonce'] ); |
||
176 | unset( $request['nonce'] ); |
||
177 | |||
178 | $api_url = Jetpack::fix_url_for_bad_hosts( Jetpack::api_url( 'partner_provision_nonce_check' ) ); |
||
179 | $response = Jetpack_Client::_wp_remote_request( |
||
180 | esc_url_raw( add_query_arg( 'nonce', $nonce, $api_url ) ), |
||
181 | array( 'method' => 'GET' ), |
||
182 | true |
||
183 | ); |
||
184 | |||
185 | if ( |
||
186 | 200 !== wp_remote_retrieve_response_code( $response ) || |
||
187 | 'OK' !== trim( wp_remote_retrieve_body( $response ) ) |
||
188 | ) { |
||
189 | return $this->error( |
||
190 | new Jetpack_Error( |
||
191 | 'invalid_nonce', |
||
192 | __( 'There was an issue validating this request.', 'jetpack' ), |
||
193 | 400 |
||
194 | ), |
||
195 | 'jpc_remote_register_fail' |
||
196 | ); |
||
197 | } |
||
198 | |||
199 | if ( ! Jetpack_Options::get_option( 'id' ) || ! Jetpack_Data::get_access_token() || ! empty( $request['force'] ) ) { |
||
|
|||
200 | wp_set_current_user( $user->ID ); |
||
201 | |||
202 | // This code mostly copied from Jetpack::admin_page_load. |
||
203 | Jetpack::maybe_set_version_option(); |
||
204 | $registered = Jetpack::try_registration(); |
||
205 | if ( is_wp_error( $registered ) ) { |
||
206 | return $this->error( $registered, 'jpc_remote_register_fail' ); |
||
207 | } elseif ( ! $registered ) { |
||
208 | return $this->error( |
||
209 | new Jetpack_Error( |
||
210 | 'registration_error', |
||
211 | __( 'There was an unspecified error registering the site', 'jetpack' ), |
||
212 | 400 |
||
213 | ), |
||
214 | 'jpc_remote_register_fail' |
||
215 | ); |
||
216 | } |
||
217 | } |
||
218 | |||
219 | $this->tracking->record_user_event( 'jpc_remote_register_success' ); |
||
220 | |||
221 | return array( |
||
222 | 'client_id' => Jetpack_Options::get_option( 'id' ) |
||
223 | ); |
||
224 | } |
||
225 | |||
226 | /** |
||
227 | * This XML-RPC method is called from the /jpphp/provision endpoint on WPCOM in order to |
||
228 | * register this site so that a plan can be provisioned. |
||
229 | * |
||
230 | * @param array $request An array containing at minimum a nonce key and a local_username key. |
||
231 | * |
||
232 | * @return WP_Error|array |
||
233 | */ |
||
234 | public function remote_provision( $request ) { |
||
235 | $user = $this->fetch_and_verify_local_user( $request ); |
||
236 | |||
237 | if ( ! $user ) { |
||
238 | return $this->error( new WP_Error( 'input_error', __( 'Valid user is required', 'jetpack' ), 400 ), 'jpc_remote_provision_fail' ); |
||
239 | } |
||
240 | |||
241 | if ( is_wp_error( $user ) || is_a( $user, 'IXR_Error' ) ) { |
||
242 | return $this->error( $user, 'jpc_remote_provision_fail' ); |
||
243 | } |
||
244 | |||
245 | $site_icon = get_site_icon_url(); |
||
246 | |||
247 | $auto_enable_sso = ( ! Jetpack::is_active() || Jetpack::is_module_active( 'sso' ) ); |
||
248 | |||
249 | /** This filter is documented in class.jetpack-cli.php */ |
||
250 | View Code Duplication | if ( apply_filters( 'jetpack_start_enable_sso', $auto_enable_sso ) ) { |
|
251 | $redirect_uri = add_query_arg( |
||
252 | array( |
||
253 | 'action' => 'jetpack-sso', |
||
254 | 'redirect_to' => rawurlencode( admin_url() ), |
||
255 | ), |
||
256 | wp_login_url() // TODO: come back to Jetpack dashboard? |
||
257 | ); |
||
258 | } else { |
||
259 | $redirect_uri = admin_url(); |
||
260 | } |
||
261 | |||
262 | // Generate secrets. |
||
263 | $role = Jetpack::translate_user_to_role( $user ); |
||
264 | $secrets = Jetpack::init()->generate_secrets( 'authorize', $user->ID ); |
||
265 | |||
266 | $response = array( |
||
267 | 'jp_version' => JETPACK__VERSION, |
||
268 | 'redirect_uri' => $redirect_uri, |
||
269 | 'user_id' => $user->ID, |
||
270 | 'user_email' => $user->user_email, |
||
271 | 'user_login' => $user->user_login, |
||
272 | 'scope' => Jetpack::sign_role( $role, $user->ID ), |
||
273 | 'secret' => $secrets['secret_1'], |
||
274 | 'is_active' => Jetpack::is_active(), |
||
275 | ); |
||
276 | |||
277 | if ( $site_icon ) { |
||
278 | $response['site_icon'] = $site_icon; |
||
279 | } |
||
280 | |||
281 | if ( ! empty( $request['onboarding'] ) ) { |
||
282 | Jetpack::create_onboarding_token(); |
||
283 | $response['onboarding_token'] = Jetpack_Options::get_option( 'onboarding' ); |
||
284 | } |
||
285 | |||
286 | return $response; |
||
287 | } |
||
288 | |||
289 | /** |
||
290 | * Given an array containing a local user identifier and a nonce, will attempt to fetch and set |
||
291 | * an access token for the given user. |
||
292 | * |
||
293 | * @param array $request An array containing local_user and nonce keys at minimum. |
||
294 | * @return mixed |
||
295 | */ |
||
296 | public function remote_connect( $request, $ixr_client = false ) { |
||
297 | if ( Jetpack::is_active() ) { |
||
298 | return $this->error( |
||
299 | new WP_Error( |
||
300 | 'already_connected', |
||
301 | __( 'Jetpack is already connected.', 'jetpack' ), |
||
302 | 400 |
||
303 | ), |
||
304 | 'jpc_remote_connect_fail' |
||
305 | ); |
||
306 | } |
||
307 | |||
308 | $user = $this->fetch_and_verify_local_user( $request ); |
||
309 | |||
310 | if ( ! $user || is_wp_error( $user ) || is_a( $user, 'IXR_Error' ) ) { |
||
311 | return $this->error( |
||
312 | new WP_Error( |
||
313 | 'input_error', |
||
314 | __( 'Valid user is required.', 'jetpack' ), |
||
315 | 400 |
||
316 | ), |
||
317 | 'jpc_remote_connect_fail' |
||
318 | ); |
||
319 | } |
||
320 | |||
321 | if ( empty( $request['nonce'] ) ) { |
||
322 | return $this->error( |
||
323 | new WP_Error( |
||
324 | 'input_error', |
||
325 | __( 'A non-empty nonce must be supplied.', 'jetpack' ), |
||
326 | 400 |
||
327 | ), |
||
328 | 'jpc_remote_connect_fail' |
||
329 | ); |
||
330 | } |
||
331 | |||
332 | if ( ! $ixr_client ) { |
||
333 | Jetpack::load_xml_rpc_client(); |
||
334 | $ixr_client = new Jetpack_IXR_Client(); |
||
335 | } |
||
336 | $ixr_client->query( 'jetpack.getUserAccessToken', array( |
||
337 | 'nonce' => sanitize_text_field( $request['nonce'] ), |
||
338 | 'external_user_id' => $user->ID, |
||
339 | ) ); |
||
340 | |||
341 | $token = $ixr_client->isError() ? false : $ixr_client->getResponse(); |
||
342 | if ( empty( $token ) ) { |
||
343 | return $this->error( |
||
344 | new WP_Error( |
||
345 | 'token_fetch_failed', |
||
346 | __( 'Failed to fetch user token from WordPress.com.', 'jetpack' ), |
||
347 | 400 |
||
348 | ), |
||
349 | 'jpc_remote_connect_fail' |
||
350 | ); |
||
351 | } |
||
352 | $token = sanitize_text_field( $token ); |
||
353 | |||
354 | Jetpack::update_user_token( $user->ID, sprintf( '%s.%d', $token, $user->ID ), true ); |
||
355 | |||
356 | $this->do_post_authorization(); |
||
357 | |||
358 | return Jetpack::is_active(); |
||
359 | } |
||
360 | |||
361 | private function fetch_and_verify_local_user( $request ) { |
||
388 | |||
389 | private function tracks_record_error( $name, $error, $user = null ) { |
||
390 | if ( is_wp_error( $error ) ) { |
||
391 | $this->tracking->record_user_event( $name, array( |
||
392 | 'error_code' => $error->get_error_code(), |
||
393 | 'error_message' => $error->get_error_message() |
||
394 | ), $user ); |
||
395 | } elseif( is_a( $error, 'IXR_Error' ) ) { |
||
396 | $this->tracking->record_user_event( $name, array( |
||
397 | 'error_code' => $error->code, |
||
404 | |||
405 | /** |
||
406 | * @return WP_Error|string secret_2 on success, WP_Error( error_code => error_code, error_message => error description, error_data => status code ) on failure |
||
407 | * |
||
408 | * Possible error_codes: |
||
409 | * |
||
410 | * verify_secret_1_missing |
||
411 | * verify_secret_1_malformed |
||
412 | * verify_secrets_missing: verification secrets are not found in database |
||
413 | * verify_secrets_incomplete: verification secrets are only partially found in database |
||
414 | * verify_secrets_expired: verification secrets have expired |
||
415 | * verify_secrets_mismatch: stored secret_1 does not match secret_1 sent by Jetpack.WordPress.com |
||
416 | * state_missing: required parameter of state not found |
||
417 | * state_malformed: state is not a digit |
||
418 | * invalid_state: state in request does not match the stored state |
||
419 | * |
||
420 | * The 'authorize' and 'register' actions have additional error codes |
||
421 | * |
||
422 | * Possible values for action are `authorize`, `publicize` and `register`. |
||
423 | * |
||
424 | * state_missing: a state ( user id ) was not supplied |
||
425 | * state_malformed: state is not the correct data type |
||
426 | * invalid_state: supplied state does not match the stored state |
||
427 | */ |
||
428 | function verify_action( $params ) { |
||
495 | |||
496 | /** |
||
497 | * Wrapper for wp_authenticate( $username, $password ); |
||
498 | * |
||
499 | * @return WP_User|bool |
||
500 | */ |
||
501 | function login() { |
||
518 | |||
519 | /** |
||
520 | * Returns the current error as an IXR_Error |
||
521 | * |
||
522 | * @return bool|IXR_Error |
||
523 | */ |
||
524 | function error( $error = null, $tracks_event_name = null, $user = null ) { |
||
547 | |||
548 | /* API Methods */ |
||
549 | |||
550 | /** |
||
551 | * Just authenticates with the given Jetpack credentials. |
||
552 | * |
||
553 | * @return string The current Jetpack version number |
||
554 | */ |
||
555 | function test_connection() { |
||
558 | |||
559 | function test_api_user_code( $args ) { |
||
601 | |||
602 | /** |
||
603 | * Disconnect this blog from the connected wordpress.com account |
||
604 | * @return boolean |
||
605 | */ |
||
606 | function disconnect_blog() { |
||
618 | |||
619 | /** |
||
620 | * Unlink a user from WordPress.com |
||
621 | * |
||
622 | * This will fail if called by the Master User. |
||
623 | */ |
||
624 | function unlink_user() { |
||
628 | |||
629 | /** |
||
630 | * Returns any object that is able to be synced |
||
631 | */ |
||
632 | function sync_object( $args ) { |
||
641 | |||
642 | /** |
||
643 | * Returns the home URL and site URL for the current site which can be used on the WPCOM side for |
||
644 | * IDC mitigation to decide whether sync should be allowed if the home and siteurl values differ between WPCOM |
||
645 | * and the remote Jetpack site. |
||
646 | * |
||
647 | * @return array |
||
648 | */ |
||
649 | function validate_urls_for_idc_mitigation() { |
||
655 | |||
656 | /** |
||
657 | * Returns what features are available. Uses the slug of the module files. |
||
658 | * |
||
659 | * @return array |
||
660 | */ |
||
661 | View Code Duplication | function features_available() { |
|
670 | |||
671 | /** |
||
672 | * Returns what features are enabled. Uses the slug of the modules files. |
||
673 | * |
||
674 | * @return array |
||
675 | */ |
||
676 | View Code Duplication | function features_enabled() { |
|
685 | |||
686 | function update_attachment_parent( $args ) { |
||
695 | |||
696 | function json_api( $args = array() ) { |
||
798 | |||
799 | /** |
||
800 | * Handles authorization actions after connecting a site, such as enabling modules. |
||
801 | * |
||
802 | * This do_post_authorization() is used in this class, as opposed to calling |
||
803 | * Jetpack::handle_post_authorization_actions() directly so that we can mock this method as necessary. |
||
804 | * |
||
805 | * @return void |
||
806 | */ |
||
807 | public function do_post_authorization() { |
||
812 | } |
||
813 |
This method has been deprecated. The supplier of the class has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.