Passed
Pull Request — main (#442)
by MusikAnimal
08:00 queued 04:10
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

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\Helper\I18nHelper;
8
use App\Model\Pages;
9
use App\Model\Project;
10
use App\Repository\PageRepository;
11
use App\Repository\PagesRepository;
12
use App\Repository\ProjectRepository;
13
use App\Repository\UserRepository;
14
use GuzzleHttp\Client;
15
use GuzzleHttp\Exception\ClientException;
16
use Psr\Cache\CacheItemPoolInterface;
17
use Symfony\Component\DependencyInjection\ContainerInterface;
18
use Symfony\Component\HttpFoundation\JsonResponse;
19
use Symfony\Component\HttpFoundation\RedirectResponse;
20
use Symfony\Component\HttpFoundation\RequestStack;
21
use Symfony\Component\HttpFoundation\Response;
22
use Symfony\Component\HttpKernel\Exception\HttpException;
23
use Symfony\Component\Routing\Annotation\Route;
24
25
/**
26
 * This controller serves the Pages tool.
27
 */
28
class PagesController extends XtoolsController
29
{
30
    protected PagesRepository $pagesRepo;
31
32
    public function __construct(
33
        RequestStack $requestStack,
34
        ContainerInterface $container,
35
        CacheItemPoolInterface $cache,
36
        Client $guzzle,
37
        I18nHelper $i18n,
38
        ProjectRepository $projectRepo,
39
        UserRepository $userRepo,
40
        PageRepository $pageRepo,
41
        PagesRepository $pagesRepo
42
    ) {
43
        $this->pagesRepo = $pagesRepo;
44
        parent::__construct($requestStack, $container, $cache, $guzzle, $i18n, $projectRepo, $userRepo, $pageRepo);
45
    }
46
47
    /**
48
     * Get the name of the tool's index route.
49
     * This is also the name of the associated model.
50
     * @return string
51
     * @codeCoverageIgnore
52
     */
53
    public function getIndexRoute(): string
54
    {
55
        return 'Pages';
56
    }
57
58
    /**
59
     * @inheritDoc
60
     */
61
    public function tooHighEditCountRoute(): string
62
    {
63
        return $this->getIndexRoute();
64
    }
65
66
    /**
67
     * @inheritDoc
68
     */
69
    public function tooHighEditCountActionAllowlist(): array
70
    {
71
        return ['countPagesApi'];
72
    }
73
74
    /**
75
     * Display the form.
76
     * @Route("/pages", name="Pages")
77
     * @Route("/pages/index.php", name="PagesIndexPhp")
78
     * @Route("/pages/{project}", name="PagesProject")
79
     * @return Response
80
     */
81
    public function indexAction(): Response
82
    {
83
        // Redirect if at minimum project and username are given.
84
        if (isset($this->params['project']) && isset($this->params['username'])) {
85
            return $this->redirectToRoute('PagesResult', $this->params);
86
        }
87
88
        // Otherwise fall through.
89
        return $this->render('pages/index.html.twig', array_merge([
90
            'xtPageTitle' => 'tool-pages',
91
            'xtSubtitle' => 'tool-pages-desc',
92
            'xtPage' => 'Pages',
93
94
            // Defaults that will get overridden if in $params.
95
            'username' => '',
96
            'namespace' => 0,
97
            'redirects' => 'noredirects',
98
            'deleted' => 'all',
99
            'start' => '',
100
            'end' => '',
101
        ], $this->params, ['project' => $this->project]));
102
    }
103
104
    /**
105
     * Every action in this controller (other than 'index') calls this first.
106
     * @param string $redirects One of 'noredirects', 'onlyredirects' or 'all' for both.
107
     * @param string $deleted One of 'live', 'deleted' or 'all' for both.
108
     * @return Pages
109
     * @codeCoverageIgnore
110
     */
111
    protected function setUpPages(string $redirects, string $deleted): Pages
112
    {
113
        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

113
        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...
114
            $this->params['username'] = $this->user->getUsername();
115
            $this->throwXtoolsException($this->getIndexRoute(), 'error-ip-range-unsupported');
116
        }
117
118
        return new Pages(
119
            $this->pagesRepo,
120
            $this->project,
121
            $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

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