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

ArticleInfoController::isDateRangeValid()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 3
dl 0
loc 3
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\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
     * Get the name of the tool's index route. This is also the name of the associated model.
38
     * @return string
39
     * @codeCoverageIgnore
40
     */
41
    public function getIndexRoute(): string
42
    {
43
        return 'ArticleInfo';
44
    }
45
46
    public function __construct(
47
        RequestStack $requestStack,
48
        ContainerInterface $container,
49
        CacheItemPoolInterface $cache,
50
        Client $guzzle,
51
        I18nHelper $i18n,
52
        ProjectRepository $projectRepo,
53
        UserRepository $userRepo,
54
        PageRepository $pageRepo,
55
        ArticleInfoRepository $articleInfoRepo,
56
        AutomatedEditsHelper $autoEditsHelper
57
    ) {
58
        $this->articleInfoRepo = $articleInfoRepo;
59
        $this->autoEditsHelper = $autoEditsHelper;
60
        parent::__construct($requestStack, $container, $cache, $guzzle, $i18n, $projectRepo, $userRepo, $pageRepo);
61
    }
62
63
    /**
64
     * The search form.
65
     * @Route("/articleinfo", name="ArticleInfo")
66
     * @Route("/articleinfo/index.php", name="articleInfoIndexPhp")
67
     * @Route("/articleinfo/{project}", name="ArticleInfoProject")
68
     * @return Response
69
     */
70
    public function indexAction(): Response
71
    {
72
        if (isset($this->params['project']) && isset($this->params['page'])) {
73
            return $this->redirectToRoute('ArticleInfoResult', $this->params);
74
        }
75
76
        return $this->render('articleInfo/index.html.twig', array_merge([
77
            'xtPage' => 'ArticleInfo',
78
            'xtPageTitle' => 'tool-articleinfo',
79
            'xtSubtitle' => 'tool-articleinfo-desc',
80
81
            // Defaults that will get overridden if in $params.
82
            'start' => '',
83
            'end' => '',
84
            'page' => '',
85
        ], $this->params, ['project' => $this->project]));
86
    }
87
88
    /**
89
     * Setup the ArticleInfo instance and its Repository.
90
     */
91
    private function setupArticleInfo(): void
92
    {
93
        if (isset($this->articleInfo)) {
94
            return;
95
        }
96
97
        $this->articleInfo = new ArticleInfo(
98
            $this->articleInfoRepo,
99
            $this->i18n,
100
            $this->autoEditsHelper,
101
            $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

101
            /** @scrutinizer ignore-type */ $this->page,
Loading history...
102
            $this->start,
103
            $this->end
104
        );
105
    }
106
107
    /**
108
     * Generate ArticleInfo gadget script for use on-wiki. This automatically points the
109
     * script to this installation's API. Pass ?uglify=1 to uglify the code.
110
     *
111
     * @Route("/articleinfo-gadget.js", name="ArticleInfoGadget")
112
     * @link https://www.mediawiki.org/wiki/XTools/ArticleInfo_gadget
113
     *
114
     * @return Response
115
     * @codeCoverageIgnore
116
     */
117
    public function gadgetAction(): Response
118
    {
119
        $rendered = $this->renderView('articleInfo/articleinfo.js.twig');
120
        $response = new Response($rendered);
121
        $response->headers->set('Content-Type', 'text/javascript');
122
        return $response;
123
    }
124
125
    /**
126
     * Display the results in given date range.
127
     * @Route(
128
     *    "/articleinfo/{project}/{page}/{start}/{end}", name="ArticleInfoResult",
129
     *     requirements={
130
     *         "page"="(.+?)(?!\/(?:|\d{4}-\d{2}-\d{2})(?:\/(|\d{4}-\d{2}-\d{2}))?)?$",
131
     *         "start"="|\d{4}-\d{2}-\d{2}",
132
     *         "end"="|\d{4}-\d{2}-\d{2}",
133
     *     },
134
     *     defaults={
135
     *         "start"=false,
136
     *         "end"=false,
137
     *     }
138
     * )
139
     * @return Response
140
     * @codeCoverageIgnore
141
     */
