Completed
Push — master ( 3cf298...59bc3b )
by adam
05:40
created

MediawikiApi::postRequest()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

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