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

PagesController::createPagePile()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 27
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 16
nc 3
nop 2
dl 0
loc 27
rs 9.7333
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 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