Completed
Push — multipart ( 0ececc...cc4413 )
by Thomas
05:48
created

MediawikiApi::getPostRequestEncoding()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 8
ccs 5
cts 5
cp 1
rs 9.4285
cc 3
eloc 5
nc 3
nop 1
crap 3
1
<?php
2
3
namespace Mediawiki\Api;
4
5
use GuzzleHttp\Client;
6
use GuzzleHttp\ClientInterface;
7
use GuzzleHttp\Exception\RequestException;
8
use GuzzleHttp\Promise\PromiseInterface;
9
use InvalidArgumentException;
10
use Mediawiki\Api\Guzzle\ClientFactory;
11
use Psr\Http\Message\ResponseInterface;
12
use Psr\Log\LoggerAwareInterface;
13
use Psr\Log\LoggerInterface;
14
use Psr\Log\LogLevel;
15
use Psr\Log\NullLogger;
16
use SimpleXMLElement;
17
18
/**
19
 * Main class for this library
20
 *
21
 * @since 0.1
22
 *
23
 * @author Addshore
24
 */
25
class MediawikiApi implements MediawikiApiInterface, LoggerAwareInterface {
26
27
	/**
28
	 * @var ClientInterface|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
	 * @var string
54
	 */
55
	private $apiUrl;
56
57
	/**
58
	 * @since 2.0.0
59
	 *
60
	 * @param string $apiEndpoint e.g. https://en.wikipedia.org/w/api.php
61
	 *
62
	 * @return self returns a MediawikiApi instance using $apiEndpoint
63
	 */
64
	public static function newFromApiEndpoint( $apiEndpoint ) {
65
		return new self( $apiEndpoint );
66
	}
67
68
	/**
69
	 * @since 2.0.0
70
	 *
71
	 * @param string $url e.g. https://en.wikipedia.org OR https://de.wikipedia.org/wiki/Berlin
72
	 *
73
	 * @return self returns a MediawikiApi instance using the apiEndpoint provided by the RSD
74
	 *              file accessible on all Mediawiki pages
75
	 *
76
	 * @see https://en.wikipedia.org/wiki/Really_Simple_Discovery
77
	 */
78 1
	public static function newFromPage( $url ) {
79 1
		$tempClient = new Client( array( 'headers' => array( 'User-Agent' => 'addwiki-mediawiki-client' ) ) );
80 1
		$pageXml = new SimpleXMLElement( $tempClient->get( $url )->getBody() );
81 1
		$rsdElement = $pageXml->xpath( 'head/link[@type="application/rsd+xml"][@href]' );
82 1
		$rsdXml = new SimpleXMLElement( $tempClient->get( $rsdElement[0]->attributes()['href'] )->getBody() );
83
		return self::newFromApiEndpoint( $rsdXml->service->apis->api->attributes()->apiLink->__toString() );
84
	}
85
86
	/**
87
	 * @param string $apiUrl The API Url
88
	 * @param ClientInterface|null $client Guzzle Client
89
	 * @param MediawikiSession|null $session Inject a custom session here
90
	 */
91 24
	public function __construct( $apiUrl, ClientInterface $client = null, MediawikiSession $session = null ) {
92 24
		if( !is_string( $apiUrl ) ) {
93 4
			throw new InvalidArgumentException( '$apiUrl must be a string' );
94
		}
95 20
		if( $session === null ) {
96 20
			$session = new MediawikiSession( $this );
97
		}
98
99 20
		$this->apiUrl = $apiUrl;
100 20
		$this->client = $client;
101 20
		$this->session = $session;
102
103 20
		$this->logger = new NullLogger();
104 20
	}
105
106
	/**
107
	 * @return ClientInterface
108
	 */
109 21
	private function getClient() {
110 21
		if( $this->client === null ) {
111 4
			$clientFactory = new ClientFactory();
112 4
			$clientFactory->setLogger( $this->logger );
113 4
			$this->client = $clientFactory->getClient();
114
		}
115 21
		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 1
	public function getRequestAsync( Request $request ) {
142 1
		$promise = $this->getClient()->requestAsync(
143 1
			'GET',
144 1
			$this->apiUrl,
145 1
			$this->getClientRequestOptions( $request, 'query' )
146
		);
147
148
		return $promise->then( function( ResponseInterface $response ) {
149 1
			return call_user_func( array( $this, 'decodeResponse' ), $response );
150 1
		} );
151
	}
152
153
	/**
154
	 * @since 2.0
155
	 *
156
	 * @param Request $request
157
	 *
158
	 * @return PromiseInterface
159
	 *         Normally promising an array, though can be mixed (json_decode result)
160
	 *         Can throw UsageExceptions or RejectionExceptions
161
	 */
162 1
	public function postRequestAsync( Request $request ) {
163 1
		$promise = $this->getClient()->requestAsync(
164 1
			'POST',
165 1
			$this->apiUrl,
166 1
			$this->getClientRequestOptions( $request, $this->getPostRequestEncoding( $request ) )
167
		);
168
169
		return $promise->then( function( ResponseInterface $response ) {
170 1
			return call_user_func( array( $this, 'decodeResponse' ), $response );
171 1
		} );
172
	}
173
174
	/**
175
	 * @since 0.2
176
	 *
177
	 * @param Request $request
178
	 *
179
	 * @return mixed Normally an array
180
	 */
181 9
	public function getRequest( Request $request ) {
182 9
		$response = $this->getClient()->request(
183 9
			'GET',
184 9
			$this->apiUrl,
185 9
			$this->getClientRequestOptions( $request, 'query' )
186
		);
187
188 9
		return $this->decodeResponse( $response );
189
	}
190
191
	/**
192
	 * @since 0.2
193
	 *
194
	 * @param Request $request
195
	 *
196
	 * @return mixed Normally an array
197
	 */
198 10
	public function postRequest( Request $request ) {
199 10
		$response = $this->getClient()->request(
200 10
			'POST',
201 10
			$this->apiUrl,
202 10
			$this->getClientRequestOptions( $request, $this->getPostRequestEncoding( $request ) )
203
		);
204
205 10
		return $this->decodeResponse( $response );
206
	}
207
208
	/**
209
	 * @param ResponseInterface $response
210
	 *
211
	 * @return mixed
212
	 * @throws UsageException
213
	 */
214 21
	private function decodeResponse( ResponseInterface $response ) {
215 21
		$resultArray = json_decode( $response->getBody(), true );
216
217 21
		$this->logWarnings( $resultArray );
218 21
		$this->throwUsageExceptions( $resultArray );
219
220 19
		return $resultArray;
221
	}
222
223
    /**
224
     * @param Request $request
225
     *
226
     * @return string
227
     */
228 9
	private function getPostRequestEncoding( Request $request ) {
229 9
	    foreach ( $request->getParams() as $value ) {
230 9
            if ( is_resource( $value ) ) {
231 9
                return 'multipart';
232
            }
233
        }
234 8
        return 'form_params';
235
    }
236
237
	/**
238
	 * @param Request $request
239
	 * @param string $paramsKey either 'query' or 'multipart'
240
	 *
241
	 * @throws RequestException
242
	 *
243
	 * @return array as needed by ClientInterface::get and ClientInterface::post
244
	 */
245 21
	private function getClientRequestOptions( Request $request, $paramsKey ) {
246 21
	    $params = array_merge( $request->getParams(), array( 'format' => 'json' ) );
247 21
	    if ( $paramsKey === 'multipart' ) {
248 1
            $params = $this->encodeMultipartParams( $params );
249
        }
250
251
		return array(
252 21
			$paramsKey => $params,
253 21
			'headers' => array_merge( $this->getDefaultHeaders(), $request->getHeaders() ),
254
		);
255
	}
256
257
    /**
258
     * @param array $params
259
     * @return array
260
     */
261
	private function encodeMultipartParams( $params ) {
262 1
	    return array_map( function( $name, $value ) {
263
            return array(
264 1
                'name' => $name,
265 1
                'contents' => $value
266
            );
267 1
        }, array_keys( $params ), $params );
268
    }
269
270
	/**
271
	 * @return array
272
	 */
273 17
	private function getDefaultHeaders() {
274
		return array(
275 17
			'User-Agent' => $this->getUserAgent(),
276
		);
277
	}
278
279 17
	private function getUserAgent() {
280 17
		$loggedIn = $this->isLoggedin();
281 17
		if( $loggedIn ) {
282
			return 'addwiki-mediawiki-client/' . $loggedIn;
283
		}
284 17
		return 'addwiki-mediawiki-client';
285
	}
286
287
	/**
288
	 * @param $result
289
	 */
290 17
	private function logWarnings( $result ) {
291 17
		if( is_array( $result ) && array_key_exists( 'warnings', $result ) ) {
292
			foreach( $result['warnings'] as $module => $warningData ) {
293
				$this->logger->log( LogLevel::WARNING, $module . ': ' . $warningData['*'], array( 'data' => $warningData ) );
294
			}
295
		}
296 17
	}
297
298
	/**
299
	 * @param array $result
300
	 *
301
	 * @throws UsageException
302
	 */
303 17
	private function throwUsageExceptions( $result ) {
304 17
		if( is_array( $result ) && array_key_exists( 'error', $result ) ) {
305 2
			throw new UsageException(
306 2
				$result['error']['code'],
307 2
				$result['error']['info'],
308
				$result
309
			);
310
		}
311 15
	}
312
313
	/**
314
	 * @since 0.1
315
	 *
316
	 * @return bool|string false or the name of the current user
317
	 */
318 17
	public function isLoggedin() {
319 17
		return $this->isLoggedIn;
320
	}
321
322
	/**
323
	 * @since 0.1
324
	 *
325
	 * @param ApiUser $apiUser
326
	 *
327
	 * @throws UsageException
328
	 * @return bool success
329
	 */
330 2
	public function login( ApiUser $apiUser ) {
331 2
		$this->logger->log( LogLevel::DEBUG, 'Logging in' );
332 2
		$credentials = $this->getLoginParams( $apiUser );
333 2
		$result = $this->postRequest( new SimpleRequest( 'login', $credentials ) );
334 2
		if ( $result['login']['result'] == "NeedToken" ) {
335 2
			$result = $this->postRequest( new SimpleRequest( 'login', array_merge( array( 'lgtoken' => $result['login']['token'] ), $credentials) ) );
336
		}
337 2
		if ( $result['login']['result'] == "Success" ) {
338 1
			$this->isLoggedIn = $apiUser->getUsername();
339 1
			return true;
340
		}
341
342 1
		$this->isLoggedIn = false;
343 1
		$this->throwLoginUsageException( $result );
344
		return false;
345
	}
346
347
	/**
348
	 * @param ApiUser $apiUser
349
	 *
350
	 * @return string[]
351
	 */
352 2
	private function getLoginParams( ApiUser $apiUser ) {
353
		$params = array(
354 2
			'lgname' => $apiUser->getUsername(),
355 2
			'lgpassword' => $apiUser->getPassword(),
356
		);
357
358 2
		if( !is_null( $apiUser->getDomain() ) ) {
359
			$params['lgdomain'] = $apiUser->getDomain();
360
		}
361 2
		return $params;
362
	}
363
364
	/**
365
	 * @param array $result
366
	 *
367
	 * @throws UsageException
368
	 */
369 1
	private function throwLoginUsageException( $result ) {
370 1
		$loginResult = $result['login']['result'];
371
372 1
		throw new UsageException(
373 1
			'login-' . $loginResult,
374 1
			$this->getLoginExceptionMessage( $loginResult ),
375
			$result
376
		);
377
	}
378
379
	/**
380
	 * @param string $loginResult
381
	 *
382
	 * @return string
383
	 */
384 1
	private function getLoginExceptionMessage( $loginResult ) {
385
		switch( $loginResult ) {
386 1
			case 'Illegal';
387
				return 'You provided an illegal username';
388 1
			case 'NotExists';
389
				return 'The username you provided doesn\'t exist';
390 1
			case 'WrongPass';
391
				return 'The password you provided is incorrect';
392 1
			case 'WrongPluginPass';
393
				return 'An authentication plugin rather than MediaWiki itself rejected the password';
394 1
			case 'CreateBlocked';
395
				return 'The wiki tried to automatically create a new account for you, but your IP address has been blocked from account creation';
396 1
			case 'Throttled';
397
				return 'You\'ve logged in too many times in a short time.';
398 1
			case 'Blocked';
399
				return 'User is blocked';
400 1
			case 'NeedToken';
401
				return 'Either you did not provide the login token or the sessionid cookie.';
402
			default:
403 1
				return $loginResult;
404
		}
405
	}
406
407
	/**
408
	 * @since 0.1
409
	 *
410
	 * @return bool success
411
	 */
412 2
	public function logout() {
413 2
		$this->logger->log( LogLevel::DEBUG, 'Logging out' );
414 2
		$result = $this->postRequest( new SimpleRequest( 'logout' ) );
415 2
		if( $result === array() ) {
416 1
			$this->isLoggedIn = false;
417 1
			$this->clearTokens();
418 1
			return true;
419
		}
420 1
		return false;
421
	}
422
423
	/**
424
	 * @since 0.1
425
	 *
426
	 * @param string $type
427
	 *
428
	 * @return string
429
	 */
430 2
	public function getToken( $type = 'csrf' ) {
431 2
		return $this->session->getToken( $type );
432
	}
433
434
	/**
435
	 * @since 0.1
436
	 *
437
	 * Clears all tokens stored by the api
438
	 */
439 1
	public function clearTokens() {
440 1
		$this->session->clearTokens();
441 1
	}
442
443
	/**
444
	 * @return string
445
	 */
446 4
	public function getVersion(){
447 4
		if( !isset( $this->version ) ) {
448 4
			$result = $this->getRequest( new SimpleRequest( 'query', array(
449 4
				'meta' => 'siteinfo',
450
				'continue' => '',
451
			) ) );
452 4
			preg_match(
453 4
				'/\d+(?:\.\d+)+/',
454 4
				$result['query']['general']['generator'],
455
				$versionParts
456
			);
457 4
			$this->version = $versionParts[0];
458
		}
459 4
		return $this->version;
460
	}
461
462
}
463