Completed
Push — master ( 684e0f...bce5b6 )
by
unknown
02:51
created

ApiHelper::massApi()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 26
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 26
c 0
b 0
f 0
rs 8.8571
cc 1
eloc 14
nc 1
nop 5
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 LabsHelper The Labs helper. */
25
    private $labsHelper;
26
27
    /** @var CacheItemPoolInterface The cache. */
28
    protected $cache;
29
30
    /** @var ContainerInterface The DI container. */
31
    protected $container;
32
33
    /**
34
     * ApiHelper constructor.
35
     * @param ContainerInterface $container
36
     * @param LabsHelper $labsHelper
37
     */
38
    public function __construct(ContainerInterface $container, LabsHelper $labsHelper)
39
    {
40
        $this->container = $container;
41
        $this->labsHelper = $labsHelper;
42
        $this->cache = $container->get('cache.app');
43
    }
44
45
    /**
46
     * Set up the MediawikiApi object for the given project.
47
     *
48
     * @param string $project
49
     */
50
    private function setUp($project)
51
    {
52
        if (!$this->api instanceof MediawikiApi) {
53
            $project = ProjectRepository::getProject($project, $this->container);
54
            $this->api = $project->getApi();
55
        }
56
    }
57
58
    /**
59
     * Get the given user's groups on the given project.
60
     * @deprecated Use User::getGroups() instead.
61
     * @param string $project
62
     * @param string $username
63
     * @return string[]
64
     */
65 View Code Duplication
    public function groups($project, $username)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
66
    {
67
        $this->setUp($project);
68
        $params = [ "list"=>"users", "ususers"=>$username, "usprop"=>"groups" ];
69
        $query = new SimpleRequest('query', $params);
70
        $result = [];
71
72
        try {
73
            $res = $this->api->getRequest($query);
74
            if (isset($res["batchcomplete"]) && isset($res["query"]["users"][0]["groups"])) {
75
                $result = $res["query"]["users"][0]["groups"];
76
            }
77
        } catch (Exception $e) {
78
            // The api returned an error!  Ignore
79
        }
80
81
        return $result;
82
    }
83
84
    /**
85
     * Get the given user's globally-applicable groups.
86
     * @deprecated Use User::getGlobalGroups() instead.
87
     * @param string $project
88
     * @param string $username
89
     * @return string[]
90
     */
91 View Code Duplication
    public function globalGroups($project, $username)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
92
    {
93
        $this->setUp($project);
94
        $params = [ "meta"=>"globaluserinfo", "guiuser"=>$username, "guiprop"=>"groups" ];
95
        $query = new SimpleRequest('query', $params);
96
        $result = [];
97
98
        try {
99
            $res = $this->api->getRequest($query);
100
            if (isset($res["batchcomplete"]) && isset($res["query"]["globaluserinfo"]["groups"])) {
101
                $result = $res["query"]["globaluserinfo"]["groups"];
102
            }
103
        } catch (Exception $e) {
104
            // The api returned an error!  Ignore
105
        }
106
107
        return $result;
108
    }
109
110
    /**
111
     * Get a list of administrators for the given project.
112
     * @TODO Move to the Project class?
113
     * @param string $project
114
     * @return string[]
115
     */
116
    public function getAdmins($project)
117
    {
118
        $params = [
119
            'list' => 'allusers',
120
            'augroup' => 'sysop|bureaucrat|steward|oversight|checkuser',
121
            'auprop' => 'groups',
122
            'aulimit' => '500',
123
        ];
124
125
        $result = [];
126
        $adminData = $this->massApi($params, $project, 'allusers', 'aufrom');
127
128
        if (!isset($adminData['allusers'])) {
129
            // Invalid result
130
            return array();
131
        }
132
133
        $admins = $adminData['allusers'];
134
135
        foreach ($admins as $admin) {
136
            $groups = [];
137
            if (in_array("sysop", $admin["groups"])) {
138
                $groups[] = "A";
139
            }
140
            if (in_array("bureaucrat", $admin["groups"])) {
141
                $groups[] = "B";
142
            }
143
            if (in_array("steward", $admin["groups"])) {
144
                $groups[] = "S" ;
145
            }
146
            if (in_array("checkuser", $admin["groups"])) {
147
                $groups[] = "CU";
148
            }
149
            if (in_array("oversight", $admin["groups"])) {
150
                $groups[] = "OS";
151
            }
152
            if (in_array("bot", $admin["groups"])) {
153
                $groups[] = "Bot";
154
            }
155
            $result[ $admin["name"] ] = [
156
                "groups" => implode('/', $groups)
157
            ];
158
        }
159
160
        return $result;
161
    }
