Passed
Branch master (648141)
by MusikAnimal
10:42
created

PagesController::setUpPages()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 17
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 13
c 0
b 0
f 0
nc 1
nop 2
dl 0
loc 17
ccs 0
cts 0
cp 0
crap 2
rs 9.8333
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
     * @param string $redirects One of 'noredirects', 'onlyredirects' or 'all' for both.
89
     * @param string $deleted One of 'live', 'deleted' or 'all' for both.
90
     * @return Pages
91
     * @codeCoverageIgnore
92
     */
93
    public function setUpPages(string $redirects, string $deleted): Pages
94
    {
95
        $pagesRepo = new PagesRepository();
96
        $pagesRepo->setContainer($this->container);
97
        $pages = new Pages(
98
            $this->project,
99
            $this->user,
100
            $this->namespace,
101
            $redirects,
102
            $deleted,
103
            $this->start,
104
            $this->end,
105
            $this->offset
106
        );
107
        $pages->setRepository($pagesRepo);
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', array_merge($this->params, [
146
                'redirects' => 'noredirects',
147
                'deleted' => $deleted,
148
                'offset' => $this->offset,
149
            ]));
150
        }
151
152
        $pages = $this->setUpPages($redirects, $deleted);
153
        $pages->prepareData();
154
155
        $ret = [
156
            'xtPage' => 'Pages',
157
            'xtTitle' => $this->user->getUsername(),
158
            'summaryColumns' => $this->getSummaryColumns($pages),
159
            'pages' => $pages,
160
        ];
161
162
        if ('PagePile' === $this->request->query->get('format')) {
163
            return $this->getPagepileResult($this->project, $pages);
164
        }
165
166
        // Output the relevant format template.
167
        return $this->getFormattedResponse('pages/result', $ret);
168
    }
169
170
    /**
171
     * What columns to show in namespace totals table.
172
     * @param Pages $pages The Pages instance.
173
     * @return string[]
174
     * @codeCoverageIgnore
175
     */
176
    private function getSummaryColumns(Pages $pages): array
177
    {
178
        $summaryColumns = ['namespace'];
179
        if ('deleted' === $pages->getDeleted()) {
180
            // Showing only deleted pages shows only the deleted column, as redirects are non-applicable.
181
            $summaryColumns[] = 'deleted';
182
        } elseif ('onlyredirects' == $pages->getRedirects()) {
183
            // Don't show redundant pages column if only getting data on redirects or deleted pages.
184
            $summaryColumns[] = 'redirects';
185
        } elseif ('noredirects' == $pages->getRedirects()) {
186
            // Don't show redundant redirects column if only getting data on non-redirects.
187
            $summaryColumns[] = 'pages';
188
        } else {
189
            // Order is important here.
190
            $summaryColumns[] = 'pages';
191
            $summaryColumns[] = 'redirects';
192
        }
193
194
        // Show deleted column only when both deleted and live pages are visible.
195
        if ('all' === $pages->getDeleted()) {
196
            $summaryColumns[] = 'deleted';
197
        }
198
199
        $summaryColumns[] = 'total-page-size';
200
        $summaryColumns[] = 'average-page-size';
201
202
        return $summaryColumns;
203
    }
204
205
    /**
206
     * Create a PagePile for the given pages, and get a Redirect to that PagePile.
207
     * @param Project $project
208
     * @param Pages $pages
209
     * @return RedirectResponse
210
     * @throws HttpException
211
     * @see https://tools.wmflabs.org/pagepile/
212
     * @codeCoverageIgnore
213
     */
214
    private function getPagepileResult(Project $project, Pages $pages): RedirectResponse
215
    {
216
        $namespaces = $project->getNamespaces();
217
        $pageTitles = [];
218
219
        foreach (array_values($pages->getResults()) as $pagesData) {
220
            foreach ($pagesData as $page) {
221
                if (0 === (int)$page['namespace']) {
222
                    $pageTitles[] = $page['page_title'];
223
                } else {
224
                    $pageTitles[] = (
225
                        $namespaces[$page['namespace']] ?? $this->i18n->msg('unknown')
226
                    ).':'.$page['page_title'];
227
                }
228
            }
229
        }
230
231
        $pileId = $this->createPagePile($project, $pageTitles);
232
233
        return new RedirectResponse(
234
            "https://tools.wmflabs.org/pagepile/api.php?id=$pileId&action=get_data&format=html&doit1"
235
        );
236
    }
237
238
    /**
239
     * Create a PagePile with the given titles.
240
     * @param Project $project
241
     * @param string[] $pageTitles
242
     * @return int The PagePile ID.
243
     * @throws HttpException
244
     * @see https://tools.wmflabs.org/pagepile/
245
     * @codeCoverageIgnore
246
     */
