Passed
Push — master ( 09bbf5...1b30d6 )
by MusikAnimal
05:04
created

PagesController::getSummaryColumns()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 24
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
cc 5
eloc 13
nc 8
nop 1
dl 0
loc 24
ccs 0
cts 0
cp 0
crap 30
rs 8.5125
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\ProjectRepository;
18
use Xtools\UserRepository;
19
use Xtools\Pages;
20
use Xtools\Project;
21
22
/**
23
 * This controller serves the Pages tool.
24
 */
25
class PagesController extends XtoolsController
26
{
27
    const RESULTS_PER_PAGE = 1000;
28
29
    /**
30
     * Get the tool's shortname.
31
     * @return string
32
     * @codeCoverageIgnore
33
     */
34
    public function getToolShortname()
35
    {
36
        return 'pages';
37
    }
38
39
    /**
40
     * Display the form.
41
     * @Route("/pages", name="pages")
42
     * @Route("/pages", name="Pages")
43
     * @Route("/pages/", name="PagesSlash")
44
     * @Route("/pages/index.php", name="PagesIndexPhp")
45
     * @Route("/pages/{project}", name="PagesProject")
46
     * @param Request $request
47
     * @return Response
48
     */
49 1
    public function indexAction(Request $request)
50
    {
51 1
        $params = $this->parseQueryParams($request);
52
53
        // Redirect if at minimum project and username are given.
54 1
        if (isset($params['project']) && isset($params['username'])) {
55
            return $this->redirectToRoute('PagesResult', $params);
56
        }
57
58
        // Convert the given project (or default project) into a Project instance.
59 1
        $params['project'] = $this->getProjectFromQuery($params);
60
61
        // Otherwise fall through.
62 1
        return $this->render('pages/index.html.twig', array_merge([
63 1
            'xtPageTitle' => 'tool-pages',
64
            'xtSubtitle' => 'tool-pages-desc',
65
            'xtPage' => 'pages',
66
67
            // Defaults that will get overriden if in $params.
68
            'namespace' => 0,
69
            'redirects' => 'noredirects',
70
            'deleted' => 'all'
71 1
        ], $params));
72
    }
73
74
    /**
75
     * Display the results.
76
     * @Route(
77
     *     "/pages/{project}/{username}/{namespace}/{redirects}/{deleted}/{offset}", 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
The condition $ret instanceof Symfony\...dation\RedirectResponse can never be false since $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
        $pages = new Pages(
124
            $project,
125
            $user,
126
            $namespace,
127
            $redirects,
128
            $deleted,
129
            $offset
130
        );
131
        $pages->prepareData();
132
133
        $ret = [
134
            'xtPage' => 'pages',
135
            'xtTitle' => $user->getUsername(),
136
            'project' => $project,
137
            'user' => $user,
138
            'summaryColumns' => $this->getSummaryColumns($pages),
139
            'pages' => $pages,
140
            'namespace' => $namespace,
141
        ];
142
143
        if ($request->query->get('format') === 'pagepile') {
144
            return $this->getPagepileResult($project, $pages);
145
        }
146
147
        // Output the relevant format template.
148
        return $this->getFormattedReponse($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
    protected 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 HttpException
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("/api/user/pages_count/{project}/{username}/{namespace}/{redirects}/{deleted}", name="PagesApiCount",
259
     *     requirements={"namespace"="|\d+|all"})
260
     * @param Request $request
261
     * @param int|string $namespace The ID of the namespace of the page, or 'all' for all namespaces.
262
     * @param string $redirects One of 'noredirects', 'onlyredirects' or 'all' for both.
263
     * @param string $deleted One of 'live', 'deleted' or 'all' for both.
264
     * @return Response
265
     * @codeCoverageIgnore
266
     */
267
    public function countPagesApiAction(Request $request, $namespace = 0, $redirects = 'noredirects', $deleted = 'all')
268
    {
269
        $this->recordApiUsage('user/pages_count');
270
271
        $ret = $this->validateProjectAndUser($request);
272
        if ($ret instanceof RedirectResponse) {
0 ignored issues
show
introduced by
The condition $ret instanceof Symfony\...dation\RedirectResponse can never be false since $ret is always a sub-type of Symfony\Component\HttpFoundation\RedirectResponse.
Loading history...
273
            return $ret;
274
        } else {
275
            list($project, $user) = $ret;
276
        }
277
278
        $pages = new Pages(
279
            $project,
280
            $user,
281
            $namespace,
282
            $redirects,
283
            $deleted
284
        );
285
286
        $response = new JsonResponse();
287
        $response->setEncodingOptions(JSON_NUMERIC_CHECK);
288
        $response->setStatusCode(Response::HTTP_OK);
289
290
        $counts = $pages->getCounts();
291
292
        if ($namespace !== 'all' && isset($counts[$namespace])) {
293
            $counts = $counts[$namespace];
294
        }
295
296
        $ret = [
297
            'project' => $project->getDomain(),
298
            'username' => $user->getUsername(),
299
            'namespace' => $namespace,
300
            'redirects' => $redirects,
301
            'deleted' => $deleted,
302
            'counts' => $counts,
303
        ];
304
305
        $response->setData($ret);
306
307
        return $response;
308
    }
309
310
    /**
311
     * Get the pages created by by a user.
312
     * @Route("/api/user/pages/{project}/{username}/{namespace}/{redirects}/{deleted}/{offset}", name="PagesApi",
313
     *     requirements={"namespace"="|\d+|all"})
314
     * @param Request $request
315
     * @param int|string $namespace The ID of the namespace of the page, or 'all' for all namespaces.
316
     * @param string $redirects One of 'noredirects', 'onlyredirects' or 'all' for both.
317
     * @param string $deleted One of 'live', 'deleted' or blank for both.
318
     * @param int $offset Which page of results to show.
319
     * @return Response
320
     * @codeCoverageIgnore
321
     */
322
    public function getPagesApiAction(
323
        Request $request,
324
        $namespace = 0,
325
        $redirects = 'noredirects',
326
        $deleted = 'all',
327
        $offset = 0
328
    ) {
329
        $this->recordApiUsage('user/pages');
330
331
        // Second parameter causes it return a Redirect to the index if the user has too many edits.
332
        $ret = $this->validateProjectAndUser($request, 'pages');
333
        if ($ret instanceof RedirectResponse) {
0 ignored issues
show
introduced by
The condition $ret instanceof Symfony\...dation\RedirectResponse can never be false since $ret is always a sub-type of Symfony\Component\HttpFoundation\RedirectResponse.
Loading history...
334
            return $ret;
335
        } else {
336
            list($project, $user) = $ret;
337
        }
338
339
        $pages = new Pages(
340
            $project,
341
            $user,
342
            $namespace,
343
            $redirects,
344
            $deleted,
345
            $offset
346
        );
347
348
        $response = new JsonResponse();
349
        $response->setEncodingOptions(JSON_NUMERIC_CHECK);
350
        $response->setStatusCode(Response::HTTP_OK);
351
352
        $pagesList = $pages->getResults();
353
354
        if ($namespace !== 'all' && isset($pagesList[$namespace])) {
355
            $pagesList = $pagesList[$namespace];
356
        }
357
358
        $ret = [
359
            'project' => $project->getDomain(),
360
            'username' => $user->getUsername(),
361
            'namespace' => $namespace,
362
            'redirects' => $redirects,
363
            'deleted' => $deleted
364
        ];
365
366
        if ($pages->getNumResults() === $pages->resultsPerPage()) {
367
            $ret['continue'] = $offset + 1;
368
        }
369
370
        $ret['pages'] = $pagesList;
371
372
        $response->setData($ret);
373
374
        return $response;
375
    }
376
}
377