WP2D_API   F
last analyzed

Complexity

Total Complexity 82

Size/Duplication

Total Lines 709
Duplicated Lines 1.41 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 0
Metric Value
wmc 82
lcom 1
cbo 1
dl 10
loc 709
rs 1.891
c 0
b 0
f 0

21 Methods

Rating   Name   Duplication   Size   Complexity  
A get_pod_url() 0 10 3
A __construct() 0 5 1
B init() 0 31 6
A has_last_error() 0 3 1
A get_last_error_object() 0 8 2
A get_last_error() 0 8 3
A fetch_token() 0 9 3
A check_init() 0 9 2
A check_login() 0 12 3
A is_logged_in() 0 3 1
C login() 0 70 11
A logout() 0 7 1
A deinit() 0 7 1
B post() 0 64 7
B delete() 10 52 10
A get_aspects() 0 5 2
A get_services() 0 5 2
C get_aspects_services() 0 49 13
B request() 0 61 7
A error() 0 9 1
A parse_regex() 0 10 2

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 WP2D_API 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 WP2D_API, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * API-like class to deal with HTTP(S) requests to diaspora* using WP_HTTP API.
4
 *
5
 * Basic functionality includes:
6
 * - Logging in to diaspora*
7
 * - Fetching a user's aspects and connected services
8
 * - Posting to diaspora*
9
 *
10
 * Ideas in this class are based on classes from:
11
 * https://github.com/Faldrian/WP-diaspora-postwidget/blob/master/wp-diaspora-postwidget/diasphp.php -- Thanks, Faldrian!
12
 * https://github.com/meitar/diasposter/blob/master/lib/Diaspora_Connection.php -- Thanks, Meitar
13
 *
14
 * Which in turn are based on:
15
 * https://github.com/cocreature/diaspy/blob/master/client.py -- Thanks, Moritz
16
 *
17
 * @package WP_To_Diaspora\API
18
 * @since   1.2.7
19
 */
20
21
// Exit if accessed directly.
22
defined( 'ABSPATH' ) || exit;
23
24
/**
25
 * API class to talk to diaspora*.
26
 */