162
163
    /**
164
     * Get basic info about a page via the API
165
     * @param  string  $project      Full domain of project (en.wikipedia.org)
166
     * @param  string  $page         Page title
167
     * @param  boolean $followRedir  Whether or not to resolve redirects
168
     * @return array   Associative array of data
169
     */
170
    public function getBasicPageInfo($project, $page, $followRedir)
171
    {
172
        $this->setUp($project);
173
174
        // @TODO: Also include 'extlinks' prop when we start checking for dead external links.
175
        $params = [
176
            'prop' => 'info|pageprops',
177
            'inprop' => 'protection|talkid|watched|watchers|notificationtimestamp|subjectid|url|readable',
178
            'converttitles' => '',
179
            // 'ellimit' => 20,
1 ignored issue
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
180
            // 'elexpandurl' => '',
1 ignored issue
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
181
            'titles' => $page,
182
            'formatversion' => 2
183
            // 'pageids' => $pageIds // FIXME: allow page IDs
1 ignored issue
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
184
        ];
185
186
        if ($followRedir) {
187
            $params['redirects'] = '';
188
        }
189
190
        $query = new SimpleRequest('query', $params);
191
        $result = [];
192
193
        try {
194
            $res = $this->api->getRequest($query);
195
            if (isset($res['query']['pages'])) {
196
                $result = $res['query']['pages'][0];
197
            }
198
        } catch (Exception $e) {
199
            // The api returned an error!  Ignore
200
        }
201
202
        return $result;
203
    }
204
205
    /**
206
     * Get HTML display titles of a set of pages (or the normal title if there's no display title).
207
     * This will send t/50 API requests where t is the number of titles supplied.
208
     * @param string $project The project.
209
     * @param string[] $pageTitles The titles to fetch.
210
     * @return string[] Keys are the original supplied title, and values are the display titles.
211
     */
212
    public function displayTitles($project, $pageTitles)
213
    {
214
        $this->setUp($project);
215
        $displayTitles = [];
216
        for ($n = 0; $n < count($pageTitles); $n += 50) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
217
            $titleSlice = array_slice($pageTitles, $n, 50);
218
            $params = [
219
                'prop' => 'info|pageprops',
220
                'inprop' => 'displaytitle',
221
                'titles' => join('|', $titleSlice),
222
            ];
223
            $query = new SimpleRequest('query', $params);
224
            $result = $this->api->postRequest($query);
225
226
            // Extract normalization info.
227
            $normalized = [];
228
            if (isset($result['query']['normalized'])) {
229
                array_map(
230
                    function ($e) use (&$normalized) {
231
                        $normalized[$e['to']] = $e['from'];
232
                    },
233
                    $result['query']['normalized']
234
                );
235
            }
236
237
            // Match up the normalized titles with the display titles and the original titles.
238
            foreach ($result['query']['pages'] as $pageInfo) {
239
                $displayTitle = isset($pageInfo['pageprops']['displaytitle'])
240
                    ? $pageInfo['pageprops']['displaytitle']
241
                    : $pageInfo['title'];
242
                $origTitle = isset($normalized[$pageInfo['title']])
243
                    ? $normalized[$pageInfo['title']] : $pageInfo['title'];
244
                $displayTitles[$origTitle] = $displayTitle;
245
            }
246
        }
247
248
        return $displayTitles;
249
    }
250
251
    /**
252
     * Make mass API requests to MediaWiki API
253
     * The API normally limits to 500 pages, but gives you a 'continue' value
254
     *   to finish iterating through the resource.
255
     * Adapted from https://github.com/MusikAnimal/pageviews
256
     * @param  array       $params        Associative array of params to pass to API
257
     * @param  string      $project       Project to query, e.g. en.wikipedia.org
258
     * @param  string|func $dataKey       The key for the main chunk of data, in the query hash
259
     *                                    (e.g. 'categorymembers' for API:Categorymembers).
260
     *                                    If this is a function it is given the response data,
261
     *                                    and expected to return the data we want to concatentate.
262
     * @param  string      [$continueKey] the key to look in the continue hash, if present
263
     *                                    (e.g. 'cmcontinue' for API:Categorymembers)
264
     * @param  integer     [$limit]       Max number of pages to fetch
265
     * @return array                      Associative array with data
266
     */
