Passed
Push — flex ( 91a41f )
by MusikAnimal
07:11
created

PagesController::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 9
dl 0
loc 13
rs 10
c 0
b 0
f 0

1 Method

Rating   Name   Duplication   Size   Complexity  
A PagesController::tooHighEditCountActionAllowlist() 0 3 1

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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 Symfony\Component\HttpFoundation\JsonResponse;
12
use Symfony\Component\HttpFoundation\RedirectResponse;
13
use Symfony\Component\HttpFoundation\Response;
14
use Symfony\Component\HttpKernel\Exception\HttpException;
15
use Symfony\Component\Routing\Annotation\Route;
16
17
/**
18
 * This controller serves the Pages tool.
19
 */
20
class PagesController extends XtoolsController
21
{
22
    /**
23
     * Get the name of the tool's index route.
24
     * This is also the name of the associated model.
25
     * @inheritDoc
26
     * @codeCoverageIgnore
27
     */
28
    public function getIndexRoute(): string
29
    {
30
        return 'Pages';
31
    }
32
33
    /**
34
     * @inheritDoc
35
     * @codeCoverageIgnore
36
     */
37
    public function tooHighEditCountRoute(): string
38
    {
39
        return $this->getIndexRoute();
40
    }
41
42
    /**
43
     * @inheritDoc
44
     * @codeCoverageIgnore
45
     */
46
    public function tooHighEditCountActionAllowlist(): array
47
    {
48
        return ['countPagesApi'];
49
    }
50
51
    /**
52
     * Display the form.
53
     * @Route("/pages", name="Pages")
54
     * @Route("/pages/index.php", name="PagesIndexPhp")
55
     * @Route("/pages/{project}", name="PagesProject")
56
     * @return Response
57
     */
58
    public function indexAction(): Response
59
    {
60
        // Redirect if at minimum project and username are given.
61
        if (isset($this->params['project']) && isset($this->params['username'])) {
62
            return $this->redirectToRoute('PagesResult', $this->params);
63
        }
64
65
        // Otherwise fall through.
66
        return $this->render('pages/index.html.twig', array_merge([
67
            'xtPageTitle' => 'tool-pages',
68
            'xtSubtitle' => 'tool-pages-desc',
69
            'xtPage' => 'Pages',
70
71
            // Defaults that will get overridden if in $params.
72
            'username' => '',
73
            'namespace' => 0,
74
            'redirects' => 'noredirects',
75
            'deleted' => 'all',
76
            'start' => '',
77
            'end' => '',
78
        ], $this->params, ['project' => $this->project]));
79
    }
80
81
    /**
82
     * Every action in this controller (other than 'index') calls this first.
83
     * @param PagesRepository $pagesRepo
84
     * @param string $redirects One of 'noredirects', 'onlyredirects' or 'all' for both.
85
     * @param string $deleted One of 'live', 'deleted' or 'all' for both.
86
     * @return Pages
87
     * @codeCoverageIgnore
88
     */
89
    protected function setUpPages(PagesRepository $pagesRepo, string $redirects, string $deleted): Pages
90
    {
91
        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

91
        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...
92
            $this->params['username'] = $this->user->getUsername();
93
            $this->throwXtoolsException($this->getIndexRoute(), 'error-ip-range-unsupported');
94
        }
95
96
        return new Pages(
97
            $pagesRepo,
98
            $this->project,
99
            $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

99
            /** @scrutinizer ignore-type */ $this->user,
Loading history...
100
            $this->namespace,
101
            $redirects,
102
            $deleted,
103
            $this->start,
104
            $this->end,
105
            $this->offset
106
        );
107
    }
108
109
    /**
110
     * Display the results.
111
     * @Route(
112
     *     "/pages/{project}/{username}/{namespace}/{redirects}/{deleted}/{start}/{end}/{offset}",
113
     *     name="PagesResult",
114
     *     requirements={
115
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
116
     *         "namespace"="|all|\d+",
117
     *         "redirects"="|[^/]+",
118
     *         "deleted"="|all|live|deleted",
119
     *         "start"="|\d{4}-\d{2}-\d{2}",
120
     *         "end"="|\d{4}-\d{2}-\d{2}",
121
     *         "offset"="|\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}",
122
     *     },
123
     *     defaults={
124
     *         "namespace"=0,
125
     *         "start"=false,
126
     *         "end"=false,
127
     *         "offset"=false,
128
     *     }
129
     * )
130
     * @param PagesRepository $pagesRepo
131
     * @param string $redirects One of 'noredirects', 'onlyredirects' or 'all' for both.
132
     * @param string $deleted One of 'live', 'deleted' or 'all' for both.
133
     * @return RedirectResponse|Response
134
     * @codeCoverageIgnore
135
     */
