Passed
Push — main ( ec4ebd...49d96d )
by MusikAnimal
04:21
created

ArticleInfoController::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 10
dl 0
loc 15
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\Exception\XtoolsHttpException;
8
use App\Helper\AutomatedEditsHelper;
9
use App\Helper\I18nHelper;
10
use App\Model\ArticleInfo;
11
use App\Model\Authorship;
12
use App\Model\Page;
13
use App\Model\Project;
14
use App\Repository\ArticleInfoRepository;
15
use App\Repository\PageRepository;
16
use App\Repository\ProjectRepository;
17
use App\Repository\UserRepository;
18
use GuzzleHttp\Client;
19
use GuzzleHttp\Exception\ServerException;
20
use Psr\Cache\CacheItemPoolInterface;
21
use Psr\Container\ContainerInterface;
22
use Symfony\Component\HttpFoundation\JsonResponse;
23
use Symfony\Component\HttpFoundation\RequestStack;
24
use Symfony\Component\HttpFoundation\Response;
25
use Symfony\Component\Routing\Annotation\Route;
26
27
/**
28
 * This controller serves the search form and results for the ArticleInfo tool
29
 */
30
class ArticleInfoController extends XtoolsController
31
{
32
    protected ArticleInfo $articleInfo;
33
    protected ArticleInfoRepository $articleInfoRepo;
34
    protected AutomatedEditsHelper $autoEditsHelper;
35
36
    /**
37
     * @inheritDoc
38
     * @codeCoverageIgnore
39
     */
40
    public function getIndexRoute(): string
41
    {
42
        return 'ArticleInfo';
43
    }
44
45
    /**
46
     * @param RequestStack $requestStack
47
     * @param ContainerInterface $container
48
     * @param CacheItemPoolInterface $cache
49
     * @param Client $guzzle
50
     * @param I18nHelper $i18n
51
     * @param ProjectRepository $projectRepo
52
     * @param UserRepository $userRepo
53
     * @param PageRepository $pageRepo
54
     * @param ArticleInfoRepository $articleInfoRepo
55
     * @param AutomatedEditsHelper $autoEditsHelper
56
     */
57
    public function __construct(
58
        RequestStack $requestStack,
59
        ContainerInterface $container,
60
        CacheItemPoolInterface $cache,
61
        Client $guzzle,
62
        I18nHelper $i18n,
63
        ProjectRepository $projectRepo,
64
        UserRepository $userRepo,
65
        PageRepository $pageRepo,
66
        ArticleInfoRepository $articleInfoRepo,
67
        AutomatedEditsHelper $autoEditsHelper
68
    ) {
69
        $this->articleInfoRepo = $articleInfoRepo;
70
        $this->autoEditsHelper = $autoEditsHelper;
71
        parent::__construct($requestStack, $container, $cache, $guzzle, $i18n, $projectRepo, $userRepo, $pageRepo);
72
    }
73
74
    /**
75
     * The search form.
76
     * @Route("/articleinfo", name="ArticleInfo")
77
     * @Route("/articleinfo/index.php", name="articleInfoIndexPhp")
78
     * @Route("/articleinfo/{project}", name="ArticleInfoProject")
79
     * @return Response
80
     */
81
    public function indexAction(): Response
82
    {
83
        if (isset($this->params['project']) && isset($this->params['page'])) {
84
            return $this->redirectToRoute('ArticleInfoResult', $this->params);
85
        }
86
87
        return $this->render('articleInfo/index.html.twig', array_merge([
88
            'xtPage' => 'ArticleInfo',
89
            'xtPageTitle' => 'tool-articleinfo',
90
            'xtSubtitle' => 'tool-articleinfo-desc',
91
92
            // Defaults that will get overridden if in $params.
93
            'start' => '',
94
            'end' => '',
95
            'page' => '',
96
        ], $this->params, ['project' => $this->project]));
97
    }
98
99
    /**
100
     * Setup the ArticleInfo instance and its Repository.
101
     */
102
    private function setupArticleInfo(): void
103
    {
104
        if (isset($this->articleInfo)) {
105
            return;
106
        }
107
108
        $this->articleInfo = new ArticleInfo(
109
            $this->articleInfoRepo,
110
            $this->i18n,
111
            $this->autoEditsHelper,
112
            $this->page,
0 ignored issues
show
Bug introduced by
It seems like $this->page can also be of type null; however, parameter $page of App\Model\ArticleInfo::__construct() does only seem to accept App\Model\Page, 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

112
            /** @scrutinizer ignore-type */ $this->page,
Loading history...
113
            $this->start,
114
            $this->end
115
        );
116
    }
117
118
    /**
119
     * Generate ArticleInfo gadget script for use on-wiki. This automatically points the
120
     * script to this installation's API. Pass ?uglify=1 to uglify the code.
121
     *
122
     * @Route("/articleinfo-gadget.js", name="ArticleInfoGadget")
123
     * @link https://www.mediawiki.org/wiki/XTools/ArticleInfo_gadget
124
     *
125
     * @return Response
126
     * @codeCoverageIgnore
127
     */
128
    public function gadgetAction(): Response
129
    {
130
        $rendered = $this->renderView('articleInfo/articleinfo.js.twig');
131
        $response = new Response($rendered);
132
        $response->headers->set('Content-Type', 'text/javascript');
133
        return $response;
134
    }
135
136
    /**
137
     * Display the results in given date range.
138
     * @Route(
139
     *    "/articleinfo/{project}/{page}/{start}/{end}", name="ArticleInfoResult",
140
     *     requirements={
141
     *         "page"="(.+?)(?!\/(?:|\d{4}-\d{2}-\d{2})(?:\/(|\d{4}-\d{2}-\d{2}))?)?$",
142
     *         "start"="|\d{4}-\d{2}-\d{2}",
143
     *         "end"="|\d{4}-\d{2}-\d{2}",
144
     *     },
145
     *     defaults={
146
     *         "start"=false,
147
     *         "end"=false,
148
     *     }
149
     * )
150
     * @return Response
151
     * @codeCoverageIgnore
152
     */
153
    public function resultAction(): Response
154
    {
155
        if (!$this->isDateRangeValid($this->page, $this->start, $this->end)) {
0 ignored issues
show
Bug introduced by
It seems like $this->page can also be of type null; however, parameter $page of App\Controller\ArticleIn...ler::isDateRangeValid() does only seem to accept App\Model\Page, 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

155
        if (!$this->isDateRangeValid(/** @scrutinizer ignore-type */ $this->page, $this->start, $this->end)) {
Loading history...
156
            $this->addFlashMessage('notice', 'date-range-outside-revisions');
157
158
            return $this->redirectToRoute('ArticleInfo', [
159
                'project' => $this->request->get('project'),
160
            ]);
161
        }
162
163
        $this->setupArticleInfo();
164
        $this->articleInfo->prepareData();
165
166
        $maxRevisions = $this->getParameter('app.max_page_revisions');
167
168
        // Show message if we hit the max revisions.
169
        if ($this->articleInfo->tooManyRevisions()) {
170
            $this->addFlashMessage('notice', 'too-many-revisions', [
171
                $this->i18n->numberFormat($maxRevisions),
172
                $maxRevisions,
173
            ]);
174
        }
175
176
        // For when there is very old data (2001 era) which may cause miscalculations.
177
        if ($this->articleInfo->getFirstEdit()->getYear() < 2003) {
178
            $this->addFlashMessage('warning', 'old-page-notice');
179
        }
180
181
        // When all username info has been hidden (see T303724).
182
        if (0 === $this->articleInfo->getNumEditors()) {
183
            $this->addFlashMessage('warning', 'error-usernames-missing');
184
        }
185
186
        $ret = [
187
            'xtPage' => 'ArticleInfo',
188
            'xtTitle' => $this->page->getTitle(),
0 ignored issues
show
Bug introduced by
The method getTitle() 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

188
            'xtTitle' => $this->page->/** @scrutinizer ignore-call */ getTitle(),

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...
189
            'project' => $this->project,
190
            'editorlimit' => (int)$this->request->query->get('editorlimit', 20),
191
            'botlimit' => $this->request->query->get('botlimit', 10),
192
            'pageviewsOffset' => 60,
193
            'ai' => $this->articleInfo,
194
            'showAuthorship' => Authorship::isSupportedPage($this->page) && $this->articleInfo->getNumEditors() > 0,
0 ignored issues
show
Bug introduced by
It seems like $this->page can also be of type null; however, parameter $page of App\Model\Authorship::isSupportedPage() does only seem to accept App\Model\Page, 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

194
            'showAuthorship' => Authorship::isSupportedPage(/** @scrutinizer ignore-type */ $this->page) && $this->articleInfo->getNumEditors() > 0,
Loading history...
195
        ];
196
197
        // Output the relevant format template.
198
        return $this->getFormattedResponse('articleInfo/result', $ret);
199
    }
200
201
    /**
202
     * Check if there were any revisions of given page in given date range.
203
     * @param Page $page
204
     * @param false|int $start
205
     * @param false|int $end
206
     * @return bool
207
     */
208
    private function isDateRangeValid(Page $page, $start, $end): bool
209
    {
210
        return $page->getNumRevisions(null, $start, $end) > 0;
211
    }
212
213
    /************************ API endpoints ************************/
214
215
    /**
216
     * Get basic info on a given article.
217
     * @Route(
218
     *     "/api/articleinfo/{project}/{page}",
219
     *     name="ArticleInfoApiAction",
220
     *     requirements={"page"=".+"}
221
     * )
222
     * @Route("/api/page/articleinfo/{project}/{page}", requirements={"page"=".+"})
223
     * @return Response|JsonResponse
224
     * See ArticleInfoControllerTest::testArticleInfoApi()
225
     * @codeCoverageIgnore
226
     */
227
    public function articleInfoApiAction(): Response
228
    {
229
        $this->recordApiUsage('page/articleinfo');
230
231
        $this->setupArticleInfo();
232
        $data = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $data is dead and can be removed.
Loading history...
233
234
        try {
235
            $data = $this->articleInfo->getArticleInfoApiData($this->project, $this->page);
0 ignored issues
show
Bug introduced by
It seems like $this->page can also be of type null; however, parameter $page of App\Model\ArticleInfoApi::getArticleInfoApiData() does only seem to accept App\Model\Page, 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

235
            $data = $this->articleInfo->getArticleInfoApiData($this->project, /** @scrutinizer ignore-type */ $this->page);
Loading history...
236
        } catch (ServerException $e) {
237
            // The Wikimedia action API can fail for any number of reasons. To our users
238
            // any ServerException means the data could not be fetched, so we capture it here
239
            // to avoid the flood of automated emails when the API goes down, etc.
240
            $data['error'] = $this->i18n->msg('api-error', [$this->project->getDomain()]);
241
        }
242
243
        if ('html' === $this->request->query->get('format')) {
244
            return $this->getApiHtmlResponse($this->project, $this->page, $data);
0 ignored issues
show
Bug introduced by
It seems like $this->page can also be of type null; however, parameter $page of App\Controller\ArticleIn...r::getApiHtmlResponse() does only seem to accept App\Model\Page, 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

244
            return $this->getApiHtmlResponse($this->project, /** @scrutinizer ignore-type */ $this->page, $data);
Loading history...
245
        }
246
247
        return $this->getFormattedApiResponse($data);
248
    }
249
250
    /**
251
     * Get the Response for the HTML output of the ArticleInfo API action.
252
     * @param Project $project
253
     * @param Page $page
254
     * @param string[] $data The pre-fetched data.
255
     * @return Response
256
     * @codeCoverageIgnore
257
     */
258
    private function getApiHtmlResponse(Project $project, Page $page, array $data): Response
259
    {
260
        $response = $this->render('articleInfo/api.html.twig', [
261
            'project' => $project,
262
            'page' => $page,
263
            'data' => $data,
264
        ]);
265
266
        // All /api routes by default respond with a JSON content type.
267
        $response->headers->set('Content-Type', 'text/html');
268
269
        // This endpoint is hit constantly and user could be browsing the same page over
270
        // and over (popular noticeboard, for instance), so offload brief caching to browser.
271
        $response->setClientTtl(350);
272
273
        return $response;
274
    }
275
276
    /**
277
     * Get prose statistics for the given article.
278
     * @Route(
279
     *     "/api/page/prose/{project}/{page}",
280
     *     name="PageApiProse",
281
     *     requirements={"page"=".+"}
282
     * )
283
     * @return JsonResponse
284
     * @codeCoverageIgnore
285
     */
286
    public function proseStatsApiAction(): JsonResponse
287
    {
288
        $this->recordApiUsage('page/prose');
289
        $this->setupArticleInfo();
290
        return $this->getFormattedApiResponse($this->articleInfo->getProseStats());
291
    }
292
293
    /**
294
     * Get the page assessments of one or more pages, along with various related metadata.
295
     * @Route(
296
     *     "/api/page/assessments/{project}/{pages}",
297
     *     name="PageApiAssessments",
298
     *     requirements={"pages"=".+"}
299
     * )
300
     * @param string $pages May be multiple pages separated by pipes, e.g. Foo|Bar|Baz
301
     * @return JsonResponse
302
     * @codeCoverageIgnore
303
     */
304
    public function assessmentsApiAction(string $pages): JsonResponse
305
    {
306
        $this->recordApiUsage('page/assessments');
307
308
        $pages = explode('|', $pages);
309
        $out = [];
310
311
        foreach ($pages as $pageTitle) {
312
            try {
313
                $page = $this->validatePage($pageTitle);
314
                $assessments = $page->getProject()
315
                    ->getPageAssessments()
316
                    ->getAssessments($page);
317
318
                $out[$page->getTitle()] = $this->request->get('classonly')
319
                    ? $assessments['assessment']
320
                    : $assessments;
321
            } catch (XtoolsHttpException $e) {
322
                $out[$pageTitle] = false;
323
            }
324
        }
325
326
        return $this->getFormattedApiResponse($out);
327
    }
328
329
    /**
330
     * Get number of in and outgoing links and redirects to the given page.
331
     * @Route(
332
     *     "/api/page/links/{project}/{page}",
333
     *     name="PageApiLinks",
334
     *     requirements={"page"=".+"}
335
     * )
336
     * @return JsonResponse
337
     * @codeCoverageIgnore
338
     */
339
    public function linksApiAction(): JsonResponse
340
    {
341
        $this->recordApiUsage('page/links');
342
        return $this->getFormattedApiResponse($this->page->countLinksAndRedirects());
343
    }
344
345
    /**
346
     * Get the top editors to a page.
347
     * @Route(
348
     *     "/api/page/top_editors/{project}/{page}/{start}/{end}/{limit}", name="PageApiTopEditors",
349
     *     requirements={
350
     *         "page"="(.+?)(?!\/(?:|\d{4}-\d{2}-\d{2})(?:\/(|\d{4}-\d{2}-\d{2}))?(?:\/(\d+))?)?$",
351
     *         "start"="|\d{4}-\d{2}-\d{2}",
352
     *         "end"="|\d{4}-\d{2}-\d{2}",
353
     *         "limit"="|\d+"
354
     *     },
355
     *     defaults={
356
     *         "start"=false,
357
     *         "end"=false,
358
     *         "limit"=20,
359
     *     }
360
     * )
361
     * @return JsonResponse
362
     * @codeCoverageIgnore
363
     */
364
    public function topEditorsApiAction(): JsonResponse
365
    {
366
        $this->recordApiUsage('page/top_editors');
367
368
        $this->setupArticleInfo();
369
        $topEditors = $this->articleInfo->getTopEditorsByEditCount(
370
            (int)$this->limit,
371
            '' != $this->request->query->get('nobots')
372
        );
373
374
        return $this->getFormattedApiResponse([
375
            'top_editors' => $topEditors,
376
        ]);
377
    }
378
}
379