Completed
Push — master ( 8004a5...3cf298 )
by adam
02:47
created

MediawikiApi::createRetryHandler()   C

Complexity

Conditions 9
Paths 1

Size

Total Lines 60
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 26.496

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 60
ccs 10
cts 25
cp 0.4
rs 6.8358
cc 9
eloc 34
nc 1
nop 0
crap 26.496

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Mediawiki\Api;
4
5
use GuzzleHttp\Client;
6
use GuzzleHttp\Exception\ConnectException;
7
use GuzzleHttp\Exception\RequestException;
8
use GuzzleHttp\Handler\CurlHandler;
9
use GuzzleHttp\HandlerStack;
10
use GuzzleHttp\Middleware;
11
use GuzzleHttp\Promise\PromiseInterface;
12
use GuzzleHttp\Psr7\Request as Psr7Request;
13
use GuzzleHttp\Psr7\Response as Psr7Response;
14
use InvalidArgumentException;
15
use Psr\Http\Message\ResponseInterface;
16
use Psr\Log\LoggerAwareInterface;
17
use Psr\Log\LoggerInterface;
18
use Psr\Log\LogLevel;
19
use Psr\Log\NullLogger;
20
21
/**
22
 * @author Addshore
23
 */
24
class MediawikiApi implements LoggerAwareInterface {
25
26
	/**
27
	 * @var Client|null Should be accessed through getClient
28
	 */
29
	private $client = null;
30
31
	/**
32
	 * @var bool|string
33
	 */
34
	private $isLoggedIn;
35
36
	/**
37
	 * @var MediawikiSession
38
	 */
39
	private $session;
40
41
	/**
42
	 * @var string
43
	 */
44
	private $version;
45
46
	/**
47
	 * @var LoggerInterface
48
	 */
49
	private $logger;
50
51 23
	/**
52 23
	 * @param string $apiUrl The API Url
53 4
	 * @param Client|null $client Guzzle Client
54
	 * @param MediawikiSession|null $session Inject a custom session here
55 19
	 */
56 3
	public function __construct( $apiUrl, Client $client = null, MediawikiSession $session = null ) {
57 19
		if( !is_string( $apiUrl ) ) {
58
			throw new InvalidArgumentException( '$apiUrl must be a string' );
59
		}
60 19
		if( $session === null ) {
61 19
			$session = new MediawikiSession( $this );
62 19
		}
63
		// Warn people about a badly configured Client
64 19
		if( $client !== null ) {
65 19
			if( $client->getConfig( 'cookies' ) === false ) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
66 19
				// TODO Somehow flag that things will not work?
67
			}
68 19
		}
69 19
70
		$this->apiUrl = $apiUrl;
0 ignored issues
show
Bug introduced by
The property apiUrl does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
71
		$this->client = $client;
72
		$this->session = $session;
73
74
		$this->logger = new NullLogger();
75
	}
76
77
	/**
78
	 * @return Client
79
	 */
80
	private function getClient() {
81
		if( $this->client === null ) {
82
			$handlerStack = HandlerStack::create( new CurlHandler() );
83
			$handlerStack->push( Middleware::retry( $this->createRetryHandler() ) );
84
			$this->client = new Client( array(
85
				'cookies' => true,
86
				'handler' => $handlerStack,
87
			) );
88
		}
89
		return $this->client;
90
	}
91
92
	private function createRetryHandler() {
93
		return function (
94
			$retries,
95
			Psr7Request $request,
96
			Psr7Response $response = null,
97
			RequestException $exception = null
98
		) {
99
			// Don't retry if we have run out of retries
100
			if ( $retries >= 5 ) {
101
				return false;
102
			}
103
104
			$shouldRetry = false;
105
106
			// Retry connection exceptions
107
			if( $exception instanceof ConnectException ) {
108
				$shouldRetry = true;
109
			}
110
111
			if( $response ) {
112
				$headers = $response->getHeaders();
113
114
				// Retry on server errors
115
				if( $response->getStatusCode() >= 500 ) {
116
					$shouldRetry = true;
117
				}
118
119
				// Retry if we have a response with an API error worth retrying
120
				if ( array_key_exists( 'mediawiki-api-error', $headers ) ) {
121
					if ( in_array(
122
						$headers['mediawiki-api-error'],
123
						array(
124
							'ratelimited',
125
							'readonly',
126
							'internal_api_error_DBQueryError',
127
						)
128
					) ) {
129
						$shouldRetry = true;
130
					}
131
				}
132 8
			}
133 8
134 8
			// Log if we are retrying
135 8
			if( $shouldRetry ) {
136 8
				$this->logger->warning(
137
					sprintf(
138 8
						'Retrying %s %s %s/5, %s',
139
						$request->getMethod(),
140
						$request->getUri(),
141
						$retries + 1,
142
						$response ? 'status code: ' . $response->getStatusCode() :
143
							$exception->getMessage()
0 ignored issues
show
Bug introduced by
It seems like $exception is not always an object, but can also be of type null. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
144
					),
145
					[ $request->getHeader( 'Host' )[0] ]
146
				);
147
			}
148 8
149 8
			return $shouldRetry;
150 8
		};
151 8
	}