136
    public function resultAction(
137
        PagesRepository $pagesRepo,
138
        string $redirects = 'noredirects',
139
        string $deleted = 'all'
140
    ) {
141
        // Check for legacy values for 'redirects', and redirect
142
        // back with correct values if need be. This could be refactored
143
        // out to XtoolsController, but this is the only tool in the suite
144
        // that deals with redirects, so we'll keep it confined here.
145
        $validRedirects = ['', 'noredirects', 'onlyredirects', 'all'];
146
        if ('none' === $redirects || !in_array($redirects, $validRedirects)) {
147
            return $this->redirectToRoute('PagesResult', array_merge($this->params, [
148
                'redirects' => 'noredirects',
149
                'deleted' => $deleted,
150
                'offset' => $this->offset,
151
            ]));
152
        }
153
154
        $pages = $this->setUpPages($pagesRepo, $redirects, $deleted);
155
        $pages->prepareData();
156
157
        $ret = [
158
            'xtPage' => 'Pages',
159
            'xtTitle' => $this->user->getUsername(),
160
            'summaryColumns' => $this->getSummaryColumns($pages),
161
            'pages' => $pages,
162
        ];
163
164
        if ('PagePile' === $this->request->query->get('format')) {
165
            return $this->getPagepileResult($this->project, $pages);
166
        }
167
168
        // Output the relevant format template.
169
        return $this->getFormattedResponse('pages/result', $ret);
170
    }
171
172
    /**
173
     * What columns to show in namespace totals table.
174
     * @param Pages $pages The Pages instance.
175
     * @return string[]
176
     * @codeCoverageIgnore
177
     */
178
    private function getSummaryColumns(Pages $pages): array
179
    {
180
        $summaryColumns = ['namespace'];
181
        if ('deleted' === $pages->getDeleted()) {
182
            // Showing only deleted pages shows only the deleted column, as redirects are non-applicable.
183
            $summaryColumns[] = 'deleted';
184
        } elseif ('onlyredirects' == $pages->getRedirects()) {
185
            // Don't show redundant pages column if only getting data on redirects or deleted pages.
186
            $summaryColumns[] = 'redirects';
187
        } elseif ('noredirects' == $pages->getRedirects()) {
188
            // Don't show redundant redirects column if only getting data on non-redirects.
189
            $summaryColumns[] = 'pages';
190
        } else {
191
            // Order is important here.
192
            $summaryColumns[] = 'pages';
193
            $summaryColumns[] = 'redirects';
194
        }
195
196
        // Show deleted column only when both deleted and live pages are visible.
197
        if ('all' === $pages->getDeleted()) {
198
            $summaryColumns[] = 'deleted';
199
        }
200
201
        $summaryColumns[] = 'total-page-size';
202
        $summaryColumns[] = 'average-page-size';
203
204
        return $summaryColumns;
205
    }
206
207
    /**
208
     * Create a PagePile for the given pages, and get a Redirect to that PagePile.
209
     * @param Project $project
210
     * @param Pages $pages
211
     * @return RedirectResponse
212
     * @throws HttpException
213
     * @see https://pagepile.toolforge.org
214
     * @codeCoverageIgnore
215
     */
216
    private function getPagepileResult(Project $project, Pages $pages): RedirectResponse
217
    {
218
        $namespaces = $project->getNamespaces();
219
        $pageTitles = [];
220
221
        foreach (array_values($pages->getResults()) as $pagesData) {
222
            foreach ($pagesData as $page) {
223
                if (0 === (int)$page['namespace']) {
224
                    $pageTitles[] = $page['page_title'];
225
                } else {
226
                    $pageTitles[] = (
227
                        $namespaces[$page['namespace']] ?? $this->i18n->msg('unknown')
228
                    ).':'.$page['page_title'];
229
                }
230
            }
231
        }
232
233
        $pileId = $this->createPagePile($project, $pageTitles);
234
235
        return new RedirectResponse(
236
            "https://pagepile.toolforge.org/api.php?id=$pileId&action=get_data&format=html&doit1"
237
        );
238
    }
239
240
    /**
241
     * Create a PagePile with the given titles.
242
     * @param Project $project
243
     * @param string[] $pageTitles
244
     * @return int The PagePile ID.
245
     * @throws HttpException
246
     * @see https://pagepile.toolforge.org/
247
     * @codeCoverageIgnore
248
     */
