|
1
|
|
|
<?php |
|
2
|
|
|
/** |
|
3
|
|
|
* This file contains only the ApiHelper class. |
|
4
|
|
|
*/ |
|
5
|
|
|
|
|
6
|
|
|
namespace AppBundle\Helper; |
|
7
|
|
|
|
|
8
|
|
|
use Mediawiki\Api\MediawikiApi; |
|
9
|
|
|
use Mediawiki\Api\SimpleRequest; |
|
10
|
|
|
use Mediawiki\Api\FluentRequest; |
|
11
|
|
|
use Psr\Cache\CacheItemPoolInterface; |
|
12
|
|
|
use Symfony\Component\Config\Definition\Exception\Exception; |
|
13
|
|
|
use Symfony\Component\DependencyInjection\ContainerInterface; |
|
14
|
|
|
use Xtools\ProjectRepository; |
|
15
|
|
|
|
|
16
|
|
|
/** |
|
17
|
|
|
* This is a helper for calling the MediaWiki API. |
|
18
|
|
|
*/ |
|
19
|
|
|
class ApiHelper extends HelperBase |
|
20
|
|
|
{ |
|
21
|
|
|
/** @var MediawikiApi The API object. */ |
|
22
|
|
|
private $api; |
|
23
|
|
|
|
|
24
|
|
|
/** @var CacheItemPoolInterface The cache. */ |
|
25
|
|
|
protected $cache; |
|
26
|
|
|
|
|
27
|
|
|
/** @var ContainerInterface The DI container. */ |
|
28
|
|
|
protected $container; |
|
29
|
|
|
|
|
30
|
|
|
/** |
|
31
|
|
|
* ApiHelper constructor. |
|
32
|
|
|
* @param ContainerInterface $container |
|
33
|
|
|
*/ |
|
34
|
|
|
public function __construct(ContainerInterface $container) |
|
35
|
|
|
{ |
|
36
|
|
|
$this->container = $container; |
|
37
|
|
|
$this->cache = $container->get('cache.app'); |
|
38
|
|
|
} |
|
39
|
|
|
|
|
40
|
|
|
/** |
|
41
|
|
|
* Set up the MediawikiApi object for the given project. |
|
42
|
|
|
* |
|
43
|
|
|
* @param string $project |
|
44
|
|
|
*/ |
|
45
|
|
|
private function setUp($project) |
|
46
|
|
|
{ |
|
47
|
|
|
if (!$this->api instanceof MediawikiApi) { |
|
48
|
|
|
$project = ProjectRepository::getProject($project, $this->container); |
|
49
|
|
|
$this->api = $project->getApi(); |
|
50
|
|
|
} |
|
51
|
|
|
} |
|
52
|
|
|
|
|
53
|
|
|
/** |
|
54
|
|
|
* Get the given user's groups on the given project. |
|
55
|
|
|
* @deprecated Use User::getGroups() instead. |
|
56
|
|
|
* @param string $project |
|
57
|
|
|
* @param string $username |
|
58
|
|
|
* @return string[] |
|
59
|
|
|
*/ |
|
60
|
|
View Code Duplication |
public function groups($project, $username) |
|
|
|
|
|
|
61
|
|
|
{ |
|
62
|
|
|
$this->setUp($project); |
|
63
|
|
|
$params = [ "list"=>"users", "ususers"=>$username, "usprop"=>"groups" ]; |
|
64
|
|
|
$query = new SimpleRequest('query', $params); |
|
65
|
|
|
$result = []; |
|
66
|
|
|
|
|
67
|
|
|
try { |
|
68
|
|
|
$res = $this->api->getRequest($query); |
|
69
|
|
|
if (isset($res["batchcomplete"]) |
|
70
|
|
|
&& isset($res["query"]["users"][0]["groups"]) |
|
71
|
|
|
) { |
|
72
|
|
|
$result = $res["query"]["users"][0]["groups"]; |
|
73
|
|
|
} |
|
74
|
|
|
} catch (Exception $e) { |
|
75
|
|
|
// The api returned an error! Ignore |
|
76
|
|
|
} |
|
77
|
|
|
|
|
78
|
|
|
return $result; |
|
79
|
|
|
} |
|
80
|
|
|
|
|
81
|
|
|
/** |
|
82
|
|
|
* Get the given user's globally-applicable groups. |
|
83
|
|
|
* @deprecated Use User::getGlobalGroups() instead. |
|
84
|
|
|
* @param string $project |
|
85
|
|
|
* @param string $username |
|
86
|
|
|
* @return string[] |
|
87
|
|
|
*/ |
|
88
|
|
View Code Duplication |
public function globalGroups($project, $username) |
|
|
|
|
|
|
89
|
|
|
{ |
|
90
|
|
|
$this->setUp($project); |
|
91
|
|
|
$params = [ "meta"=>"globaluserinfo", "guiuser"=>$username, "guiprop"=>"groups" ]; |
|
92
|
|
|
$query = new SimpleRequest('query', $params); |
|
93
|
|
|
$result = []; |
|
94
|
|
|
|
|
95
|
|
|
try { |
|
96
|
|
|
$res = $this->api->getRequest($query); |
|
97
|
|
|
if (isset($res["batchcomplete"]) && isset($res["query"]["globaluserinfo"]["groups"])) { |
|
98
|
|
|
$result = $res["query"]["globaluserinfo"]["groups"]; |
|
99
|
|
|
} |
|
100
|
|
|
} catch (Exception $e) { |
|
101
|
|
|
// The api returned an error! Ignore |
|
102
|
|
|
} |
|
103
|
|
|
|
|
104
|
|
|
return $result; |
|
105
|
|
|
} |
|
106
|
|
|
|
|
107
|
|
|
/** |
|
108
|
|
|
* Get HTML display titles of a set of pages (or the normal title if there's no display title). |
|
109
|
|
|
* This will send t/50 API requests where t is the number of titles supplied. |
|
110
|
|
|
* @param string $project The project. |
|
111
|
|
|
* @param string[] $pageTitles The titles to fetch. |
|
112
|
|
|
* @return string[] Keys are the original supplied title, and values are the display titles. |
|
113
|
|
|
*/ |
|
114
|
|
|
public function displayTitles($project, $pageTitles) |
|
115
|
|
|
{ |
|
116
|
|
|
$this->setUp($project); |
|
117
|
|
|
$displayTitles = []; |
|
118
|
|
|
$numPages = count($pageTitles); |
|
119
|
|
|
for ($n = 0; $n < $numPages; $n += 50) { |
|
120
|
|
|
$titleSlice = array_slice($pageTitles, $n, 50); |
|
121
|
|
|
$params = [ |
|
122
|
|
|
'prop' => 'info|pageprops', |
|
123
|
|
|
'inprop' => 'displaytitle', |
|
124
|
|
|
'titles' => join('|', $titleSlice), |
|
125
|
|
|
]; |
|
126
|
|
|
$query = new SimpleRequest('query', $params); |
|
127
|
|
|
$result = $this->api->postRequest($query); |
|
128
|
|
|
|
|
129
|
|
|
// Extract normalization info. |
|
130
|
|
|
$normalized = []; |
|
131
|
|
|
if (isset($result['query']['normalized'])) { |
|
132
|
|
|
array_map( |
|
133
|
|
|
function ($e) use (&$normalized) { |
|
134
|
|
|
$normalized[$e['to']] = $e['from']; |
|
135
|
|
|
}, |
|
136
|
|
|
$result['query']['normalized'] |
|
137
|
|
|
); |
|
138
|
|
|
} |
|
139
|
|
|
|
|
140
|
|
|
// Match up the normalized titles with the display titles and the original titles. |
|
141
|
|
|
foreach ($result['query']['pages'] as $pageInfo) { |
|
142
|
|
|
$displayTitle = isset($pageInfo['pageprops']['displaytitle']) |
|
143
|
|
|
? $pageInfo['pageprops']['displaytitle'] |
|
144
|
|
|
: $pageInfo['title']; |
|
145
|
|
|
$origTitle = isset($normalized[$pageInfo['title']]) |
|
146
|
|
|
? $normalized[$pageInfo['title']] : $pageInfo['title']; |
|
147
|
|
|
$displayTitles[$origTitle] = $displayTitle; |
|
148
|
|
|
} |
|
149
|
|
|
} |
|
150
|
|
|
|
|
151
|
|
|
return $displayTitles; |
|
152
|
|
|
} |
|
153
|
|
|
|
|
154
|
|
|
/** |
|
155
|
|
|
* Make mass API requests to MediaWiki API |
|
156
|
|
|
* The API normally limits to 500 pages, but gives you a 'continue' value |
|
157
|
|
|
* to finish iterating through the resource. |
|
158
|
|
|
* Adapted from https://github.com/MusikAnimal/pageviews |
|
159
|
|
|
* @param array $params Associative array of params to pass to API |
|
160
|
|
|
* @param string $project Project to query, e.g. en.wikipedia.org |
|
161
|
|
|
* @param string|func $dataKey The key for the main chunk of data, in the query hash |
|
162
|
|
|
* (e.g. 'categorymembers' for API:Categorymembers). |
|
163
|
|
|
* If this is a function it is given the response data, |
|
164
|
|
|
* and expected to return the data we want to concatentate. |
|
165
|
|
|
* @param string [$continueKey] the key to look in the continue hash, if present |
|
166
|
|
|
* (e.g. 'cmcontinue' for API:Categorymembers) |
|
167
|
|
|
* @param integer [$limit] Max number of pages to fetch |
|
168
|
|
|
* @return array Associative array with data |
|
169
|
|
|
*/ |
|
170
|
|
|
public function massApi($params, $project, $dataKey, $continueKey = 'continue', $limit = 5000) |
|
171
|
|
|
{ |
|
172
|
|
|
$this->setUp($project); |
|
173
|
|
|
|
|
174
|
|
|
// Passed by reference to massApiInternal so we can keep track of |
|
175
|
|
|
// everything we need during the recursive calls |
|
176
|
|
|
// The magically essential part here is $data['promise'] which we'll |
|
177
|
|
|
// wait to be resolved |
|
178
|
|
|
$data = [ |
|
179
|
|
|
'params' => $params, |
|
180
|
|
|
'project' => $project, |
|
181
|
|
|
'continueKey' => $continueKey, |
|
182
|
|
|
'dataKey' => $dataKey, |
|
183
|
|
|
'limit' => $limit, |
|
184
|
|
|
'resolveData' => [ |
|
185
|
|
|
'pages' => [] |
|
186
|
|
|
], |
|
187
|
|
|
'continueValue' => null, |
|
188
|
|
|
'promise' => new \GuzzleHttp\Promise\Promise(), |
|
189
|
|
|
]; |
|
190
|
|
|
|
|
191
|
|
|
// wait for all promises to complete, even if some of them fail |
|
192
|
|
|
\GuzzleHttp\Promise\settle($this->massApiInternal($data))->wait(); |
|
193
|
|
|
|
|
194
|
|
|
return $data['resolveData']; |
|
195
|
|
|
} |
|
196
|
|
|
|
|
197
|
|
|
/** |
|
198
|
|
|
* Internal function used by massApi() to make recursive calls |
|
199
|
|
|
* @param array &$data Everything we need to keep track of, as defined in massApi() |
|
200
|
|
|
* @return null Nothing. $data['promise']->then is used to continue flow of |
|
201
|
|
|
* execution after all recursive calls are complete |
|
202
|
|
|
*/ |
|
203
|
|
|
private function massApiInternal(&$data) |
|
204
|
|
|
{ |
|
205
|
|
|
$requestData = array_merge([ |
|
206
|
|
|
'action' => 'query', |
|
207
|
|
|
'format' => 'json', |
|
208
|
|
|
'formatversion' => '2', |
|
209
|
|
|
], $data['params']); |
|
210
|
|
|
|
|
211
|
|
|
if ($data['continueValue']) { |
|
212
|
|
|
$requestData[$data['continueKey']] = $data['continueValue']; |
|
213
|
|
|
} |
|
214
|
|
|
|
|
215
|
|
|
$query = FluentRequest::factory()->setAction('query')->setParams($requestData); |
|
216
|
|
|
$innerPromise = $this->api->getRequestAsync($query); |
|
217
|
|
|
|
|
218
|
|
|
$innerPromise->then(function ($result) use (&$data) { |
|
219
|
|
|
// some failures come back as 200s, so we still resolve and let the outer function handle it |
|
220
|
|
|
if (isset($result['error']) || !isset($result['query'])) { |
|
221
|
|
|
return $data['promise']->resolve($data); |
|
222
|
|
|
} |
|
223
|
|
|
|
|
224
|
|
|
$dataKey = $data['dataKey']; |
|
225
|
|
|
$isFinished = false; |
|
|
|
|
|
|
226
|
|
|
|
|
227
|
|
|
// allow custom function to parse the data we want, if provided |
|
228
|
|
|
if (is_callable($dataKey)) { |
|
229
|
|
|
$data['resolveData']['pages'] = array_merge( |
|
230
|
|
|
$data['resolveData']['pages'], |
|
231
|
|
|
$data['dataKey']($result['query']) |
|
232
|
|
|
); |
|
233
|
|
|
$isFinished = count($data['resolveData']['pages']) >= $data['limit']; |
|
234
|
|
|
} else { |
|
235
|
|
|
// append new data to data from last request. We might want both 'pages' and dataKey |
|
236
|
|
|
if (isset($result['query']['pages'])) { |
|
237
|
|
|
$data['resolveData']['pages'] = array_merge( |
|
238
|
|
|
$data['resolveData']['pages'], |
|
239
|
|
|
$result['query']['pages'] |
|
240
|
|
|
); |
|
241
|
|
|
} |
|
242
|
|
|
if ($result['query'][$dataKey]) { |
|
243
|
|
|
$newValues = isset($data['resolveData'][$dataKey]) ? $data['resolveData'][$dataKey] : []; |
|
244
|
|
|
$data['resolveData'][$dataKey] = array_merge($newValues, $result['query'][$dataKey]); |
|
245
|
|
|
} |
|
246
|
|
|
|
|
247
|
|
|
// If pages is not the collection we want, it will be either an empty array or one entry with |
|
248
|
|
|
// basic page info depending on what API we're hitting. So resolveData[dataKey] will hit the limit |
|
249
|
|
|
$isFinished = count($data['resolveData']['pages']) >= $data['limit'] || |
|
250
|
|
|
count($data['resolveData'][$dataKey]) >= $data['limit']; |
|
251
|
|
|
} |
|
252
|
|
|
|
|
253
|
|
|
// make recursive call if needed, waiting 100ms |
|
254
|
|
|
if (!$isFinished && isset($result['continue']) && isset($result['continue'][$data['continueKey']])) { |
|
255
|
|
|
usleep(100000); |
|
256
|
|
|
$data['continueValue'] = $result['continue'][$data['continueKey']]; |
|
257
|
|
|
return $this->massApiInternal($data); |
|
258
|
|
|
} else { |
|
259
|
|
|
// indicate there were more entries than the limit |
|
260
|
|
|
if (isset($result['continue'])) { |
|
261
|
|
|
$data['resolveData']['continue'] = true; |
|
262
|
|
|
} |
|
263
|
|
|
$data['promise']->resolve($data); |
|
264
|
|
|
} |
|
265
|
|
|
}); |
|
266
|
|
|
} |
|
267
|
|
|
} |
|
268
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.