Completed
Push — master ( 9a7c34...22a7b0 )
by adam
02:46
created

MediawikiApi::encodeMultipartParams()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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