249
    private function createPagePile(Project $project, array $pageTitles): int
250
    {
251
        $url = 'https://pagepile.toolforge.org/api.php';
252
253
        try {
254
            $res = $this->guzzle->request('GET', $url, ['query' => [
255
                'action' => 'create_pile_with_data',
256
                'wiki' => $project->getDatabaseName(),
257
                'data' => implode("\n", $pageTitles),
258
            ]]);
259
        } catch (ClientException $e) {
260
            throw new HttpException(
261
                414,
262
                'error-pagepile-too-large'
263
            );
264
        }
265
266
        $ret = json_decode($res->getBody()->getContents(), true);
267
268
        if (!isset($ret['status']) || 'OK' !== $ret['status']) {
269
            throw new HttpException(
270
                500,
271
                'Failed to create PagePile. There may be an issue with the PagePile API.'
272
            );
273
        }
274
275
        return $ret['pile']['id'];
276
    }
277
278
    /************************ API endpoints ************************/
279
280
    /**
281
     * Get a count of the number of pages created by a user,
282
     * including the number that have been deleted and are redirects.
283
     * @Route(
284
     *     "/api/user/pages_count/{project}/{username}/{namespace}/{redirects}/{deleted}/{start}/{end}",
285
     *     name="UserApiPagesCount",
286
     *     requirements={
287
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
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 PagesRepository $pagesRepo
303
     * @param string $redirects One of 'noredirects', 'onlyredirects' or 'all' for both.
304
     * @param string $deleted One of 'live', 'deleted' or 'all' for both.
305
     * @return JsonResponse
306
     * @codeCoverageIgnore
307
     */
308
    public function countPagesApiAction(
309
        PagesRepository $pagesRepo,
310
        string $redirects = 'noredirects',
311
        string $deleted = 'all'
312
    ): JsonResponse {
313
        $this->recordApiUsage('user/pages_count');
314
315
        $pages = $this->setUpPages($pagesRepo, $redirects, $deleted);
316
        $counts = $pages->getCounts();
317
318
        if ('all' !== $this->namespace && isset($counts[$this->namespace])) {
319
            $counts = $counts[$this->namespace];
320
        }
321
322
        return $this->getFormattedApiResponse(['counts' => (object)$counts]);
323
    }
324
325
    /**
326
     * Get the pages created by by a user.
327
     * @Route(
328
     *     "/api/user/pages/{project}/{username}/{namespace}/{redirects}/{deleted}/{start}/{end}/{offset}",
329
     *     name="UserApiPagesCreated",
330
     *     requirements={
331
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
332
     *         "namespace"="|\d+|all",
333
     *         "redirects"="|noredirects|onlyredirects|all",
334
     *         "deleted"="|all|live|deleted",
335
     *         "start"="|\d{4}-\d{2}-\d{2}",
336
     *         "end"="|\d{4}-\d{2}-\d{2}",
337
     *         "offset"="|\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}",
338
     *     },
339
     *     defaults={
340
     *         "namespace"=0,
341
     *         "redirects"="noredirects",
342
     *         "deleted"="all",
343
     *         "start"=false,
344
     *         "end"=false,
345
     *         "offset"=false,
346
     *     }
347
     * )
348
     * @param PagesRepository $pagesRepo
349
     * @param string $redirects One of 'noredirects', 'onlyredirects' or 'all' for both.
350
     * @param string $deleted One of 'live', 'deleted' or blank for both.
351
     * @return JsonResponse
352
     * @codeCoverageIgnore
353
     */
354
    public function getPagesApiAction(
355
        PagesRepository $pagesRepo,
356
        string $redirects = 'noredirects',
357
        string $deleted = 'all'
358
    ): JsonResponse {
359
        $this->recordApiUsage('user/pages');
360
361
        $pages = $this->setUpPages($pagesRepo, $redirects, $deleted);
362
        $pagesList = $pages->getResults();
363
364
        if ('all' !== $this->namespace && isset($pagesList[$this->namespace])) {
365
            $pagesList = $pagesList[$this->namespace];
366
        }
367
368
        $ret = [
369
            'pages' => (object)$pagesList,
370
        ];
371
372
        if ($pages->getNumResults() === $pages->resultsPerPage()) {
373
            $ret['continue'] = $pages->getLastTimestamp();
374
        }
375
376
        return $this->getFormattedApiResponse($ret);
377
    }
378
}
379