Test Failed
Push — dependency-injection ( 7565fa )
by MusikAnimal
07:05
created

PagesController::countPagesApiAction()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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