Passed
Push — main ( ec4ebd...49d96d )
by MusikAnimal
04:21
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
    /**
33
     * @param RequestStack $requestStack
34
     * @param ContainerInterface $container
35
     * @param CacheItemPoolInterface $cache
36
     * @param Client $guzzle
37
     * @param I18nHelper $i18n
38
     * @param ProjectRepository $projectRepo
39
     * @param UserRepository $userRepo
40
     * @param PageRepository $pageRepo
41
     * @param PagesRepository $pagesRepo
42
     * @codeCoverageIgnore
43
     */
44
    public function __construct(
45
        RequestStack $requestStack,
46
        ContainerInterface $container,
47
        CacheItemPoolInterface $cache,
48
        Client $guzzle,
49
        I18nHelper $i18n,
50
        ProjectRepository $projectRepo,
51
        UserRepository $userRepo,
52
        PageRepository $pageRepo,
53
        PagesRepository $pagesRepo
54
    ) {
55
        $this->pagesRepo = $pagesRepo;
56
        parent::__construct($requestStack, $container, $cache, $guzzle, $i18n, $projectRepo, $userRepo, $pageRepo);
57
    }
58
59
    /**
60
     * Get the name of the tool's index route.
61
     * This is also the name of the associated model.
62
     * @inheritDoc
63
     * @codeCoverageIgnore
64
     */
65
    public function getIndexRoute(): string
66
    {
67
        return 'Pages';
68
    }
69
70
    /**
71
     * @inheritDoc
72
     * @codeCoverageIgnore
73
     */
74
    public function tooHighEditCountRoute(): string
75
    {
76
        return $this->getIndexRoute();
77
    }
78
79
    /**
80
     * @inheritDoc
81
     * @codeCoverageIgnore
82
     */
83
    public function tooHighEditCountActionAllowlist(): array
84
    {
85
        return ['countPagesApi'];
86
    }
87
88
    /**
89
     * Display the form.
90
     * @Route("/pages", name="Pages")
91
     * @Route("/pages/index.php", name="PagesIndexPhp")
92
     * @Route("/pages/{project}", name="PagesProject")
93
     * @return Response
94
     */
95
    public function indexAction(): Response
96
    {
97
        // Redirect if at minimum project and username are given.
98
        if (isset($this->params['project']) && isset($this->params['username'])) {
99
            return $this->redirectToRoute('PagesResult', $this->params);
100
        }
101
102
        // Otherwise fall through.
103
        return $this->render('pages/index.html.twig', array_merge([
104
            'xtPageTitle' => 'tool-pages',
105
            'xtSubtitle' => 'tool-pages-desc',
106
            'xtPage' => 'Pages',
107
108
            // Defaults that will get overridden if in $params.
109
            'username' => '',
110
            'namespace' => 0,
111
            'redirects' => 'noredirects',
112
            'deleted' => 'all',
113
            'start' => '',
114
            'end' => '',
115
        ], $this->params, ['project' => $this->project]));
116
    }
117
118
    /**
119
     * Every action in this controller (other than 'index') calls this first.
120
     * @param string $redirects One of 'noredirects', 'onlyredirects' or 'all' for both.
121
     * @param string $deleted One of 'live', 'deleted' or 'all' for both.
122
     * @return Pages
123
     * @codeCoverageIgnore
124
     */
125
    protected function setUpPages(string $redirects, string $deleted): Pages
126
    {
127
        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

127
        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...
128
            $this->params['username'] = $this->user->getUsername();
129
            $this->throwXtoolsException($this->getIndexRoute(), 'error-ip-range-unsupported');
130
        }
131
132
        return new Pages(
133
            $this->pagesRepo,
134
            $this->project,
135
            $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

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