27
class WP2D_API {
28
29
	/**
30
	 * The provider name to display when posting to diaspora*.
31
	 *
32
	 * @var string
33
	 */
34
	public $provider = 'WP to diaspora*';
35
36
	/**
37
	 * The last http request error that occurred.
38
	 *
39
	 * @var WP_Error
40
	 */
41
	private $last_error;
42
43
	/**
44
	 * Security token to be used for making requests.
45
	 *
46
	 * @var string
47
	 */
48
	private $token;
49
50
	/**
51
	 * Save the cookies for the requests.
52
	 *
53
	 * @var array
54
	 */
55
	private $cookies;
56
57
	/**
58
	 * The last http request made to diaspora*.
59
	 * Contains the response and request infos.
60
	 *
61
	 * @var object
62
	 */
63
	private $last_request;
64
65
	/**
66
	 * Is this a secure server, use HTTPS instead of HTTP?
67
	 *
68
	 * @var boolean
69
	 */
70
	private $is_secure;
71
72
	/**
73
	 * The pod domain to make the http requests to.
74
	 *
75
	 * @var string
76
	 */
77
	private $pod;
78
79
	/**
80
	 * Username to use when logging in to diaspora*.
81
	 *
82
	 * @var string
83
	 */
84
	private $username;
85
86
	/**
87
	 * Password to use when logging in to diaspora*.
88
	 *
89
	 * @var string
90
	 */
91
	private $password;
92
93
	/**
94
	 * Remember the current login state.
95
	 *
96
	 * @var boolean
97
	 */
98
	private $is_logged_in = false;
99
100
	/**
101
	 * The list of user's aspects, which get set after ever http request.
102
	 *
103
	 * @var array
104
	 */
105
	private $aspects = [];
106
107
	/**
108
	 * The list of user's connected services, which get set after ever http request.
109
	 *
110
	 * @var array
111
	 */
112
	private $services = [];
113
114
	/**
115
	 * List of regex expressions used to filter out details from http request responses.
116
	 *
117
	 * @var array
118
	 */
119
	private static $regexes = [
120
		'token'    => '/content="(.*?)" name="csrf-token"|name="csrf-token" content="(.*?)"/',
121
		'aspects'  => '/"aspects"\:(\[.*?\])/',
122
		'services' => '/"configured_services"\:(\[.*?\])/',
123
	];
124
125
	/**
126
	 * The full pod url, with the used protocol.
127
	 *
128
	 * @param string $path Path to add to the pod url.
129
	 *
130
	 * @return string Full pod url.
131
	 */
132
	public function get_pod_url( $path = '' ) {
133
		$path = trim( $path, ' /' );
134
135
		// Add a slash to the beginning?
136
		if ( '' !== $path ) {
137
			$path = '/' . $path;
138
		}
139
140
		return sprintf( 'http%s://%s%s', $this->is_secure ? 's' : '', $this->pod, $path );
141
	}
142
143
	/**
144
	 * Constructor to initialise the connection to diaspora*.
145
	 *
146
	 * @param string $pod       The pod domain to connect to.
147
	 * @param bool   $is_secure Is this a secure server? (Default: true).
148
	 */
149
	public function __construct( $pod, $is_secure = true ) {
150
		// Set class variables.
151
		$this->pod       = $pod;
152
		$this->is_secure = (bool) $is_secure;
153
	}
154
155
	/**
156
	 * Initialise the connection to diaspora*. The pod and protocol can be changed by passing new parameters.
157
	 * Check if we can connect to the pod to retrieve the token.
158
	 *
159
	 * @param string $pod       Pod domain to connect to, if it should be changed.
160
	 * @param bool   $is_secure Is this a secure server? (Default: true).
161
	 *
162
	 * @return bool True if we could get the token, else false.
163
	 */
164
	public function init( $pod = null, $is_secure = true ) {
165
		// If we are changing pod, we need to fetch a new token.
166
		$force_new_token = false;
167
168
		// When initialising a connection, clear the last error.
169
		// This is important when multiple init tries happen.
170
		$this->last_error = null;
171
172
		// Change the pod we are connecting to?
173
		if ( null !== $pod && ( $this->pod !== $pod || $this->is_secure !== $is_secure ) ) {
174
			$this->pod       = $pod;
175
			$this->is_secure = (bool) $is_secure;
176
			$force_new_token = true;
177
		}
178
179
		// Get and save the token.
180
		if ( null === $this->fetch_token( $force_new_token ) ) {
181
			$error = $this->has_last_error() ? ' ' . $this->get_last_error() : '';
182
			$this->error( 'wp2d_api_init_failed',
183
				sprintf(
184
					_x( 'Failed to initialise connection to pod "%s".', 'Placeholder is the full pod URL.', 'wp-to-diaspora' ),
185
					$this->get_pod_url()
186
				) . $error,
187
				[ 'help_tab' => 'troubleshooting' ]
188
			);
189
190
			return false;
191
		}
192
193
		return true;
194
	}
195
196
	/**
197
	 * Check if there is an API error around.
198
	 *
199
	 * @return bool If there is an API error around.
200
	 */
201
	public function has_last_error() {
202
		return is_wp_error( $this->last_error );
203
	}
204
205
	/**
206
	 * Get the last API error object.
207
	 *
208
	 * @param bool $clear If the error should be cleared after returning it.
209
	 *
210
	 * @return WP_Error|null The last API error object or null.
211
	 */
212
	public function get_last_error_object( $clear = true ) {
213
		$last_error = $this->last_error;
214
		if ( $clear ) {
215
			$this->last_error = null;
216
		}
217
218
		return $last_error;
219
	}
220
221
	/**
222
	 * Get the last API error message.
223
	 *
224
	 * @param bool $clear If the error should be cleared after returning it.
225
	 *
226
	 * @return string The last API error message.
227
	 */
228
	public function get_last_error( $clear = false ) {
229
		$last_error = $this->has_last_error() ? $this->last_error->get_error_message() : '';
230
		if ( $clear ) {
231
			$this->last_error = null;
232
		}
233
234
		return $last_error;
235
	}
236
237
	/**
238
	 * Fetch the secure token from Diaspora and save it for future use.
239
	 *
240
	 * @param bool $force Force to fetch a new token.
241
	 *
242
	 * @return string The fetched token.
243
	 */
244
	private function fetch_token( $force = false ) {
245
		if ( null === $this->token || $force ) {
246
			// Go directly to the sign in page, as it would redirect to there anyway.
247
			// Since _request function automatically saves the new token, just call it with no data.
248
			$this->request( '/users/sign_in' );
249
		}
250
251
		return $this->token;
252
	}
253
254
	/**
255
	 * Check if the API has been initialised. Otherwise set the last error.
256
	 *
257
	 * @return bool Has the connection been initialised?
258
	 */
259
	private function check_init() {
260
		if ( null === $this->token ) {
261
			$this->error( 'wp2d_api_connection_not_initialised', __( 'Connection not initialised.', 'wp-to-diaspora' ) );
262
263
			return false;
264
		}
265
266
		return true;
267
	}
268
269
	/**
270
	 * Check if we're logged in. Otherwise set the last error.
271
	 *
272
	 * @return bool Are we logged in already?
273
	 */
274
	private function check_login() {
275
		if ( ! $this->check_init() ) {
276
			return false;
277
		}
278
		if ( ! $this->is_logged_in() ) {
279
			$this->error( 'wp2d_api_not_logged_in', __( 'Not logged in.', 'wp-to-diaspora' ) );
280
281
			return false;
282
		}
283
284
		return true;
285
	}
286
287
	/**
288
	 * Check if we are logged in.
289
	 *
290
	 * @return bool Are we logged in already?
291
	 */
292
	public function is_logged_in() {
293
		return $this->is_logged_in;
294
	}
295
296
	/**
297
	 * Log in to diaspora*.
298
	 *
299
	 * @param string $username Username used for login.
300
	 * @param string $password Password used for login.
301
	 * @param bool   $force    Force a new login even if we are already logged in.
302
	 *
303
	 * @return bool Did the login succeed?
304
	 */
305
	public function login( $username, $password, $force = false ) {
306
		// Has the connection been initialised?
307
		if ( ! $this->check_init() ) {
308
			$this->logout();
309
310
			return false;
311
		}
312
313
		// Username and password both need to be set.
314
		if ( ! isset( $username, $password ) || '' === $username || '' === $password ) {
315
			// Invalid credentials.
316
			$this->error(
317
				'wp2d_api_login_failed',
318
				__( 'Invalid credentials. Please re-save your login info.', 'wp-to-diaspora' ),
319
				[ 'help_tab' => 'troubleshooting' ]
320
			);
321
			$this->logout();
322
323
			return false;
324
		}
325
326
		// If we are already logged in and not forcing a relogin, return.
327
		if ( ! $force &&
328
			$username === $this->username &&
329
			$password === $this->password &&
330
			$this->is_logged_in()
331
		) {
332
			return true;
333
		}
334
335
		// Set the newly passed username and password.
336
		$this->username = $username;
337
		$this->password = $password;
338
339
		// Set up the login parameters.
340
		$params = [
341
			'user[username]'     => $this->username,
342
			'user[password]'     => $this->password,
343
			'authenticity_token' => $this->fetch_token(),
344
		];
345
346
		$args = [
347
			'method' => 'POST',
348
			'body'   => $params,
349
		];
350
351
		// Try to sign in.
352
		$this->request( '/users/sign_in', $args );
353
354
		// Can we load the bookmarklet to make sure we're logged in?
355
		$response = $this->request( '/bookmarklet' );
356
357
		// If the request isn't successful, we are not logged in correctly.
358
		if ( is_wp_error( $response ) || 200 !== $response->code ) {
359
			// Login failed.
360
			$this->error(
361
				'wp2d_api_login_failed',
362
				__( 'Login failed. Check your login details.', 'wp-to-diaspora' ),
363
				[ 'help_tab' => 'troubleshooting' ]
364
			);
365
			$this->logout();
366
367
			return false;
368
		}
369
370
		// Login succeeded.
371
		$this->is_logged_in = true;
372
373
		return true;
374
	}
375
376
	/**
377
	 * Perform a logout, resetting all login info.
378
	 *
379
	 * @since 1.6.0
380
	 */
381
	public function logout() {
382
		$this->is_logged_in = false;
383
		$this->username     = null;
384
		$this->password     = null;
385
		$this->aspects      = [];
386
		$this->services     = [];
387
	}
388
389
	/**
390
	 * Perform a de-initialisation, resetting all class variables.
391
	 *
392
	 * @since 1.7.0
393
	 */
394
	public function deinit() {
395
		$this->logout();
396
		$this->last_error   = null;
397
		$this->token        = null;
398
		$this->cookies      = [];
399
		$this->last_request = null;
400
	}
401
402
	/**
403
	 * Post to diaspora*.
404
	 *
405
	 * @param string       $text       The text to post.
406
	 * @param array|string $aspects    The aspects to post to. Array or comma separated ids.
407
	 * @param array        $extra_data Any extra data to be added to the post call.
408
	 *
409
	 * @return bool|object Return the response data of the new diaspora* post if successfully posted, else false.
410
	 */
411
	public function post( $text, $aspects = 'public', array $extra_data = [] ) {
412
		// Are we logged in?
413
		if ( ! $this->check_login() ) {
414
			return false;
415
		}
416
417
		// Put the aspects into a clean array.
418
		$aspects = array_filter( WP2D_Helpers::str_to_arr( $aspects ) );
419
420
		// If no aspects have been selected or the public one is also included, choose public only.
421
		if ( empty( $aspects ) || in_array( 'public', $aspects, true ) ) {
422
			$aspects = 'public';
423
		}
424
425
		// Prepare post data.
426
		$post_data = [
427
			'aspect_ids'     => $aspects,
428
			'status_message' => [
429
				'text'                  => $text,
430
				'provider_display_name' => $this->provider,
431
			],
432
		];
433
434
		// Add any extra data to the post.
435
		if ( ! empty( $extra_data ) ) {
436
			$post_data += $extra_data;
437
		}
438
439
		$post_data = wp_json_encode( $post_data );
440
441
		$args = [
442
			'method'  => 'POST',
443
			'body'    => $post_data,
444
			'headers' => [
445
				'Accept'       => 'application/json',
446
				'Content-Type' => 'application/json',
447
				'X-CSRF-Token' => $this->fetch_token(),
448
			],
449
		];
450
451
		// Submit the post.
452
		$response = $this->request( '/status_messages', $args );
453
454
		if ( is_wp_error( $response ) ) {
455
			$this->error( 'wp2d_api_post_failed', $response->get_error_message() );
456
457
			return false;
458
		}
459
460
		$diaspost = json_decode( $response->body );
461
		if ( 201 !== $response->code ) {
462
			$this->error(
463
				'wp2d_api_post_failed',
464
				$diaspost->error ?? _x( 'Unknown error occurred.', 'When an unknown error occurred in the WP2D_API object.', 'wp-to-diaspora' )
465
			);
466
467
			return false;
468
		}
469
470
		// Add additional info to our diaspora post object.
471
		$diaspost->permalink = $this->get_pod_url( '/posts/' . $diaspost->guid );
472
473
		return $diaspost;
474
	}
475
476
	/**
477
	 * Delete a post or comment from diaspora*.
478
	 *
479
	 * @since 1.6.0
480
	 *
481
	 * @param string $what What to delete, 'post' or 'comment'.
482
	 * @param string $id   The ID of the post or comment to delete.
483
	 *
484
	 * @return bool If the deletion was successful.
485
	 */
486
	public function delete( $what, $id ) {
487
		// Are we logged in?
488
		if ( ! $this->check_login() ) {
489
			return false;
490
		}
491
492
		// For now, only deleting posts and comments is allowed.
493
		if ( ! in_array( $what, [ 'post', 'comment' ], true ) ) {
494
			$this->error( 'wp2d_api_delete_failed', __( 'You can only delete posts and comments.', 'wp-to-diaspora' ) );
495
496
			return false;
497
		}
498
499
		$args = [
500
			'method'  => 'DELETE',
501
			'headers' => [
502
				'Accept'       => 'application/json',
503
				'Content-Type' => 'application/json',
504
				'X-CSRF-Token' => $this->fetch_token(),
505
			],
506
		];
507
508
		// Try to delete the post or comment.
509
		$response = $this->request( '/' . $what . 's/' . $id, $args );
510
511
		if ( is_wp_error( $response ) ) {
512
			$error_message = $response->get_error_message();
513
		} else {
514
			switch ( $response->code ) {
515
				case 204:
516
					return true;
517 View Code Duplication
				case 404:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
518
					$error_message = ( 'post' === $what )
519
						? __( 'The post you tried to delete does not exist.', 'wp-to-diaspora' )
520
						: __( 'The comment you tried to delete does not exist.', 'wp-to-diaspora' );
521
					break;
522 View Code Duplication
				case 403:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
523
					$error_message = ( 'post' === $what )
524
						? __( 'The post you tried to delete does not belong to you.', 'wp-to-diaspora' )
525
						: __( 'The comment you tried to delete does not belong to you.', 'wp-to-diaspora' );
526
					break;
527
				case 500:
528
				default:
529
					$error_message = _x( 'Unknown error occurred.', 'When an unknown error occurred in the WP2D_API object.', 'wp-to-diaspora' );
530
					break;
531
			}
532
		}
533
534
		$this->error( 'wp2d_api_delete_' . $what . '_failed', $error_message );
535
536
		return false;
537
	}
538
539
	/**
540
	 * Get the list of aspects.
541
	 *
542
	 * @param bool $force Force to fetch new aspects.
543
	 *
544
	 * @return array|bool Array of aspect objects or false.
545
	 */
546
	public function get_aspects( $force = false ) {
547
		$this->aspects = $this->get_aspects_services( 'aspects', $this->aspects, $force );
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->get_aspects_servi...$this->aspects, $force) can also be of type false. However, the property $aspects is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
548
549
		return is_array( $this->aspects ) ? $this->aspects : false;
550
	}