247
    private function createPagePile(Project $project, array $pageTitles): int
248
    {
249
        /** @var GuzzleHttp\Client $client */
250
        $client = $this->container->get('eight_points_guzzle.client.xtools');
251
252
        $url = 'https://tools.wmflabs.org/pagepile/api.php';
253
254
        try {
255
            $res = $client->request('GET', $url, ['query' => [
256
                'action' => 'create_pile_with_data',
257
                'wiki' => $project->getDatabaseName(),
258
                'data' => implode("\n", $pageTitles),
259
            ]]);
260
        } catch (GuzzleHttp\Exception\ClientException $e) {
261
            throw new HttpException(
262
                414,
263
                'error-pagepile-too-large'
264
            );
265
        }
266
267
        $ret = json_decode($res->getBody()->getContents(), true);
268
269
        if (!isset($ret['status']) || 'OK' !== $ret['status']) {
270
            throw new HttpException(
271
                500,
272
                'Failed to create PagePile. There may be an issue with the PagePile API.'
273
            );
274
        }
275
276
        return $ret['pile']['id'];
277
    }
278
279
    /************************ API endpoints ************************/
280
281
    /**
282
     * Get a count of the number of pages created by a user,
283
     * including the number that have been deleted and are redirects.
284
     * @Route(
285
     *     "/api/user/pages_count/{project}/{username}/{namespace}/{redirects}/{deleted}/{start}/{end}",
286
     *     name="UserApiPagesCount",
287
     *     requirements={
288
     *         "namespace"="|\d+|all",
289
     *         "redirects"="|noredirects|onlyredirects|all",
290
     *         "deleted"="|all|live|deleted",
291
     *         "start"="|\d{4}-\d{2}-\d{2}",
292
     *         "end"="|\d{4}-\d{2}-\d{2}",
293
     *     },
294
     *     defaults={
295
     *         "namespace"=0,
296
     *         "redirects"="noredirects",
297
     *         "deleted"="all",
298
     *         "start"=false,
299
     *         "end"=false,
300
     *     }
301
     * )
302
     * @param string $redirects One of 'noredirects', 'onlyredirects' or 'all' for both.
303
     * @param string $deleted One of 'live', 'deleted' or 'all' for both.
304
     * @return JsonResponse
305
     * @codeCoverageIgnore
306
     */
307
    public function countPagesApiAction(string $redirects = 'noredirects', string $deleted = 'all'): JsonResponse
308
    {
309
        $this->recordApiUsage('user/pages_count');
310
311
        $pages = $this->setUpPages($redirects, $deleted);
312
        $counts = $pages->getCounts();
313
314
        if ('all' !== $this->namespace && isset($counts[$this->namespace])) {
315
            $counts = $counts[$this->namespace];
316
        }
317
318
        return $this->getFormattedApiResponse(['counts' => $counts]);
319
    }
320
321
    /**
322
     * Get the pages created by by a user.
323
     * @Route(
324
     *     "/api/user/pages/{project}/{username}/{namespace}/{redirects}/{deleted}/{start}/{end}/{offset}",
325
     *     name="UserApiPagesCreated",
326
     *     requirements={
327
     *         "namespace"="|\d+|all",
328
     *         "redirects"="|noredirects|onlyredirects|all",
329
     *         "deleted"="|all|live|deleted",
330
     *         "start"="|\d{4}-\d{2}-\d{2}",
331
     *         "end"="|\d{4}-\d{2}-\d{2}",
332
     *     },
333
     *     defaults={
334
     *         "namespace"=0,
335
     *         "redirects"="noredirects",
336
     *         "deleted"="all",
337
     *         "start"=false,
338
     *         "end"=false,
339
     *         "offset"=0,
340
     *     }
341
     * )
342
     * @param string $redirects One of 'noredirects', 'onlyredirects' or 'all' for both.
343
     * @param string $deleted One of 'live', 'deleted' or blank for both.
344
     * @return JsonResponse
345
     * @codeCoverageIgnore
346
     */
347
    public function getPagesApiAction(string $redirects = 'noredirects', string $deleted = 'all'): JsonResponse
348
    {
349
        $this->recordApiUsage('user/pages');
350
351
        $pages = $this->setUpPages($redirects, $deleted);
352
        $pagesList = $pages->getResults();
353
354
        if ('all' !== $this->namespace && isset($pagesList[$this->namespace])) {
355
            $pagesList = $pagesList[$this->namespace];
356
        }
357
358
        $ret = [
359
            'pages' => $pagesList,
360
        ];
361
362
        if ($pages->getNumResults() === $pages->resultsPerPage()) {
363
            $ret['continue'] = $this->offset + 1;
364
        }
365
366
        return $this->getFormattedApiResponse($ret);
367
    }
368
}
369