Passed
Push — master ( e6ce91...429907 )
by MusikAnimal
04:46
created

PagesController   A

Complexity

Total Complexity 29

Size/Duplication

Total Lines 331
Duplicated Lines 0 %

Test Coverage

Coverage 90.91%

Importance

Changes 0
Metric Value
eloc 121
dl 0
loc 331
ccs 10
cts 11
cp 0.9091
rs 10
c 0
b 0
f 0
wmc 29

9 Methods

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