551
552
	/**
553
	 * Get the list of connected services.
554
	 *
555
	 * @param bool $force Force to fetch new connected services.
556
	 *
557
	 * @return array|bool Array of service objects or false.
558
	 */
559
	public function get_services( $force = false ) {
560
		$this->services = $this->get_aspects_services( 'services', $this->services, $force );
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->get_aspects_servi...this->services, $force) can also be of type false. However, the property $services is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
561
562
		return is_array( $this->services ) ? $this->services : false;
563
	}
564
565
	/**
566
	 * Get the list of aspects or connected services.
567
	 *
568
	 * @param string $type  Type of list to get.
569
	 * @param array  $list  The current list of items.
570
	 * @param bool   $force Force to fetch new list.
571
	 *
572
	 * @return array|bool List of fetched aspects or services, or false.
573
	 */
574
	private function get_aspects_services( $type, $list, $force ) {
575
		if ( ! $this->check_login() ) {
576
			return false;
577
		}
578
579
		// Fetch the new list if the current list is empty or a reload is forced.
580
		if ( $force || empty( $list ) ) {
581
			$response = $this->request( '/bookmarklet' );
582
583
			if ( is_wp_error( $response ) || 200 !== $response->code ) {
584
				switch ( $type ) {
585
					case 'aspects':
586
						$this->error( 'wp2d_api_getting_aspects_failed', __( 'Error loading aspects.', 'wp-to-diaspora' ) );
587
						break;
588
					case 'services':
589
						$this->error( 'wp2d_api_getting_services_failed', __( 'Error loading services.', 'wp-to-diaspora' ) );
590
						break;
591
					default:
592
						$this->error( 'wp2d_api_getting_aspects_services_failed', _x( 'Unknown error occurred.', 'When an unknown error occurred in the WP2D_API object.', 'wp-to-diaspora' ) );
593
						break;
594
				}
595
596
				return false;
597
			}
598
599
			// Load the aspects or services.
600
			$raw_list = json_decode( $this->parse_regex( $type, $response->body ) );
601
			if ( is_array( $raw_list ) ) {
602
				// In case this fetch is forced, empty the list.
603
				$list = [];
604
605
				if ( 'aspects' === $type ) {
606
					// Add the 'public' aspect, as it's global and not user specific.
607
					$list['public'] = __( 'Public', 'wp-to-diaspora' );
608
609
					// Add all user specific aspects.
610
					foreach ( $raw_list as $aspect ) {
611
						$list[ $aspect->id ] = $aspect->name;
612
					}
613
				} elseif ( 'services' === $type ) {
614
					foreach ( $raw_list as $service ) {
615
						$list[ $service ] = ucfirst( $service );
616
					}
617
				}
618
			}
619
		}
620
621
		return $list;
622
	}
