Passed
Push — deletion-summary ( 54f099 )
by MusikAnimal
06:52 queued 33s
created

PagesController::getDeletionSummaryApiAction()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 4
dl 0
loc 10
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace App\Controller;
6
7
use App\Model\Pages;
8
use App\Model\Project;
9
use App\Repository\PagesRepository;
10
use GuzzleHttp\Exception\ClientException;
11
use OpenApi\Annotations as OA;
12
use Symfony\Component\HttpFoundation\JsonResponse;
13
use Symfony\Component\HttpFoundation\RedirectResponse;
14
use Symfony\Component\HttpFoundation\Response;
15
use Symfony\Component\HttpKernel\Exception\HttpException;
16
use Symfony\Component\Routing\Annotation\Route;
17
18
/**
19
 * This controller serves the Pages tool.
20
 */
21
class PagesController extends XtoolsController
22
{
23
    /**
24
     * Get the name of the tool's index route.
25
     * This is also the name of the associated model.
26
     * @inheritDoc
27
     * @codeCoverageIgnore
28
     */
29
    public function getIndexRoute(): string
30
    {
31
        return 'Pages';
32
    }
33
34
    /**
35
     * @inheritDoc
36
     * @codeCoverageIgnore
37
     */
38
    public function tooHighEditCountRoute(): string
39
    {
40
        return $this->getIndexRoute();
41
    }
42
43
    /**
44
     * @inheritDoc
45
     * @codeCoverageIgnore
46
     */
47
    public function tooHighEditCountActionAllowlist(): array
48
    {
49
        return ['countPagesApi'];
50
    }
51
52
    /**
53
     * Display the form.
54
     * @Route("/pages", name="Pages")
55
     * @Route("/pages/index.php", name="PagesIndexPhp")
56
     * @Route("/pages/{project}", name="PagesProject")
57
     * @return Response
58
     */
59
    public function indexAction(): Response
60
    {
61
        // Redirect if at minimum project and username are given.
62
        if (isset($this->params['project']) && isset($this->params['username'])) {
63
            return $this->redirectToRoute('PagesResult', $this->params);
64
        }
65
66
        // Otherwise fall through.
67
        return $this->render('pages/index.html.twig', array_merge([
68
            'xtPageTitle' => 'tool-pages',
69
            'xtSubtitle' => 'tool-pages-desc',
70
            'xtPage' => 'Pages',
71
72
            // Defaults that will get overridden if in $params.
73
            'username' => '',
74
            'namespace' => 0,
75
            'redirects' => 'noredirects',
76
            'deleted' => 'all',
77
            'start' => '',
78
            'end' => '',
79
        ], $this->params, ['project' => $this->project]));
80
    }
81
82
    /**
83
     * Every action in this controller (other than 'index') calls this first.
84
     * @param PagesRepository $pagesRepo
85
     * @param string $redirects One of the Pages::REDIR_ constants.
86
     * @param string $deleted One of the Pages::DEL_ constants.
87
     * @return Pages
88
     * @codeCoverageIgnore
89
     */
90
    protected function setUpPages(PagesRepository $pagesRepo, string $redirects, string $deleted): Pages
91
    {
92
        if ($this->user->isIpRange()) {
0 ignored issues
show
Bug introduced by
The method isIpRange() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

92
        if ($this->user->/** @scrutinizer ignore-call */ isIpRange()) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
93
            $this->params['username'] = $this->user->getUsername();
94
            $this->throwXtoolsException($this->getIndexRoute(), 'error-ip-range-unsupported');
95
        }
96
97
        return new Pages(
98
            $pagesRepo,
99
            $this->project,
100
            $this->user,
0 ignored issues
show
Bug introduced by
It seems like $this->user can also be of type null; however, parameter $user of App\Model\Pages::__construct() does only seem to accept App\Model\User, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

100
            /** @scrutinizer ignore-type */ $this->user,
Loading history...
101
            $this->namespace,
102
            $redirects,
103
            $deleted,
104
            $this->start,
105
            $this->end,
106
            $this->offset
107
        );
108
    }
109
110
    /**
111
     * Display the results.
112
     * @Route(
113
     *     "/pages/{project}/{username}/{namespace}/{redirects}/{deleted}/{start}/{end}/{offset}",
114
     *     name="PagesResult",
115
     *     requirements={
116
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
117
     *         "namespace"="|all|\d+",
118
     *         "redirects"="|[^/]+",
119
     *         "deleted"="|all|live|deleted",
120
     *         "start"="|\d{4}-\d{2}-\d{2}",
121
     *         "end"="|\d{4}-\d{2}-\d{2}",
122
     *         "offset"="|\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}",
123
     *     },
124
     *     defaults={
125
     *         "namespace"=0,
126
     *         "start"=false,
127
     *         "end"=false,
128
     *         "offset"=false,
129
     *     }
130
     * )
131
     * @param PagesRepository $pagesRepo
132
     * @param string $redirects One of the Pages::REDIR_ constants.
133
     * @param string $deleted One of the Pages::DEL_ constants.
134
     * @return RedirectResponse|Response
135
     * @codeCoverageIgnore
136
     */
137
    public function resultAction(
138
        PagesRepository $pagesRepo,
139
        string $redirects = Pages::REDIR_NONE,
140
        string $deleted = Pages::DEL_ALL
141
    ) {
142
        // Check for legacy values for 'redirects', and redirect
143
        // back with correct values if need be. This could be refactored
144
        // out to XtoolsController, but this is the only tool in the suite
145
        // that deals with redirects, so we'll keep it confined here.
146
        $validRedirects = ['', Pages::REDIR_NONE, Pages::REDIR_ONLY, Pages::REDIR_ALL];
147
        if ('none' === $redirects || !in_array($redirects, $validRedirects)) {
148
            return $this->redirectToRoute('PagesResult', array_merge($this->params, [
149
                'redirects' => Pages::REDIR_NONE,
150
                'deleted' => $deleted,
151
                'offset' => $this->offset,
152
            ]));
153
        }
154
155
        $pages = $this->setUpPages($pagesRepo, $redirects, $deleted);
156
        $pages->prepareData();
157
158
        $ret = [
159
            'xtPage' => 'Pages',
160
            'xtTitle' => $this->user->getUsername(),
161
            'summaryColumns' => $pages->getSummaryColumns(),
162
            'pages' => $pages,
163
        ];
164
165
        if ('PagePile' === $this->request->query->get('format')) {
166
            return $this->getPagepileResult($this->project, $pages);
167
        }
168
169
        // Output the relevant format template.
170
        return $this->getFormattedResponse('pages/result', $ret);
171
    }
172
173
    /**
174
     * Create a PagePile for the given pages, and get a Redirect to that PagePile.
175
     * @param Project $project
176
     * @param Pages $pages
177
     * @return RedirectResponse
178
     * @throws HttpException
179
     * @see https://pagepile.toolforge.org
180
     * @codeCoverageIgnore
181
     */
182
    private function getPagepileResult(Project $project, Pages $pages): RedirectResponse
183
    {
184
        $namespaces = $project->getNamespaces();
185
        $pageTitles = [];
186
187
        foreach (array_values($pages->getResults()) as $pagesData) {
188
            foreach ($pagesData as $page) {
189
                if (0 === (int)$page['namespace']) {
190
                    $pageTitles[] = $page['page_title'];
191
                } else {
192
                    $pageTitles[] = (
193
                        $namespaces[$page['namespace']] ?? $this->i18n->msg('unknown')
194
                    ).':'.$page['page_title'];
195
                }
196
            }
197
        }
198
199
        $pileId = $this->createPagePile($project, $pageTitles);
200
201
        return new RedirectResponse(
202
            "https://pagepile.toolforge.org/api.php?id=$pileId&action=get_data&format=html&doit1"
203
        );
204
    }
205
206
    /**
207
     * Create a PagePile with the given titles.
208
     * @param Project $project
209
     * @param string[] $pageTitles
210
     * @return int The PagePile ID.
211
     * @throws HttpException
212
     * @see https://pagepile.toolforge.org/
213
     * @codeCoverageIgnore
214
     */
215
    private function createPagePile(Project $project, array $pageTitles): int
216
    {
217
        $url = 'https://pagepile.toolforge.org/api.php';
218
219
        try {
220
            $res = $this->guzzle->request('GET', $url, ['query' => [
221
                'action' => 'create_pile_with_data',
222
                'wiki' => $project->getDatabaseName(),
223
                'data' => implode("\n", $pageTitles),
224
            ]]);
225
        } catch (ClientException $e) {
226
            throw new HttpException(
227
                414,
228
                'error-pagepile-too-large'
229
            );
230
        }
231
232
        $ret = json_decode($res->getBody()->getContents(), true);
233
234
        if (!isset($ret['status']) || 'OK' !== $ret['status']) {
235
            throw new HttpException(
236
                500,
237
                'Failed to create PagePile. There may be an issue with the PagePile API.'
238
            );
239
        }
240
241
        return $ret['pile']['id'];
242
    }
243
244
    /************************ API endpoints ************************/
245
246
    /**
247
     * Count the number of pages created by a user.
248
     * @Route(
249
     *     "/api/user/pages_count/{project}/{username}/{namespace}/{redirects}/{deleted}/{start}/{end}",
250
     *     name="UserApiPagesCount",
251
     *     requirements={
252
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
253
     *         "namespace"="|\d+|all",
254
     *         "redirects"="|noredirects|onlyredirects|all",
255
     *         "deleted"="|all|live|deleted",
256
     *         "start"="|\d{4}-\d{2}-\d{2}",
257
     *         "end"="|\d{4}-\d{2}-\d{2}",
258
     *     },
259
     *     defaults={
260
     *         "namespace"=0,
261
     *         "redirects"="noredirects",
262
     *         "deleted"="all",
263
     *         "start"=false,
264
     *         "end"=false,
265
     *     },
266
     *     methods={"GET"}
267
     * )
268
     * @OA\Tag(name="User API")
269
     * @OA\Get(description="Get the number of pages created by a user, keyed by namespace.")
270
     * @OA\Parameter(ref="#/components/parameters/Project")
271
     * @OA\Parameter(ref="#/components/parameters/UsernameOrSingleIp")
272
     * @OA\Parameter(ref="#/components/parameters/Namespace")
273
     * @OA\Parameter(ref="#/components/parameters/Redirects")
274
     * @OA\Parameter(ref="#/components/parameters/Deleted")
275
     * @OA\Parameter(ref="#/components/parameters/Start")
276
     * @OA\Parameter(ref="#/components/parameters/End")
277
     * @OA\Response(
278
     *     response=200,
279
     *     description="Page counts",
280
     *     @OA\JsonContent(
281
     *         @OA\Property(property="project", ref="#/components/parameters/Project/schema"),
282
     *         @OA\Property(property="username", ref="#/components/parameters/UsernameOrSingleIp/schema"),
283
     *         @OA\Property(property="namespace", ref="#/components/schemas/Namespace"),
284
     *         @OA\Property(property="redirects", ref="#/components/parameters/Redirects/schema"),
285
     *         @OA\Property(property="deleted", ref="#components/parameters/Deleted/schema"),
286
     *         @OA\Property(property="start", ref="#components/parameters/start/schema"),
287
     *         @OA\Property(property="end", ref="#components/parameters/end/schema"),
288
     *         @OA\Property(property="counts", type="object", example={
289
     *             "0": {
290
     *                 "count": 5,
291
     *                 "total_length": 500,
292
     *                 "avg_length": 100
293
     *             },
294
     *             "2": {
295
     *                 "count": 1,
296
     *                 "total_length": 200,
297
     *                 "avg_length": 200
298
     *             }
299
     *         }),
300
     *         @OA\Property(property="elapsed_time", ref="#/components/schemas/elapsed_time")
301
     *     )
302
     * )
303
     * @OA\Response(response=404, ref="#/components/responses/404")
304
     * @OA\Response(response=501, ref="#/components/responses/501")
305
     * @OA\Response(response=503, ref="#/components/responses/503")
306
     * @OA\Response(response=504, ref="#/components/responses/504")
307
     * @param PagesRepository $pagesRepo
308
     * @param string $redirects One of 'noredirects', 'onlyredirects' or 'all' for both.
309
     * @param string $deleted One of 'live', 'deleted' or 'all' for both.
310
     * @return JsonResponse
311
     * @codeCoverageIgnore
312
     */
313
    public function countPagesApiAction(
314
        PagesRepository $pagesRepo,
315
        string $redirects = Pages::REDIR_NONE,
316
        string $deleted = Pages::DEL_ALL
317
    ): JsonResponse {
318
        $this->recordApiUsage('user/pages_count');
319
320
        $pages = $this->setUpPages($pagesRepo, $redirects, $deleted);
321
        $counts = $pages->getCounts();
322
323
        return $this->getFormattedApiResponse(['counts' => (object)$counts]);
324
    }
325
326
    /**
327
     * Get the pages created by by a user.
328
     * @Route(
329
     *     "/api/user/pages/{project}/{username}/{namespace}/{redirects}/{deleted}/{start}/{end}/{offset}",
330
     *     name="UserApiPagesCreated",
331
     *     requirements={
332
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
333
     *         "namespace"="|\d+|all",
334
     *         "redirects"="|noredirects|onlyredirects|all",
335
     *         "deleted"="|all|live|deleted",
336
     *         "start"="|\d{4}-\d{2}-\d{2}",
337
     *         "end"="|\d{4}-\d{2}-\d{2}",
338
     *         "offset"="|\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}",
339
     *     },
340
     *     defaults={
341
     *         "namespace"=0,
342
     *         "redirects"="noredirects",
343
     *         "deleted"="all",
344
     *         "start"=false,
345
     *         "end"=false,
346
     *         "offset"=false,
347
     *     },
348
     *     methods={"GET"}
349
     * )
350
     * @OA\Tag(name="User API")
351
     * @OA\Get(description="Get pages created by a user, keyed by namespace.")
352
     * @OA\Parameter(ref="#/components/parameters/Project")
353
     * @OA\Parameter(ref="#/components/parameters/UsernameOrSingleIp")
354
     * @OA\Parameter(ref="#/components/parameters/Namespace")
355
     * @OA\Parameter(ref="#/components/parameters/Redirects")
356
     * @OA\Parameter(ref="#/components/parameters/Deleted")
357
     * @OA\Parameter(ref="#/components/parameters/Start")
358
     * @OA\Parameter(ref="#/components/parameters/End")
359
     * @OA\Parameter(ref="#/components/parameters/Offset")
360
     * @OA\Parameter(name="format", in="query",
361
     *     @OA\Schema(default="json", type="string", enum={"json","wikitext","pagepile","csv","tsv"})
362
     * )
363
     * @OA\Response(
364
     *     response=200,
365
     *     description="Pages created",
366
     *     @OA\JsonContent(
367
     *         @OA\Property(property="project", ref="#/components/parameters/Project/schema"),
368
     *         @OA\Property(property="username", ref="#/components/parameters/UsernameOrSingleIp/schema"),
369
     *         @OA\Property(property="namespace", ref="#/components/schemas/Namespace"),
370
     *         @OA\Property(property="redirects", ref="#/components/parameters/Redirects/schema"),
371
     *         @OA\Property(property="deleted", ref="#components/parameters/Deleted/schema"),
372
     *         @OA\Property(property="start", ref="#components/parameters/Start/schema"),
373
     *         @OA\Property(property="end", ref="#components/parameters/End/schema"),
374
     *         @OA\Property(property="pages", type="object",
375
     *             @OA\Property(property="namespace ID", ref="#/components/schemas/PageCreation")
376
     *         ),
377
     *         @OA\Property(property="elapsed_time", ref="#/components/schemas/elapsed_time")
378
     *     )
379
     * )
380
     * @OA\Response(response=404, ref="#/components/responses/404")
381
     * @OA\Response(response=501, ref="#/components/responses/501")
382
     * @OA\Response(response=503, ref="#/components/responses/503")
383
     * @OA\Response(response=504, ref="#/components/responses/504")
384
     * @param PagesRepository $pagesRepo
385
     * @param string $redirects One of 'noredirects', 'onlyredirects' or 'all' for both.
386
     * @param string $deleted One of 'live', 'deleted' or blank for both.
387
     * @return JsonResponse
388
     * @codeCoverageIgnore
389
     */
390
    public function getPagesApiAction(
391
        PagesRepository $pagesRepo,
392
        string $redirects = Pages::REDIR_NONE,
393
        string $deleted = Pages::DEL_ALL
394
    ): JsonResponse {
395
        $this->recordApiUsage('user/pages');
396
397
        $pages = $this->setUpPages($pagesRepo, $redirects, $deleted);
398
        $ret = ['pages' => $pages->getResults()];
399
400
        if ($pages->getNumResults() === $pages->resultsPerPage()) {
401
            $ret['continue'] = $pages->getLastTimestamp();
402
        }
403
404
        return $this->getFormattedApiResponse($ret);
405
    }
406
407
    /**
408
     * Get the deletion summary to be shown when hovering over the "Deleted" text in the UI.
409
     * @Route(
410
     *     "/pages/deletion_summary/{project}/{username}/{namespace}/{pageTitle}/{timestamp}",
411
     *     name="PagesApiDeletionSummary"
412
     * )
413
     * @return JsonResponse
414
     * @internal
415
     */
416
    public function getDeletionSummaryApiAction(
417
        PagesRepository $pagesRepo,
418
        int $namespace,
419
        string $pageTitle,
420
        string $timestamp
421
    ): JsonResponse {
422
        // Redirect/deleted options actually don't matter here.
423
        $pages = $this->setUpPages($pagesRepo, Pages::REDIR_NONE, Pages::DEL_ALL);
424
        return $this->getFormattedApiResponse([
425
            'summary' => $pages->getDeletionSummary($this->project, $namespace, $pageTitle, $timestamp),
0 ignored issues
show
Bug introduced by
$this->project of type App\Model\Project is incompatible with the type integer expected by parameter $namespace of App\Model\Pages::getDeletionSummary(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

425
            'summary' => $pages->getDeletionSummary(/** @scrutinizer ignore-type */ $this->project, $namespace, $pageTitle, $timestamp),
Loading history...
Unused Code introduced by
The call to App\Model\Pages::getDeletionSummary() has too many arguments starting with $timestamp. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

425
            'summary' => $pages->/** @scrutinizer ignore-call */ getDeletionSummary($this->project, $namespace, $pageTitle, $timestamp),

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
426
        ]);
427
    }
428
}
429