152 8
153
	/**
154 8
	 * Sets a logger instance on the object
155
	 *
156
	 * @since 1.1
157
	 *
158
	 * @param LoggerInterface $logger
159
	 *
160
	 * @return null
161
	 */
162
	public function setLogger( LoggerInterface $logger ) {
163 16
		$this->logger = $logger;
164 16
		$this->session->setLogger( $logger );
165
	}
166 16
167 16
	/**
168
	 * @since 2.0
169 14
	 *
170
	 * @param Request $request
171
	 *
172
	 * @return PromiseInterface
173
	 *         Normally promising an array, though can be mixed (json_decode result)
174
	 *         Can throw UsageExceptions or RejectionExceptions
175
	 */
176
	public function getRequestAsync( Request $request ) {
177
		$promise = $this->getClient()->getAsync(
178
			$this->apiUrl,
179
			$this->getClientRequestOptions( $request, 'query' )
180 16
		);
181
182 16
		return $promise->then( function( ResponseInterface $response ) {
183 16
			return call_user_func( array( $this, 'decodeResponse' ), $response );
184 16
		} );
185
	}
186
187
	/**
188
	 * @since 2.0
189
	 *
190 16
	 * @param Request $request
191
	 *
192 16
	 * @return PromiseInterface
193 16
	 *         Normally promising an array, though can be mixed (json_decode result)
194
	 *         Can throw UsageExceptions or RejectionExceptions
195
	 */
196 16
	public function postRequestAsync( Request $request ) {
197 16
		$promise = $this->getClient()->postAsync(
198 16
			$this->apiUrl,
199
			$this->getClientRequestOptions( $request, 'form_params' )
200
		);
201 16
202
		return $promise->then( function( ResponseInterface $response ) {
203
			return call_user_func( array( $this, 'decodeResponse' ), $response );
204
		} );
205
	}
206
207 16
	/**
208 16
	 * @since 0.2
209
	 *
210
	 * @param Request $request
211
	 *
212
	 * @return mixed Normally an array
213 16
	 */
214
	public function getRequest( Request $request ) {
215
		$response = $this->getClient()->get(
216
			$this->apiUrl,
217
			$this->getClientRequestOptions( $request, 'query' )
218
		);
219
220 16
		return $this->decodeResponse( $response );
221 16
	}
222 2
223 2
	/**
224 2
	 * @since 0.2
225
	 *
226 2
	 * @param Request $request
227
	 *
228 14
	 * @return mixed Normally an array
229
	 */
230
	public function postRequest( Request $request ) {
231
		$response = $this->getClient()->post(
232
			$this->apiUrl,
233
			$this->getClientRequestOptions( $request, 'form_params' )
234
		);
235 16
236 16
		return $this->decodeResponse( $response );
237
	}
238
239
	/**
240
	 * @param ResponseInterface $response
241
	 *
242
	 * @return mixed
243
	 * @throws UsageException
244
	 */
245
	private function decodeResponse( ResponseInterface $response ) {
246
		$resultArray = json_decode( $response->getBody(), true );
247 2
248 2
		$this->logWarnings( $resultArray );
249
		$this->throwUsageExceptions( $resultArray );
250
251 2
		return $resultArray;
252 2
	}
253 2
254
	/**
255 2
	 * @param Request $request
256
	 * @param string $paramsKey either 'query' or 'form_params'
257
	 *
258
	 * @throws RequestException
259 2
	 *
260 2
	 * @return array as needed by ClientInterface::get and ClientInterface::post
261 2
	 */
262 2
	private function getClientRequestOptions( Request $request, $paramsKey ) {
263 2
		return array(
264 1
			$paramsKey => array_merge( $request->getParams(), array( 'format' => 'json' ) ),
265 1
			'headers' => array_merge( $this->getDefaultHeaders(), $request->getHeaders() ),
266
		);
267
	}
268 1
269 1
	/**
270
	 * @return array
271
	 */
272
	private function getDefaultHeaders() {
273
		return array(
274
			'User-Agent' => $this->getUserAgent(),
275
		);
276
	}
277
278 1
	private function getUserAgent() {
279 1
		$loggedIn = $this->isLoggedin();
280
		if( $loggedIn ) {
281 1
			return 'addwiki-mediawiki-client/' . $loggedIn;
282
		}
283
		return 'addwiki-mediawiki-client';
284
	}
285
286
	/**
287 1
	 * @param $result
288
	 */
289
	private function logWarnings( $result ) {
290
		if( is_array( $result ) && array_key_exists( 'warnings', $result ) ) {
291
			foreach( $result['warnings'] as $module => $warningData ) {
292
				$this->logger->log( LogLevel::WARNING, $module . ': ' . $warningData['*'], array( 'data' => $warningData ) );
293 1
			}
294
		}
295
	}
296
297
	/**
298
	 * @param array $result
299 1
	 *
300
	 * @throws UsageException
301
	 */
302
	private function throwUsageExceptions( $result ) {
303
		if( is_array( $result ) && array_key_exists( 'error', $result ) ) {
304
			throw new UsageException(
305 1
				$result['error']['code'],
306
				$result['error']['info'],
307
				$result
308
			);
309
		}
310
	}
