WP2D_API::get_services()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 2
eloc 3
nc 2
nop 1
1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 27 and the first side effect is on line 22.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
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 {
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
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 = array();
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 = array();
113
114
	/**
115
	 * List of regex expressions used to filter out details from http request responses.
116
	 *
117
	 * @var array
118
	 */
119
	private $_regexes = array(
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
	 * @return string Full pod url.
130
	 */
131
	public function get_pod_url( $path = '' ) {
132
		$path = trim( $path, ' /' );
133
134
		// Add a slash to the beginning?
135
		if ( '' !== $path ) {
136
			$path = '/' . $path;
137
		}
138
139
		return sprintf( 'http%s://%s%s', ( $this->_is_secure ) ? 's' : '', $this->_pod, $path );
140
	}
141
142
	/**
143
	 * Constructor to initialise the connection to diaspora*.
144
	 *
145
	 * @param string  $pod       The pod domain to connect to.
146
	 * @param boolean $is_secure Is this a secure server? (Default: true).
147
	 */
148
	public function __construct( $pod, $is_secure = true ) {
149
		// Set class variables.
150
		$this->_pod       = $pod;
151
		$this->_is_secure = (bool) $is_secure;
152
	}
153
154
	/**
155
	 * Initialise the connection to diaspora*. The pod and protocol can be changed by passing new parameters.
156
	 * Check if we can connect to the pod to retrieve the token.
157
	 *
158
	 * @param string  $pod       Pod domain to connect to, if it should be changed.
159
	 * @param boolean $is_secure Is this a secure server? (Default: true).
160
	 * @return boolean True if we could get the token, else false.
161
	 */
162
	public function init( $pod = null, $is_secure = true ) {
163
		// If we are changing pod, we need to fetch a new token.
164
		$force_new_token = false;
165
166
		// When initialising a connection, clear the last error.
167
		// This is important when multiple init tries happen.
168
		$this->_last_error = null;
169
170
		// Change the pod we are connecting to?
171
		if ( isset( $pod ) && ( $this->_pod !== $pod || $this->_is_secure !== $is_secure ) ) {
172
			$this->_pod       = $pod;
173
			$this->_is_secure = (bool) $is_secure;
174
			$force_new_token  = true;
175
		}
176
177
		// Get and save the token.
178
		if ( null === $this->_fetch_token( $force_new_token ) ) {
179
			$error = ( $this->has_last_error() ) ? ' ' . $this->get_last_error() : '';
180
			$this->_error( 'wp2d_api_init_failed',
181
				sprintf(
182
					_x( 'Failed to initialise connection to pod "%s".', 'Placeholder is the full pod URL.', 'wp-to-diaspora' ),
183
					$this->get_pod_url()
184
				) . $error,
185
				array( 'help_tab' => 'troubleshooting' )
186
			);
187
188
			return false;
189
		}
190
		return true;
191
	}
192
193
	/**
194
	 * Check if there is an API error around.
195
	 *
196
	 * @return boolean If there is an API error around.
197
	 */
198
	public function has_last_error() {
199
		return is_wp_error( $this->_last_error );
200
	}
201
202
	/**
203
	 * Get the last API error object.
204
	 *
205
	 * @param boolean $clear If the error should be cleared after returning it.
206
	 * @return WP_Error|null The last API error object or null.
207
	 */
208
	public function get_last_error_object( $clear = true ) {
209
		$last_error = $this->_last_error;
210
		$clear && $this->_last_error = null;
211
		return $last_error;
212
	}
213
214
	/**
215
	 * Get the last API error message.
216
	 *
217
	 * @param boolean $clear If the error should be cleared after returning it.
218
	 * @return string The last API error message.
219
	 */
220
	public function get_last_error( $clear = false ) {
221
		$last_error = ( $this->has_last_error() ) ? $this->_last_error->get_error_message() : '';
222
		$clear && $this->_last_error = null;
223
		return $last_error;
224
	}
225
226
	/**
227
	 * Fetch the secure token from Diaspora and save it for future use.
228
	 *
229
	 * @param boolean $force Force to fetch a new token.
230
	 * @return string The fetched token.
231
	 */
232
	private function _fetch_token( $force = false ) {
233
		if ( ! isset( $this->_token ) || (bool) $force ) {
234
			// Go directly to the sign in page, as it would redirect to there anyway.
235
			// Since _request function automatically saves the new token, just call it with no data.
236
			$this->_request( '/users/sign_in' );
237
		}
238
		return $this->_token;
239
	}
240
241
	/**
242
	 * Check if the API has been initialised. Otherwise set the last error.
243
	 *
244
	 * @return boolean Has the connection been initialised?
245
	 */
246
	private function _check_init() {
247
		if ( is_null( $this->_token ) ) {
248
			$this->_error( 'wp2d_api_connection_not_initialised', __( 'Connection not initialised.', 'wp-to-diaspora' ) );
249
			return false;
250
		}
251
		return true;
252
	}
253
254
	/**
255
	 * Check if we're logged in. Otherwise set the last error.
256
	 *
257
	 * @return boolean Are we logged in already?
258
	 */
259
	private function _check_login() {
260
		if ( ! $this->_check_init() ) {
261
			return false;
262
		}
263
		if ( ! $this->is_logged_in() ) {
264
			$this->_error( 'wp2d_api_not_logged_in', __( 'Not logged in.', 'wp-to-diaspora' ) );
265
			return false;
266
		}
267
		return true;
268
	}
269
270
	/**
271
	 * Check if we are logged in.
272
	 *
273
	 * @return boolean Are we logged in already?
274
	 */
275
	public function is_logged_in() {
276
		return $this->_is_logged_in;
277
	}
278
279
	/**
280
	 * Log in to diaspora*.
281
	 *
282
	 * @param string  $username Username used for login.
283
	 * @param string  $password Password used for login.
284
	 * @param boolean $force    Force a new login even if we are already logged in.
285
	 * @return boolean Did the login succeed?
286
	 */
287
	public function login( $username, $password, $force = false ) {
288
		// Has the connection been initialised?
289
		if ( ! $this->_check_init() ) {
290
			$this->logout();
291
			return false;
292
		}
293
294
		// Username and password both need to be set.
295
		$username = ( isset( $username ) && '' !== $username ) ? $username : null;
296
		$password = ( isset( $password ) && '' !== $password ) ? $password : null;
297
		if ( ! isset( $username, $password ) ) {
298
			$this->logout();
299
			return false;
300
		}
301
302
		// If we are already logged in and not forcing a relogin, return.
303
		if ( ! $force && $this->is_logged_in() &&
304
			$username === $this->_username &&
305
			$password === $this->_password ) {
306
			return true;
307
		}
308
309
		// Set the newly passed username and password.
310
		$this->_username = $username;
311
		$this->_password = $password;
312
313
		// Set up the login parameters.
314
		$params = array(
315
			'user[username]'     => $this->_username,
316
			'user[password]'     => $this->_password,
317
			'authenticity_token' => $this->_fetch_token(),
318
		);
319
320
		$args = array(
321
			'method' => 'POST',
322
			'body'   => $params,
323
		);
324
325
		// Try to sign in.
326
		$this->_request( '/users/sign_in', $args );
327
328
		// Can we load the bookmarklet to make sure we're logged in?
329
		$response = $this->_request( '/bookmarklet' );
330
331
		// If the request isn't successful, we are not logged in correctly.
332
		if ( is_wp_error( $response ) || 200 !== $response->code ) {
333
			// Login failed.
334
			$this->_error( 'wp2d_api_login_failed', __( 'Login failed. Check your login details.', 'wp-to-diaspora' ), array( 'help_tab' => 'troubleshooting' ) );
335
			$this->logout();
336
			return false;
337
		}
338
339
		// Login succeeded.
340
		$this->_is_logged_in = true;
341
		return true;
342
	}
343
344
	/**
345
	 * Perform a logout, resetting all login info.
346
	 *
347
	 * @since 1.6.0
348
	 */
349
	public function logout() {
350
		$this->_is_logged_in = false;
351
		$this->_username = null;
352
		$this->_password = null;
353
		$this->_aspects = array();
354
		$this->_services = array();
355
	}
356
357
	/**
358
	 * Perform a deinitialisation, resetting all class variables.
359
	 *
360
	 * @since 1.7.0
361
	 */
362
	public function deinit() {
363
		$this->logout();
364
		$this->_last_error = null;
365
		$this->_token = null;
366
		$this->_cookies = array();
367
		$this->_last_request = null;
368
	}
369
370
	/**
371
	 * Post to diaspora*.
372
	 *
373
	 * @param string       $text       The text to post.
374
	 * @param array|string $aspects    The aspects to post to. Array or comma seperated ids.
375
	 * @param array        $extra_data Any extra data to be added to the post call.
376
	 * @return boolean|object Return the response data of the new diaspora* post if successfully posted, else false.
377
	 */
378
	public function post( $text, $aspects = 'public', $extra_data = array() ) {
379
		// Are we logged in?
380
		if ( ! $this->_check_login() ) {
381
			return false;
382
		}
383
384
		// Put the aspects into a clean array.
385
		$aspects = array_filter( WP2D_Helpers::str_to_arr( $aspects ) );
386
387
		// If no aspects have been selected or the public one is also included, choose public only.
388
		if ( empty( $aspects ) || in_array( 'public', $aspects ) ) {
389
			$aspects = 'public';
390
		}
391
392
		// Prepare post data.
393
		$post_data = array(
394
			'aspect_ids'     => $aspects,
395
			'status_message' => array(
396
				'text' => $text,
397
				'provider_display_name' => $this->provider,
398
			),
399
		);
400
401
		// Add any extra data to the post.
402
		if ( ! empty( $extra_data ) ) {
403
				$post_data += $extra_data;
404
		}
405
406
		// Check if we can use the new wp_json_encode function.
407
		$post_data = ( function_exists( 'wp_json_encode' ) )
408
			? wp_json_encode( $post_data )
409
			: json_encode( $post_data );
410
411
		$args = array(
412
			'method'  => 'POST',
413
			'body'    => $post_data,
414
			'headers' => array(
415
				'Accept'       => 'application/json',
416
				'Content-Type' => 'application/json',
417
				'X-CSRF-Token' => $this->_fetch_token(),
418
			),
419
		);
420
421
		// Submit the post.
422
		$response = $this->_request( '/status_messages', $args );
423
424
		if ( is_wp_error( $response ) ) {
425
			$this->_error( 'wp2d_api_post_failed', $response->get_error_message() );
426
			return false;
427
		}
428
429
		$diaspost = json_decode( $response->body );
430
		if ( 201 !== $response->code ) {
431
			$this->_error( 'wp2d_api_post_failed', ( isset( $diaspost->error ) ) ? $diaspost->error : _x( 'Unknown error occurred.', 'When an unknown error occurred in the WP2D_API object.', 'wp-to-diaspora' ) );
432
			return false;
433
		}
434
435
		// Add additional info to our diaspora post object.
436
		$diaspost->permalink = $this->get_pod_url( '/posts/' . $diaspost->guid );
437
438
		return $diaspost;
439
	}
440
441
	/**
442
	 * Delete a post or comment from diaspora*.
443
	 *
444
	 * @since 1.6.0
445
	 *
446
	 * @param string $what What to delete, 'post' or 'comment'.
447
	 * @param string $id The ID of the post or comment to delete.
448
	 * @return boolean If the deletion was successful.
449
	 */
450
	public function delete( $what, $id ) {
451
		// Are we logged in?
452
		if ( ! $this->_check_login() ) {
453
			return false;
454
		}
455
456
		// For now, only deleting posts and comments is allowed.
457
		if ( ! in_array( $what, array( 'post', 'comment' ) ) ) {
458
			$this->_error( 'wp2d_api_delete_failed', __( 'You can only delete posts and comments.', 'wp-to-diaspora' ) );
459
			return false;
460
		}
461
462
		$args = array(
463
			'method'  => 'DELETE',
464
			'headers' => array(
465
				'Accept'       => 'application/json',
466
				'Content-Type' => 'application/json',
467
				'X-CSRF-Token' => $this->_fetch_token(),
468
			),
469
		);
470
471
		// Try to delete the post or comment.
472
		$response = $this->_request( '/' . $what . 's/' . $id, $args );
473
474
		$error_message = '';
0 ignored issues
show
Unused Code introduced by
$error_message 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...
475
476
		if ( is_wp_error( $response ) ) {
477
			$error_message = $response->get_error_message();
478
		} else {
479
			switch ( $response->code ) {
480
				case 204:
481
					return true;
482
				case 404:
483
					$error_message = ( 'post' === $what )
484
						? __( 'The post you tried to delete does not exist.', 'wp-to-diaspora' )
485
						: __( 'The comment you tried to delete does not exist.', 'wp-to-diaspora' );
486
					break;
487
488
				// Due to diaspora* returning a proper 403 when trying to delete a foreign comment
489
				// but returning a 500 when trying to delete a foreign post, this needs some special attention.
490
				case 403:
491
					if ( 'comment' === $what ) {
492
						$error_message = __( 'The comment you tried to delete does not belong to you.', 'wp-to-diaspora' );
493
						break;
494
					}
495
					// Fall through...
496
				case 500:
497
					if ( 'post' === $what ) {
498
						$error_message = __( 'The post you tried to delete does not belong to you.', 'wp-to-diaspora' );
499
						break;
500
					}
501
					// Fall through...
502
				default:
503
					$error_message = _x( 'Unknown error occurred.', 'When an unknown error occurred in the WP2D_API object.', 'wp-to-diaspora' );
504
					break;
505
			}
506
		}
507
508
		$this->_error( 'wp2d_api_delete_' . $what . '_failed', $error_message );
509
		return false;
510
	}
511
512
	/**
513
	 * Get the list of aspects.
514
	 *
515
	 * @param boolean $force Force to fetch new aspects.
516
	 * @return array Array of aspect objects.
517
	 */
518
	public function get_aspects( $force = false ) {
519
		$this->_aspects = $this->_get_aspects_services( 'aspects', $this->_aspects, $force );
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->_get_aspects_serv...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...
520
		return ( is_array( $this->_aspects ) ) ? $this->_aspects : false;
521
	}
522
523
	/**
524
	 * Get the list of connected services.
525
	 *
526
	 * @param boolean $force Force to fetch new connected services.
527
	 * @return array Array of service objects.
528
	 */
529
	public function get_services( $force = false ) {
530
		$this->_services = $this->_get_aspects_services( 'services', $this->_services, $force );
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->_get_aspects_serv...his->_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...
531
		return ( is_array( $this->_services ) ) ? $this->_services : false;
532
	}
533
534
	/**
535
	 * Get the list of aspects or connected services.
536
	 *
537
	 * @param string  $type  Type of list to get.
538
	 * @param array   $list  The current list of items.
539
	 * @param boolean $force Force to fetch new list.
540
	 * @return boolean Was the list fetched successfully?
541
	 */
542
	private function _get_aspects_services( $type, $list, $force ) {
543
		if ( ! $this->_check_login() ) {
544
			return false;
545
		}
546
547
		// Fetch the new list if the current list is empty or a reload is forced.
548
		if ( empty( $list ) || (bool) $force ) {
549
			$response = $this->_request( '/bookmarklet' );
550
551
			if ( is_wp_error( $response ) || 200 !== $response->code ) {
552
				switch ( $type ) {
553
					case 'aspects':
554
						$this->_error( 'wp2d_api_getting_aspects_failed', __( 'Error loading aspects.', 'wp-to-diaspora' ) );
555
						break;
556
					case 'services':
557
						$this->_error( 'wp2d_api_getting_services_failed', __( 'Error loading services.', 'wp-to-diaspora' ) );
558
						break;
559
					default:
560
						$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' ) );
561
						break;
562
				}
563
				return false;
564
			}
565
566
			// Load the aspects or services.
567
			if ( is_array( $raw_list = json_decode( $this->_parse_regex( $type, $response->body ) ) ) ) {
568
				// In case this fetch is forced, empty the list.
569
				$list = array();
570
571
				switch ( $type ) {
572
					case 'aspects':
573
						// Add the 'public' aspect, as it's global and not user specific.
574
						$list['public'] = __( 'Public', 'wp-to-diaspora' );
575
576
						// Add all user specific aspects.
577
						foreach ( $raw_list as $aspect ) {
578
							$list[ $aspect->id ] = $aspect->name;
579
						}
580
						break;
581
					case 'services':
582
						foreach ( $raw_list as $service ) {
583
							$list[ $service ] = ucfirst( $service );
584
						}
585
						break;
586
				}
587
			}
588
		}
589
		return $list;
590
	}
591
592
	/**
593
	 * Send an http(s) request via WP_HTTP API.
594
	 *
595
	 * @see WP_Http::request()
596
	 *
597
	 * @param string $url  The URL to request.
598
	 * @param array  $args Arguments to be posted with the request.
599
	 * @return object An object containing details about this request.
600
	 */
601
	private function _request( $url, $args = array() ) {
602
		// Prefix the full pod URL if necessary.
603
		if ( 0 === strpos( $url, '/' ) ) {
604
			$url = $this->get_pod_url( $url );
605
		}
606
607
		// Disable redirections so we can verify HTTP response codes.
608
		$defaults = array(
609
			'redirection' => 0,
610
			'sslverify'   => true,
611
			'timeout'     => 60,
612
			'method'      => 'GET',
613
		);
614
615
		// If the certificate bundle has been downloaded manually, use that instead.
616
		// NOTE: This should actually never be necessary, it's a fallback!
617
		if ( file_exists( WP2D_DIR . '/cacert.pem' ) ) {
618
			$defaults['sslcertificates'] = WP2D_DIR . '/cacert.pem';
619
		}
620
621
		// Set the correct cookie.
622
		if ( ! empty( $this->_cookies ) ) {
623
			$defaults['cookies'] = $this->_cookies;
624
		}
625
626
		$args = wp_parse_args( $args, $defaults );
627
628
		// Get the response from the WP_HTTP request.
629
		$response = wp_remote_request( $url, $args );
630
631
		if ( is_wp_error( $response ) ) {
632
			$this->_last_error = $response;
633
			return $response;
634
		}
635
636
		// Get the headers and the html response.
637
		$headers = wp_remote_retrieve_headers( $response );
638
		$body    = wp_remote_retrieve_body( $response );
639
640
		// Remember this request.
641
		$this->_last_request = new stdClass();
642
		$this->_last_request->response = $response;
643
		$this->_last_request->headers  = $headers;
644
		$this->_last_request->body     = $body;
645
		$this->_last_request->message  = wp_remote_retrieve_response_message( $response );
646
		$this->_last_request->code     = wp_remote_retrieve_response_code( $response );
647
648
		// Save the new token.
649
		if ( $token = $this->_parse_regex( 'token', $body ) ) {
650
			$this->_token = $token;
651
		}
652
653
		// Save the latest cookies.
654
		if ( isset( $response['cookies'] ) ) {
655
			$this->_cookies = $response['cookies'];
656
		}
657
658
		// Return the last request details.
659
		return $this->_last_request;
660
	}
661
662
	/**
663
	 * Helper method to set the last occurred error.
664
	 *
665
	 * @see WP_Error::__construct()
666
	 * @since 1.6.0
667
	 *
668
	 * @param  string|int $code    Error code.
669
	 * @param  string     $message Error message.
670
	 * @param  mixed      $data    Error data.
671
	 */
672
	private function _error( $code, $message, $data = '' ) {
673
		// Always add the code and message of the last request.
674
		$data = array_merge( array_filter( (array) $data ), array(
675
			'code'    => ( isset( $this->_last_request->code ) ) ? $this->_last_request->code : null,
676
			'message' => ( isset( $this->_last_request->message ) ) ? $this->_last_request->message : null,
677
		) );
678
		$this->_last_error = new WP_Error( $code, $message, $data );
679
	}
680
681
	/**
682
	 * Parse the regex and return the found string.
683
	 *
684
	 * @param string $regex   Shorthand of a saved regex or a custom regex.
685
	 * @param string $content Text to parse the regex with.
686
	 * @return string The found string, or an empty string.
687
	 */
688
	private function _parse_regex( $regex, $content ) {
689
		// Use a shorthand regex if available.
690
		if ( array_key_exists( $regex, $this->_regexes ) ) {
691
			$regex = $this->_regexes[ $regex ];
692
		}
693
694
		preg_match( $regex, $content, $matches );
695
		return trim( array_pop( $matches ) );
696
	}
697
}
698