623
624
	/**
625
	 * Send an http(s) request via WP_HTTP API.
626
	 *
627
	 * @see WP_Http::request()
628
	 *
629
	 * @param string $url  The URL to request.
630
	 * @param array  $args Arguments to be posted with the request.
631
	 *
632
	 * @return object An object containing details about this request.
633
	 */
634
	private function request( $url, array $args = [] ) {
635
		// Prefix the full pod URL if necessary.
636
		if ( 0 === strpos( $url, '/' ) ) {
637
			$url = $this->get_pod_url( $url );
638
		}
639
640
		// Disable redirections so we can verify HTTP response codes.
641
		$defaults = [
642
			'method'      => 'GET',
643
			'redirection' => 0,
644
			'sslverify'   => true,
645
			'timeout'     => 60,
646
		];
647
648
		// If the certificate bundle has been downloaded manually, use that instead.
649
		// NOTE: This should actually never be necessary, it's a fallback!
650
		if ( file_exists( WP2D_DIR . '/cacert.pem' ) ) {
651
			$defaults['sslcertificates'] = WP2D_DIR . '/cacert.pem';
652
		}
653
654
		// Set the correct cookie.
655
		if ( ! empty( $this->cookies ) ) {
656
			$defaults['cookies'] = $this->cookies;
657
		}
658
659
		$args = wp_parse_args( $args, $defaults );
660
661
		// Get the response from the WP_HTTP request.
662
		$response = wp_remote_request( $url, $args );
663
664
		if ( is_wp_error( $response ) ) {
665
			$this->last_error = $response;
666
667
			return $response;
668
		}
669
670
		// Get the headers and the HTML response.
671
		$headers = wp_remote_retrieve_headers( $response );
672
		$body    = wp_remote_retrieve_body( $response );
673
674
		// Remember this request.
675
		$this->last_request           = new stdClass();
676
		$this->last_request->response = $response;
677
		$this->last_request->headers  = $headers;
678
		$this->last_request->body     = $body;
679
		$this->last_request->message  = wp_remote_retrieve_response_message( $response );
680
		$this->last_request->code     = wp_remote_retrieve_response_code( $response );
681
682
		// Save the new token.
683
		if ( $token = $this->parse_regex( 'token', $body ) ) {
684
			$this->token = $token;
685
		}
686
687
		// Save the latest cookies.
688
		if ( isset( $response['cookies'] ) ) {
689
			$this->cookies = $response['cookies'];
690
		}
691
692
		// Return the last request details.
693
		return $this->last_request;
694
	}
