Test Failed
Push — master ( e77fba...b33856 )
by MusikAnimal
07:16
created

PagesController::setUPages()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 19
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 14
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 19
ccs 0
cts 0
cp 0
crap 2
rs 9.7998
1
<?php
2
/**
3
 * This file contains only the PagesController class.
4
 */
5
6
declare(strict_types=1);
7
8
namespace AppBundle\Controller;
9
10
use AppBundle\Helper\I18nHelper;
11
use AppBundle\Model\Pages;
12
use AppBundle\Model\Project;
13
use AppBundle\Repository\PagesRepository;
14
use GuzzleHttp;
15
use Symfony\Component\DependencyInjection\ContainerInterface;
16
use Symfony\Component\HttpFoundation\JsonResponse;
17
use Symfony\Component\HttpFoundation\RedirectResponse;
18
use Symfony\Component\HttpFoundation\RequestStack;
19
use Symfony\Component\HttpFoundation\Response;
20
use Symfony\Component\HttpKernel\Exception\HttpException;
21
use Symfony\Component\Routing\Annotation\Route;
22
23
/**
24
 * This controller serves the Pages tool.
25
 */
26
class PagesController extends XtoolsController
27
{
28
    /**
29
     * Get the name of the tool's index route.
30
     * This is also the name of the associated model.
31
     * @return string
32
     * @codeCoverageIgnore
33
     */
34
    public function getIndexRoute(): string
35
    {
36
        return 'Pages';
37
    }
38
39
    /**
40
     * PagesController constructor.
41
     * @param RequestStack $requestStack
42
     * @param ContainerInterface $container
43
     * @param I18nHelper $i18n
44
     */
45 1
    public function __construct(RequestStack $requestStack, ContainerInterface $container, I18nHelper $i18n)
46
    {
47
        // Causes the tool to redirect to the index page if the user has too high of an edit count.
48 1
        $this->tooHighEditCountAction = $this->getIndexRoute();
49
50
        // The countPagesApi action is exempt from the edit count limitation.
51 1
        $this->tooHighEditCountActionBlacklist = ['countPagesApi'];
52
53 1
        parent::__construct($requestStack, $container, $i18n);
54 1
    }
55
56
    /**
57
     * Display the form.
58
     * @Route("/pages", name="Pages")
59
     * @Route("/pages/index.php", name="PagesIndexPhp")
60
     * @Route("/pages/{project}", name="PagesProject")
61
     * @return Response
62
     */
63 1
    public function indexAction(): Response
64
    {
65
        // Redirect if at minimum project and username are given.
66 1
        if (isset($this->params['project']) && isset($this->params['username'])) {
67
            return $this->redirectToRoute('PagesResult', $this->params);
68
        }
69
70
        // Otherwise fall through.
71 1
        return $this->render('pages/index.html.twig', array_merge([
72 1
            'xtPageTitle' => 'tool-pages',
73
            'xtSubtitle' => 'tool-pages-desc',
74
            'xtPage' => 'Pages',
75
76
            // Defaults that will get overridden if in $params.
77
            'username' => '',
78
            'namespace' => 0,
79
            'redirects' => 'noredirects',
80
            'deleted' => 'all',
81
            'start' => '',
82
            'end' => '',
83 1
        ], $this->params, ['project' => $this->project]));
84
    }
85
86
    /**
87
     * Every action in this controller (other than 'index') calls this first.
88
     * @return Pages
89
     * @codeCoverageIgnore
90
     */
91
    public function setUPages(): Pages
92
    {
93
        $pagesRepo = new PagesRepository();
94
        $pagesRepo->setContainer($this->container);
95
        $pages = new Pages(
96
            $this->project,
97
            $this->user,
98
            $this->namespace,
99
            $this->redirects,
100
            $this->deleted,
101
            $this->start,
102
            $this->end,
103
            $this->offset
104
        );
105
        $pages->setRepository($pagesRepo);
106
107
        $pages->prepareData();
108
109
        return $pages;
110
    }
111
112
    /**
113
     * Display the results.
114
     * @Route(
115
     *     "/pages/{project}/{username}/{namespace}/{redirects}/{deleted}/{start}/{end}/{offset}",
116
     *     name="PagesResult",
117
     *     requirements={
118
     *         "namespace"="|all|\d+",
119
     *         "redirects"="|[^/]+",
120
     *         "deleted"="|all|live|deleted",
121
     *         "start"="|\d{4}-\d{2}-\d{2}",
122
     *         "end"="|\d{4}-\d{2}-\d{2}",
123
     *         "offset"="|\d+",
124
     *     },
125
     *     defaults={
126
     *         "namespace"=0,
127
     *         "start"=false,
128
     *         "end"=false,
129
     *         "offset"=0,
130
     *     }
131
     * )
132
     * @param string $redirects One of 'noredirects', 'onlyredirects' or 'all' for both.
133
     * @param string $deleted One of 'live', 'deleted' or 'all' for both.
134
     * @return RedirectResponse|Response
135
     * @codeCoverageIgnore
136
     */
137
    public function resultAction(string $redirects = 'noredirects', string $deleted = 'all')
138
    {
139
        // Check for legacy values for 'redirects', and redirect
140
        // back with correct values if need be. This could be refactored
141
        // out to XtoolsController, but this is the only tool in the suite
142
        // that deals with redirects, so we'll keep it confined here.
143
        $validRedirects = ['', 'noredirects', 'onlyredirects', 'all'];
144
        if ('none' === $redirects || !in_array($redirects, $validRedirects)) {
145
            return $this->redirectToRoute('PagesResult', [
146
                'redirects' => 'noredirects',
147
                'deleted' => $deleted,
148
                'offset' => $this->offset,
149
            ]);
150
        }
151
152
        $pages = $this->setUPages();
153
154
        $ret = [
155
            'xtPage' => 'Pages',
156
            'xtTitle' => $this->user->getUsername(),
157
            'summaryColumns' => $this->getSummaryColumns($pages),
158
            'pages' => $pages,
159
        ];
160
161
        if ('PagePile' === $this->request->query->get('format')) {
162
            return $this->getPagepileResult($this->project, $pages);
163
        }
164
165
        // Output the relevant format template.
166
        return $this->getFormattedResponse('pages/result', $ret);
167
    }
168
169
    /**
170
     * What columns to show in namespace totals table.
171
     * @param Pages $pages The Pages instance.
172
     * @return string[]
173
     * @codeCoverageIgnore
174
     */
175
    private function getSummaryColumns(Pages $pages): array
176
    {
177
        $summaryColumns = ['namespace'];
178
        if ('deleted' === $pages->getDeleted()) {
179
            // Showing only deleted pages shows only the deleted column, as redirects are non-applicable.
180
            $summaryColumns[] = 'deleted';
181
        } elseif ('onlyredirects' == $pages->getRedirects()) {
182
            // Don't show redundant pages column if only getting data on redirects or deleted pages.
183
            $summaryColumns[] = 'redirects';
184
        } elseif ('noredirects' == $pages->getRedirects()) {
185
            // Don't show redundant redirects column if only getting data on non-redirects.
186
            $summaryColumns[] = 'pages';
187
        } else {
188
            // Order is important here.
189
            $summaryColumns[] = 'pages';
190
            $summaryColumns[] = 'redirects';
191
        }
192
193
        // Show deleted column only when both deleted and live pages are visible.
194
        if ('all' === $pages->getDeleted()) {
195
            $summaryColumns[] = 'deleted';
196
        }
197
198
        return $summaryColumns;
199
    }
200
201
    /**
202
     * Create a PagePile for the given pages, and get a Redirect to that PagePile.
203
     * @param Project $project
204
     * @param Pages $pages
205
     * @return RedirectResponse
206
     * @throws HttpException
207
     * @see https://tools.wmflabs.org/pagepile/
208
     * @codeCoverageIgnore
209
     */
210
    private function getPagepileResult(Project $project, Pages $pages): RedirectResponse
211
    {
212
        $namespaces = $project->getNamespaces();
213
        $pageTitles = [];
214
215
        foreach (array_values($pages->getResults()) as $pagesData) {
216
            foreach ($pagesData as $page) {
217
                if (0 === (int)$page['namespace']) {
218
                    $pageTitles[] = $page['page_title'];
219
                } else {
220
                    $pageTitles[] = (
221
                        $namespaces[$page['namespace']] ?? $this->i18n->msg('unknown')
222
                    ).':'.$page['page_title'];
223
                }
224
            }
225
        }
226
227
        $pileId = $this->createPagePile($project, $pageTitles);
228
229
        return new RedirectResponse(
230
            "https://tools.wmflabs.org/pagepile/api.php?id=$pileId&action=get_data&format=html&doit1"
231
        );
232
    }
233
234
    /**
235
     * Create a PagePile with the given titles.
236
     * @param Project $project
237
     * @param string[] $pageTitles
238
     * @return int The PagePile ID.
239
     * @throws HttpException
240
     * @see https://tools.wmflabs.org/pagepile/
241
     * @codeCoverageIgnore
242
     */
243
    private function createPagePile(Project $project, array $pageTitles): int
244
    {
245
        /** @var GuzzleHttp\Client $client */
246
        $client = $this->container->get('eight_points_guzzle.client.xtools');
247
248
        $url = 'https://tools.wmflabs.org/pagepile/api.php';
249
250
        try {
251
            $res = $client->request('GET', $url, ['query' => [
252
                'action' => 'create_pile_with_data',
253
                'wiki' => $project->getDatabaseName(),
254
                'data' => implode("\n", $pageTitles),
255
            ]]);
256
        } catch (GuzzleHttp\Exception\ClientException $e) {
257
            throw new HttpException(
258
                414,
259
                'error-pagepile-too-large'
260
            );
261
        }
262
263
        $ret = json_decode($res->getBody()->getContents(), true);
264
265
        if (!isset($ret['status']) || 'OK' !== $ret['status']) {
266
            throw new HttpException(
267
                500,
268
                'Failed to create PagePile. There may be an issue with the PagePile API.'
269
            );
270
        }
271
272
        return $ret['pile']['id'];
273
    }
274
275
    /************************ API endpoints ************************/
276
277
    /**
278
     * Get a count of the number of pages created by a user,
279
     * including the number that have been deleted and are redirects.
280
     * @Route(
281
     *     "/api/user/pages_count/{project}/{username}/{namespace}/{redirects}/{deleted}/{start}/{end}",
282
     *     name="UserApiPagesCount",
283
     *     requirements={
284
     *         "namespace"="|\d+|all",
285
     *         "redirects"="|noredirects|onlyredirects|all",
286
     *         "deleted"="|all|live|deleted",
287
     *         "start"="|\d{4}-\d{2}-\d{2}",
288
     *         "end"="|\d{4}-\d{2}-\d{2}",
289
     *     },
290
     *     defaults={
291
     *         "namespace"=0,
292
     *         "redirects"="noredirects",
293
     *         "deleted"="all",
294
     *         "start"=false,
295
     *         "end"=false,
296
     *     }
297
     * )
298
     * @param string $redirects One of 'noredirects', 'onlyredirects' or 'all' for both.
299
     * @param string $deleted One of 'live', 'deleted' or 'all' for both.
300
     * @return JsonResponse
301
     * @codeCoverageIgnore
302
     */
303
    public function countPagesApiAction(string $redirects = 'noredirects', string $deleted = 'all'): JsonResponse
304
    {
305
        $this->recordApiUsage('user/pages_count');
306
307
        $pagesRepo = new PagesRepository();
308
        $pagesRepo->setContainer($this->container);
309
        $pages = new Pages(
310
            $this->project,
311
            $this->user,
312
            $this->namespace,
313
            $redirects,
314
            $deleted,
315
            $this->start,
316
            $this->end
317
        );
318
        $pages->setRepository($pagesRepo);
319
320
        $counts = $pages->getCounts();
321
322
        if ('all' !== $this->namespace && isset($counts[$this->namespace])) {
323
            $counts = $counts[$this->namespace];
324
        }
325
326
        return $this->getFormattedApiResponse(['counts' => $counts]);
327
    }
328
329
    /**
330
     * Get the pages created by by a user.
331
     * @Route(
332
     *     "/api/user/pages/{project}/{username}/{namespace}/{redirects}/{deleted}/{start}/{end}/{offset}",
333
     *     name="UserApiPagesCreated",
334
     *     requirements={
335
     *         "namespace"="|\d+|all",
336
     *         "redirects"="|noredirects|onlyredirects|all",
337
     *         "deleted"="|all|live|deleted",
338
     *         "start"="|\d{4}-\d{2}-\d{2}",
339
     *         "end"="|\d{4}-\d{2}-\d{2}",
340
     *     },
341
     *     defaults={
342
     *         "namespace"=0,
343
     *         "redirects"="noredirects",
344
     *         "deleted"="all",
345
     *         "start"=false,
346
     *         "end"=false,
347
     *         "offset"=0,
348
     *     }
349
     * )
350
     * @param string $redirects One of 'noredirects', 'onlyredirects' or 'all' for both.
351
     * @param string $deleted One of 'live', 'deleted' or blank for both.
352
     * @return JsonResponse
353
     * @codeCoverageIgnore
354
     */
355
    public function getPagesApiAction(string $redirects = 'noredirects', string $deleted = 'all'): JsonResponse
356
    {
357
        $this->recordApiUsage('user/pages');
358
359
        $pagesRepo = new PagesRepository();
360
        $pagesRepo->setContainer($this->container);
361
        $pages = new Pages(
362
            $this->project,
363
            $this->user,
364
            $this->namespace,
365
            $redirects,
366
            $deleted,
367
            $this->start,
368
            $this->end,
369
            $this->offset
370
        );
371
        $pages->setRepository($pagesRepo);
372
373
        $pagesList = $pages->getResults();
374
375
        if ('all' !== $this->namespace && isset($pagesList[$this->namespace])) {
376
            $pagesList = $pagesList[$this->namespace];
377
        }
378
379
        $ret = [
380
            'pages' => $pagesList,
381
        ];
382
383
        if ($pages->getNumResults() === $pages->resultsPerPage()) {
384
            $ret['continue'] = $this->offset + 1;
385
        }
386
387
        return $this->getFormattedApiResponse($ret);
388
    }
389
}
390