311 1
312
	/**
313
	 * @since 0.1
314
	 *
315
	 * @return bool|string false or the name of the current user
316
	 */
317 1
	public function isLoggedin() {
318
		return $this->isLoggedIn;
319
	}
320
321
	/**
322
	 * @since 0.1
323 1
	 *
324
	 * @param ApiUser $apiUser
325
	 *
326
	 * @throws UsageException
327
	 * @return bool success
328
	 */
329 1
	public function login( ApiUser $apiUser ) {
330 1
		$this->logger->log( LogLevel::DEBUG, 'Logging in' );
331 1
332 1
		$credentials = array(
333
			'lgname' => $apiUser->getUsername(),
334 1
			'lgpassword' => $apiUser->getPassword(),
335 1
		);
336
337
		if( !is_null( $apiUser->getDomain() ) ) {
338
			$credentials['lgdomain'] = $apiUser->getDomain();
339
		}
340
341
		$result = $this->postRequest( new SimpleRequest( 'login', $credentials ) );
342 2
		if ( $result['login']['result'] == "NeedToken" ) {
343 2
			$result = $this->postRequest( new SimpleRequest( 'login', array_merge( array( 'lgtoken' => $result['login']['token'] ), $credentials) ) );
344 2
		}
345 2
		if ( $result['login']['result'] == "Success" ) {
346 1
			$this->isLoggedIn = $apiUser->getUsername();
347 1
			return true;
348 1
		}
349
350 1
		$this->isLoggedIn = false;
351
		$this->throwLoginUsageException( $result );
352
		return false;
353
	}
354
355
	/**
356
	 * @param array $result
357
	 *
358
	 * @throws UsageException
359
	 */
360
	private function throwLoginUsageException( $result ) {
361
		$loginResult = $result['login']['result'];
362
		switch( $loginResult ) {
363
			case 'Illegal';
364
				throw new UsageException(
365
					'login-' . $loginResult,
366
					'You provided an illegal username',
367
					$result
368 1
				);
369 1
			case 'NotExists';
370 1
				throw new UsageException(
371
					'login-' . $loginResult,
372
					'The username you provided doesn\'t exist',
373
					$result
374
				);
375 4
			case 'WrongPass';
376 4
				throw new UsageException(
377 4
					'login-' . $loginResult,
378 4
					'The password you provided is incorrect',
379 4
					$result
380 4
				);
381 4
			case 'WrongPluginPass';
382 4
				throw new UsageException(
383 4
					'login-' . $loginResult,
384
					'An authentication plugin rather than MediaWiki itself rejected the password',
385 4
					$result
386 4
				);
387 4
			case 'CreateBlocked';
388 4
				throw new UsageException(
389
					'login-' . $loginResult,
390
					'The wiki tried to automatically create a new account for you, but your IP address has been blocked from account creation',
391
					$result
392
				);
393
			case 'Throttled';
394
				throw new UsageException(
395
					'login-' . $loginResult,
396
					'You\'ve logged in too many times in a short time.',
397
					$result
398
				);
399
			case 'Blocked';
400
				throw new UsageException(
401
					'login-' . $loginResult,
402
					'User is blocked',
403
					$result
404
				);
405
			case 'NeedToken';
406
				throw new UsageException(
407
					'login-' . $loginResult,
408
					'Either you did not provide the login token or the sessionid cookie.',
409
					$result
410
				);
411
			default:
412
				throw new UsageException(
413
					'login-' . $loginResult,
414
					$loginResult,
415
					$result
416
				);
417
		}
418
	}
419
420
	/**
421
	 * @since 0.1
422
	 * @return bool success
423
	 */
424
	public function logout() {
425
		$this->logger->log( LogLevel::DEBUG, 'Logging out' );
426
		$result = $this->postRequest( new SimpleRequest( 'logout' ) );
427
		if( $result === array() ) {
428
			$this->isLoggedIn = false;
429
			$this->clearTokens();
430
			return true;
431
		}
432
		return false;
433
	}
434
435
	/**
436
	 * @since 0.1
437
	 *
438
	 * @param string $type
439
	 *
440
	 * @return string
441
	 */
442
	public function getToken( $type = 'csrf' ) {
443
		return $this->session->getToken( $type );
444
	}
445
446
	/**
447
	 * @since 0.1
448
	 * Clears all tokens stored by the api
449
	 */
450
	public function clearTokens() {
451
		$this->session->clearTokens();
452
	}
453
454
	/**
455
	 * @return string
456
	 */
457
	public function getVersion(){
458
		if( !isset( $this->version ) ) {
459
			$result = $this->getRequest( new SimpleRequest( 'query', array(
460
				'meta' => 'siteinfo',
461
				'continue' => '',
462
			) ) );
463
			preg_match(
464
				'/\d+(?:\.\d+)+/',
465
				$result['query']['general']['generator'],
466
				$versionParts
467
			);
468
			$this->version = $versionParts[0];
469
		}
470
		return $this->version;
471
	}
472
473
}
474