695
696
	/**
697
	 * Helper method to set the last occurred error.
698
	 *
699
	 * @since 1.6.0
700
	 *
701
	 * @see   WP_Error::__construct()
702
	 *
703
	 * @param string|int $code    Error code.
704
	 * @param string     $message Error message.
705
	 * @param mixed      $data    Error data.
706
	 */
707
	private function error( $code, $message, $data = '' ) {
708
		// Always add the code and message of the last request.
709
		$data = array_merge( array_filter( (array) $data ), [
710
			'code'    => $this->last_request->code ?? null,
711
			'message' => $this->last_request->message ?? null,
712
		] );
713
714
		$this->last_error = new WP_Error( $code, $message, $data );
715
	}
716
717
	/**
718
	 * Parse the regex and return the found string.
719
	 *
720
	 * @param string $regex   Shorthand of a saved regex or a custom regex.
721
	 * @param string $content Text to parse the regex with.
722
	 *
723
	 * @return string The found string, or an empty string.
724
	 */
725
	private function parse_regex( $regex, $content ) {
726
		// Use a shorthand regex if available.
727
		if ( array_key_exists( $regex, self::$regexes ) ) {
728
			$regex = self::$regexes[ $regex ];
729
		}
730
731
		preg_match( $regex, $content, $matches );
732
733
		return trim( array_pop( $matches ) );
734
	}
735
}
736