142
    public function resultAction(): Response
143
    {
144
        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

144
        if (!$this->isDateRangeValid(/** @scrutinizer ignore-type */ $this->page, $this->start, $this->end)) {
Loading history...
145
            $this->addFlashMessage('notice', 'date-range-outside-revisions');
146
147
            return $this->redirectToRoute('ArticleInfo', [
148
                'project' => $this->request->get('project'),
149
            ]);
150
        }
151
152
        $this->setupArticleInfo();
153
        $this->articleInfo->prepareData();
154
155
        $maxRevisions = $this->getParameter('app.max_page_revisions');
156
157
        // Show message if we hit the max revisions.
158
        if ($this->articleInfo->tooManyRevisions()) {
159
            $this->addFlashMessage('notice', 'too-many-revisions', [
160
                $i18n->numberFormat($maxRevisions),
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $i18n seems to be never defined.
Loading history...
161
                $maxRevisions,
162
            ]);
163
        }
164
165
        // For when there is very old data (2001 era) which may cause miscalculations.
166
        if ($this->articleInfo->getFirstEdit()->getYear() < 2003) {
167
            $this->addFlashMessage('warning', 'old-page-notice');
168
        }
169
170
        // When all username info has been hidden (see T303724).
171
        if (0 === $this->articleInfo->getNumEditors()) {
172
            $this->addFlashMessage('warning', 'error-usernames-missing');
173
        }
174
175
        $ret = [
176
            'xtPage' => 'ArticleInfo',
177
            '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

177
            '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...
178
            'project' => $this->project,
179
            'editorlimit' => (int)$this->request->query->get('editorlimit', 20),
180
            'botlimit' => $this->request->query->get('botlimit', 10),
181
            'pageviewsOffset' => 60,
182
            'ai' => $this->articleInfo,
183
            '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

183
            'showAuthorship' => Authorship::isSupportedPage(/** @scrutinizer ignore-type */ $this->page) && $this->articleInfo->getNumEditors() > 0,
Loading history...
184
        ];
185
186
        // Output the relevant format template.
187
        return $this->getFormattedResponse('articleInfo/result', $ret);
188
    }
189
190
    /**
191
     * Check if there were any revisions of given page in given date range.
192
     * @param Page $page
193
     * @param false|int $start
194
     * @param false|int $end
195
     * @return bool
196
     */
197
    private function isDateRangeValid(Page $page, $start, $end): bool
198
    {
199
        return $page->getNumRevisions(null, $start, $end) > 0;
200
    }
201
202
    /************************ API endpoints ************************/
203
204
    /**
205
     * Get basic info on a given article.
206
     * @Route(
207
     *     "/api/articleinfo/{project}/{page}",
208
     *     name="ArticleInfoApiAction",
209
     *     requirements={"page"=".+"}
210
     * )
211
     * @Route("/api/page/articleinfo/{project}/{page}", requirements={"page"=".+"})
212
     * @return Response|JsonResponse
213
     * See ArticleInfoControllerTest::testArticleInfoApi()
214
     * @codeCoverageIgnore
215
     */
216
    public function articleInfoApiAction(): Response
217
    {
218
        $this->recordApiUsage('page/articleinfo');
219
220
        $this->setupArticleInfo();
221
        $data = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $data is dead and can be removed.
Loading history...
222
223
        try {
224
            $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

224
            $data = $this->articleInfo->getArticleInfoApiData($this->project, /** @scrutinizer ignore-type */ $this->page);
Loading history...
225
        } catch (ServerException $e) {
226
            // The Wikimedia action API can fail for any number of reasons. To our users
227
            // any ServerException means the data could not be fetched, so we capture it here
228
            // to avoid the flood of automated emails when the API goes down, etc.
229
            $data['error'] = $this->i18n->msg('api-error', [$this->project->getDomain()]);
230
        }
231
232
        if ('html' === $this->request->query->get('format')) {
233
            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

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