Completed
Push — master ( 749236...2c4679 )
by adam
03:25
created

MediawikiApi   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 416
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 13

Test Coverage

Coverage 61.2%

Importance

Changes 32
Bugs 1 Features 13
Metric Value
wmc 44
c 32
b 1
f 13
lcom 1
cbo 13
dl 0
loc 416
ccs 112
cts 183
cp 0.612
rs 8.3396

22 Methods

Rating   Name   Duplication   Size   Complexity  
A newFromApiEndpoint() 0 3 1
A newFromPage() 0 3 1
A __construct() 0 14 3
A getClient() 0 15 2
A setLogger() 0 4 1
A getRequestAsync() 0 10 1
A postRequestAsync() 0 10 1
A getRequest() 0 8 1
A postRequest() 0 8 1
A decodeResponse() 0 8 1
A getClientRequestOptions() 0 6 1
A getDefaultHeaders() 0 5 1
A getUserAgent() 0 7 2
A logWarnings() 0 7 4
A throwUsageExceptions() 0 9 3
A isLoggedin() 0 3 1
B login() 0 25 4
C throwLoginUsageException() 0 59 9
A logout() 0 10 2
A getToken() 0 3 1
A clearTokens() 0 3 1
A getVersion() 0 15 2

How to fix   Complexity   

Complex Class

Complex classes like MediawikiApi 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 MediawikiApi, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Mediawiki\Api;
4
5
use GuzzleHttp\Client;
6
use GuzzleHttp\Exception\RequestException;
7
use GuzzleHttp\Handler\CurlHandler;
8
use GuzzleHttp\HandlerStack;
9
use GuzzleHttp\Promise\PromiseInterface;
10
use InvalidArgumentException;
11
use Mediawiki\Api\Guzzle\MiddlewareFactory;
12
use Psr\Http\Message\ResponseInterface;
13
use Psr\Log\LoggerAwareInterface;
14
use Psr\Log\LoggerInterface;
15
use Psr\Log\LogLevel;
16
use Psr\Log\NullLogger;
17
18
/**
19
 * Main class for this library
20
 *
21
 * @since 0.1
22
 *
23
 * @author Addshore
24
 */
