Passed
Push — master ( 35c320...7deb12 )
by MusikAnimal
04:42
created

PagesController::getIndexRoute()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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