267
    public function massApi($params, $project, $dataKey, $continueKey = 'continue', $limit = 5000)
268
    {
269
        $this->setUp($project);
270
271
        // Passed by reference to massApiInternal so we can keep track of
272
        //   everything we need during the recursive calls
273
        // The magically essential part here is $data['promise'] which we'll
274
        //   wait to be resolved
275
        $data = [
276
            'params' => $params,
277
            'project' => $project,
278
            'continueKey' => $continueKey,
279
            'dataKey' => $dataKey,
280
            'limit' => $limit,
281
            'resolveData' => [
282
                'pages' => []
283
            ],
284
            'continueValue' => null,
285
            'promise' => new \GuzzleHttp\Promise\Promise(),
286
        ];
287
288
        // wait for all promises to complete, even if some of them fail
289
        \GuzzleHttp\Promise\settle($this->massApiInternal($data))->wait();
290
291
        return $data['resolveData'];
292
    }
293
294
    /**
295
     * Internal function used by massApi() to make recursive calls
296
     * @param  array &$data Everything we need to keep track of, as defined in massApi()
297
     * @return null         Nothing. $data['promise']->then is used to continue flow of
298
     *                      execution after all recursive calls are complete
299
     */
300
    private function massApiInternal(&$data)
301
    {
302
        $requestData = array_merge([
303
            'action' => 'query',
304
            'format' => 'json',
305
            'formatversion' => '2',
306
        ], $data['params']);
307
308
        if ($data['continueValue']) {
309
            $requestData[$data['continueKey']] = $data['continueValue'];
310
        }
311
312
        $query = FluentRequest::factory()->setAction('query')->setParams($requestData);
313
        $innerPromise = $this->api->getRequestAsync($query);
314
315
        $innerPromise->then(function ($result) use (&$data) {
316
            // some failures come back as 200s, so we still resolve and let the outer function handle it
317
            if (isset($result['error']) || !isset($result['query'])) {
318
                return $data['promise']->resolve($data);
319
            }
320
321
            $dataKey = $data['dataKey'];
322
            $isFinished = false;
0 ignored issues
show
Unused Code introduced by
$isFinished is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
323
324
            // allow custom function to parse the data we want, if provided
325
            if (is_callable($dataKey)) {
326
                $data['resolveData']['pages'] = array_merge(
327
                    $data['resolveData']['pages'],
328
                    $data['dataKey']($result['query'])
329
                );
330
                $isFinished = count($data['resolveData']['pages']) >= $data['limit'];
331
            } else {
332
                // append new data to data from last request. We might want both 'pages' and dataKey
333
                if (isset($result['query']['pages'])) {
334
                    $data['resolveData']['pages'] = array_merge(
335
                        $data['resolveData']['pages'],
336
                        $result['query']['pages']
337
                    );
338
                }
339
                if ($result['query'][$dataKey]) {
340
                    $newValues = isset($data['resolveData'][$dataKey]) ? $data['resolveData'][$dataKey] : [];
341
                    $data['resolveData'][$dataKey] = array_merge($newValues, $result['query'][$dataKey]);
342
                }
343
344
                // If pages is not the collection we want, it will be either an empty array or one entry with
345
                //   basic page info depending on what API we're hitting. So resolveData[dataKey] will hit the limit
346
                $isFinished = count($data['resolveData']['pages']) >= $data['limit'] ||
347
                    count($data['resolveData'][$dataKey]) >= $data['limit'];
348
            }
349
350
            // make recursive call if needed, waiting 100ms
351
            if (!$isFinished && isset($result['continue']) && isset($result['continue'][$data['continueKey']])) {
352
                usleep(100000);
353
                $data['continueValue'] = $result['continue'][$data['continueKey']];
354
                return $this->massApiInternal($data);
355
            } else {
356
                // indicate there were more entries than the limit
357
                if ($result['continue']) {
358
                    $data['resolveData']['continue'] = true;
359
                }
360
                $data['promise']->resolve($data);
361
            }
362
        });
363
    }
364
}
365