25
class MediawikiApi implements LoggerAwareInterface {
26
27
	/**
28
	 * @var Client|null Should be accessed through getClient
29
	 */
30
	private $client = null;
31
32
	/**
33
	 * @var bool|string
34
	 */
35
	private $isLoggedIn;
36
37
	/**
38
	 * @var MediawikiSession
39
	 */
40
	private $session;
41
42
	/**
43
	 * @var string
44
	 */
45
	private $version;
46
47
	/**
48
	 * @var LoggerInterface
49
	 */
50
	private $logger;
51
52
	/**
53
	 * @since 2.0.0
54
	 *
55
	 * @param string $apiEndpoint e.g. https://en.wikipedia.org/w/api.php
56
	 *
57 23
	 * @return self returns a MediawikiApi instance using $apiEndpoint
58 23
	 */
59 4
	public static function newFromApiEndpoint( $apiEndpoint ) {
60
		return new self( $apiEndpoint );
61 19
	}
62 19
63 19
	/**
64
	 * @since 2.0.0
65 19
	 *
66 16
	 * @param string $url e.g. https://en.wikipedia.org OR https://de.wikipedia.org/wiki/Berlin
67
	 *
68
	 * @return self returns a MediawikiApi instance using the apiEndpoint provided by the RSD
69 16
	 *              file accessible on all Mediawiki pages
70
	 *
71 19
	 * @see https://en.wikipedia.org/wiki/Really_Simple_Discovery
72 19
	 */
73 19
	public static function newFromPage( $url ) {
0 ignored issues
show
Unused Code introduced by
The parameter $url is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
74
		//TODO implement me
75 19
	}
76 19
77
	/**
78
	 * @access private
79
	 *
80
	 * @param string $apiUrl The API Url
81 16
	 * @param Client|null $client Guzzle Client
82 16
	 * @param MediawikiSession|null $session Inject a custom session here
83
	 */
84
	public function __construct( $apiUrl, Client $client = null, MediawikiSession $session = null ) {
85
		if( !is_string( $apiUrl ) ) {
86
			throw new InvalidArgumentException( '$apiUrl must be a string' );
87
		}
88
		if( $session === null ) {
89
			$session = new MediawikiSession( $this );
90
		}
91
92
		$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...
93
		$this->client = $client;
94 16
		$this->session = $session;
95
96
		$this->logger = new NullLogger();
97
	}
98
99
	/**
100
	 * @return Client
101
	 */
102
	private function getClient() {
103
		if( $this->client === null ) {
104
			$middlewareFactory = new MiddlewareFactory();
105
			$middlewareFactory->setLogger( $this->logger );
106
107
			$handlerStack = HandlerStack::create( new CurlHandler() );
108
			$handlerStack->push( $middlewareFactory->retry() );
109
110
			$this->client = new Client( array(
111
				'cookies' => true,
112
				'handler' => $handlerStack,
113
			) );
114
		}
115
		return $this->client;
116
	}
117
118
	/**
119
	 * Sets a logger instance on the object
120
	 *
121
	 * @since 1.1
122
	 *
123
	 * @param LoggerInterface $logger
124
	 *
125
	 * @return null
126
	 */
127
	public function setLogger( LoggerInterface $logger ) {
128
		$this->logger = $logger;
129
		$this->session->setLogger( $logger );
130
	}
131
132
	/**
133
	 * @since 2.0
134
	 *
135
	 * @param Request $request
136
	 *
137
	 * @return PromiseInterface
138
	 *         Normally promising an array, though can be mixed (json_decode result)
139
	 *         Can throw UsageExceptions or RejectionExceptions
140
	 */
141
	public function getRequestAsync( Request $request ) {
142
		$promise = $this->getClient()->getAsync(
143
			$this->apiUrl,
144
			$this->getClientRequestOptions( $request, 'query' )
145
		);
146
147
		return $promise->then( function( ResponseInterface $response ) {
148
			return call_user_func( array( $this, 'decodeResponse' ), $response );
149
		} );
150
	}
151
152
	/**
153
	 * @since 2.0
154
	 *
155
	 * @param Request $request
156
	 *
157
	 * @return PromiseInterface
158 8
	 *         Normally promising an array, though can be mixed (json_decode result)
159 8
	 *         Can throw UsageExceptions or RejectionExceptions
160 8
	 */
161 8
	public function postRequestAsync( Request $request ) {
162 8
		$promise = $this->getClient()->postAsync(
163
			$this->apiUrl,
164 8
			$this->getClientRequestOptions( $request, 'form_params' )
165
		);
166
167
		return $promise->then( function( ResponseInterface $response ) {
168
			return call_user_func( array( $this, 'decodeResponse' ), $response );
169
		} );
170
	}
171
172
	/**
173
	 * @since 0.2
174 8
	 *
175 8
	 * @param Request $request
176 8
	 *
177 8
	 * @return mixed Normally an array
178 8
	 */
179
	public function getRequest( Request $request ) {
180 8
		$response = $this->getClient()->get(
181
			$this->apiUrl,
182
			$this->getClientRequestOptions( $request, 'query' )
183
		);
184
185
		return $this->decodeResponse( $response );
186
	}
187
188
	/**
189 16
	 * @since 0.2
190 16
	 *
191
	 * @param Request $request
192 16
	 *
193 16
	 * @return mixed Normally an array
194
	 */
195 14
	public function postRequest( Request $request ) {
196
		$response = $this->getClient()->post(
197
			$this->apiUrl,
198
			$this->getClientRequestOptions( $request, 'form_params' )
199
		);
200
201
		return $this->decodeResponse( $response );
202
	}
203
204
	/**
205
	 * @param ResponseInterface $response
206 16
	 *
207
	 * @return mixed
208 16
	 * @throws UsageException
209 16
	 */
210 16
	private function decodeResponse( ResponseInterface $response ) {
211
		$resultArray = json_decode( $response->getBody(), true );
212
213
		$this->logWarnings( $resultArray );
214
		$this->throwUsageExceptions( $resultArray );
215
216 16
		return $resultArray;
217
	}
218 16
219 16
	/**
220
	 * @param Request $request
221
	 * @param string $paramsKey either 'query' or 'form_params'
222 16
	 *
223 16
	 * @throws RequestException
224 16
	 *
225
	 * @return array as needed by ClientInterface::get and ClientInterface::post
226
	 */
227 16
	private function getClientRequestOptions( Request $request, $paramsKey ) {
228
		return array(
229
			$paramsKey => array_merge( $request->getParams(), array( 'format' => 'json' ) ),
230
			'headers' => array_merge( $this->getDefaultHeaders(), $request->getHeaders() ),
231
		);
232
	}
233 16
234 16
	/**
235
	 * @return array
236
	 */
237
	private function getDefaultHeaders() {
238
		return array(
239 16
			'User-Agent' => $this->getUserAgent(),
240
		);
241
	}
242
243
	private function getUserAgent() {
244
		$loggedIn = $this->isLoggedin();
245
		if( $loggedIn ) {
246 16
			return 'addwiki-mediawiki-client/' . $loggedIn;
247 16
		}
248 2
		return 'addwiki-mediawiki-client';
249 2
	}
250 2
251
	/**
252 2
	 * @param $result
253
	 */
254 14
	private function logWarnings( $result ) {
255
		if( is_array( $result ) && array_key_exists( 'warnings', $result ) ) {
256
			foreach( $result['warnings'] as $module => $warningData ) {
257
				$this->logger->log( LogLevel::WARNING, $module . ': ' . $warningData['*'], array( 'data' => $warningData ) );
258
			}
259
		}
260
	}
261 16
262 16
	/**
263
	 * @param array $result
264
	 *
265
	 * @throws UsageException
266
	 */
267
	private function throwUsageExceptions( $result ) {
268
		if( is_array( $result ) && array_key_exists( 'error', $result ) ) {
269
			throw new UsageException(
270
				$result['error']['code'],
271
				$result['error']['info'],
272
				$result
273 2
			);
274 2
		}
275
	}
276
277 2
	/**
278 2
	 * @since 0.1
279 2
	 *
280
	 * @return bool|string false or the name of the current user
281 2
	 */
282
	public function isLoggedin() {
283
		return $this->isLoggedIn;
284
	}
285 2
286 2
	/**
287 2
	 * @since 0.1
288 2
	 *
289 2
	 * @param ApiUser $apiUser
290 1
	 *
291 1
	 * @throws UsageException
292
	 * @return bool success
293
	 */
294 1
	public function login( ApiUser $apiUser ) {
295 1
		$this->logger->log( LogLevel::DEBUG, 'Logging in' );
296
297
		$credentials = array(
298
			'lgname' => $apiUser->getUsername(),
299
			'lgpassword' => $apiUser->getPassword(),
300
		);
301
302
		if( !is_null( $apiUser->getDomain() ) ) {
303
			$credentials['lgdomain'] = $apiUser->getDomain();
304 1
		}
305 1
306
		$result = $this->postRequest( new SimpleRequest( 'login', $credentials ) );
307 1
		if ( $result['login']['result'] == "NeedToken" ) {
308
			$result = $this->postRequest( new SimpleRequest( 'login', array_merge( array( 'lgtoken' => $result['login']['token'] ), $credentials) ) );
309
		}
310
		if ( $result['login']['result'] == "Success" ) {
311
			$this->isLoggedIn = $apiUser->getUsername();
312
			return true;
313 1
		}
314
315
		$this->isLoggedIn = false;
316
		$this->throwLoginUsageException( $result );
317
		return false;
318
	}
319 1
320
	/**
321
	 * @param array $result
322
	 *
323
	 * @throws UsageException
324
	 */
325 1
	private function throwLoginUsageException( $result ) {
326
		$loginResult = $result['login']['result'];
327
		switch( $loginResult ) {
328
			case 'Illegal';
329
				throw new UsageException(
330
					'login-' . $loginResult,
331 1
					'You provided an illegal username',
332
					$result
333
				);
334
			case 'NotExists';
335
				throw new UsageException(
336
					'login-' . $loginResult,
337 1
					'The username you provided doesn\'t exist',
338
					$result
339
				);
340
			case 'WrongPass';
341
				throw new UsageException(
342
					'login-' . $loginResult,
343 1
					'The password you provided is incorrect',
344
					$result
345
				);
346
			case 'WrongPluginPass';
347
				throw new UsageException(
348
					'login-' . $loginResult,
349 1
					'An authentication plugin rather than MediaWiki itself rejected the password',
350
					$result
351
				);
352
			case 'CreateBlocked';
353
				throw new UsageException(
354
					'login-' . $loginResult,
355 1
					'The wiki tried to automatically create a new account for you, but your IP address has been blocked from account creation',
356 1
					$result
357 1
				);
358 1
			case 'Throttled';
359
				throw new UsageException(
360 1
					'login-' . $loginResult,
361 1
					'You\'ve logged in too many times in a short time.',
362
					$result
363
				);
364
			case 'Blocked';
365
				throw new UsageException(
366
					'login-' . $loginResult,
367
					'User is blocked',
368 2
					$result
369 2
				);
370 2
			case 'NeedToken';
371 2
				throw new UsageException(
372 1
					'login-' . $loginResult,
373 1
					'Either you did not provide the login token or the sessionid cookie.',
374 1
					$result
375
				);
376 1
			default:
377
				throw new UsageException(
378
					'login-' . $loginResult,
379
					$loginResult,
380
					$result
381
				);
382
		}
383
	}
384
385
	/**
386
	 * @since 0.1
387
	 *
388
	 * @return bool success
389
	 */
390
	public function logout() {
391
		$this->logger->log( LogLevel::DEBUG, 'Logging out' );
392
		$result = $this->postRequest( new SimpleRequest( 'logout' ) );
393
		if( $result === array() ) {
394 1
			$this->isLoggedIn = false;
395 1
			$this->clearTokens();
396 1
			return true;
397
		}
398
		return false;
399
	}
400
401 4
	/**
402 4
	 * @since 0.1
403 4
	 *
404 4
	 * @param string $type
405 4
	 *
406 4
	 * @return string
407 4
	 */
408 4
	public function getToken( $type = 'csrf' ) {
409 4
		return $this->session->getToken( $type );
410
	}
411 4
412 4
	/**
413 4
	 * @since 0.1
414 4
	 *
415
	 * Clears all tokens stored by the api
416
	 */
417
	public function clearTokens() {
418
		$this->session->clearTokens();
419
	}
420
421
	/**
422
	 * @return string
423
	 */
424
	public function getVersion(){
425
		if( !isset( $this->version ) ) {
426
			$result = $this->getRequest( new SimpleRequest( 'query', array(
427
				'meta' => 'siteinfo',
428
				'continue' => '',
429
			) ) );
430
			preg_match(
431
				'/\d+(?:\.\d+)+/',
432
				$result['query']['general']['generator'],
433
				$versionParts
434
			);
435
			$this->version = $versionParts[0];
436
		}
437
		return $this->version;
438
	}
439
440
}
441