x-tools /
xtools
This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include, or for example
via PHP's auto-loading mechanism.
| 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\Model\Authorship; |
||||
| 10 | use App\Model\Page; |
||||
| 11 | use App\Model\PageInfo; |
||||
| 12 | use App\Model\Project; |
||||
| 13 | use App\Repository\PageInfoRepository; |
||||
| 14 | use GuzzleHttp\Exception\ServerException; |
||||
| 15 | use OpenApi\Annotations as OA; |
||||
| 16 | use Symfony\Component\HttpFoundation\JsonResponse; |
||||
| 17 | use Symfony\Component\HttpFoundation\Response; |
||||
| 18 | use Symfony\Component\Routing\Annotation\Route; |
||||
| 19 | use Twig\Markup; |
||||
| 20 | |||||
| 21 | /** |
||||
| 22 | * This controller serves the search form and results for the PageInfo tool |
||||
| 23 | */ |
||||
| 24 | class PageInfoController extends XtoolsController |
||||
| 25 | { |
||||
| 26 | protected PageInfo $pageInfo; |
||||
| 27 | |||||
| 28 | /** |
||||
| 29 | * @inheritDoc |
||||
| 30 | * @codeCoverageIgnore |
||||
| 31 | */ |
||||
| 32 | public function getIndexRoute(): string |
||||
| 33 | { |
||||
| 34 | return 'PageInfo'; |
||||
| 35 | } |
||||
| 36 | |||||
| 37 | /** |
||||
| 38 | * The search form. |
||||
| 39 | * @Route("/pageinfo", name="PageInfo") |
||||
| 40 | * @Route("/pageinfo/{project}", name="PageInfoProject") |
||||
| 41 | * @Route("/articleinfo", name="PageInfoLegacy") |
||||
| 42 | * @Route("/articleinfo/index.php", name="PageInfoLegacyPhp") |
||||
| 43 | * @return Response |
||||
| 44 | */ |
||||
| 45 | public function indexAction(): Response |
||||
| 46 | { |
||||
| 47 | if (isset($this->params['project']) && isset($this->params['page'])) { |
||||
| 48 | return $this->redirectToRoute('PageInfoResult', $this->params); |
||||
| 49 | } |
||||
| 50 | |||||
| 51 | return $this->render('pageInfo/index.html.twig', array_merge([ |
||||
| 52 | 'xtPage' => 'PageInfo', |
||||
| 53 | 'xtPageTitle' => 'tool-pageinfo', |
||||
| 54 | 'xtSubtitle' => 'tool-pageinfo-desc', |
||||
| 55 | |||||
| 56 | // Defaults that will get overridden if in $params. |
||||
| 57 | 'start' => '', |
||||
| 58 | 'end' => '', |
||||
| 59 | 'page' => '', |
||||
| 60 | ], $this->params, ['project' => $this->project])); |
||||
| 61 | } |
||||
| 62 | |||||
| 63 | /** |
||||
| 64 | * Setup the PageInfo instance and its Repository. |
||||
| 65 | * @param PageInfoRepository $pageInfoRepo |
||||
| 66 | * @param AutomatedEditsHelper $autoEditsHelper |
||||
| 67 | * @codeCoverageIgnore |
||||
| 68 | */ |
||||
| 69 | private function setupPageInfo( |
||||
| 70 | PageInfoRepository $pageInfoRepo, |
||||
| 71 | AutomatedEditsHelper $autoEditsHelper |
||||
| 72 | ): void { |
||||
| 73 | if (isset($this->pageInfo)) { |
||||
| 74 | return; |
||||
| 75 | } |
||||
| 76 | |||||
| 77 | $this->pageInfo = new PageInfo( |
||||
| 78 | $pageInfoRepo, |
||||
| 79 | $this->i18n, |
||||
| 80 | $autoEditsHelper, |
||||
| 81 | $this->page, |
||||
|
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||||
| 82 | $this->start, |
||||
| 83 | $this->end |
||||
| 84 | ); |
||||
| 85 | } |
||||
| 86 | |||||
| 87 | /** |
||||
| 88 | * Generate PageInfo gadget script for use on-wiki. This automatically points the |
||||
| 89 | * script to this installation's API. |
||||
| 90 | * |
||||
| 91 | * @Route("/pageinfo-gadget.js", name="PageInfoGadget") |
||||
| 92 | * @link https://www.mediawiki.org/wiki/XTools/PageInfo_gadget |
||||
| 93 | * |
||||
| 94 | * @return Response |
||||
| 95 | * @codeCoverageIgnore |
||||
| 96 | */ |
||||
| 97 | public function gadgetAction(): Response |
||||
| 98 | { |
||||
| 99 | $rendered = $this->renderView('pageInfo/pageinfo.js.twig'); |
||||
| 100 | $response = new Response($rendered); |
||||
| 101 | $response->headers->set('Content-Type', 'text/javascript'); |
||||
| 102 | return $response; |
||||
| 103 | } |
||||
| 104 | |||||
| 105 | /** |
||||
| 106 | * Display the results in given date range. |
||||
| 107 | * @Route( |
||||
| 108 | * "/pageinfo/{project}/{page}/{start}/{end}", name="PageInfoResult", |
||||
| 109 | * requirements={ |
||||
| 110 | * "page"="(.+?)(?!\/(?:|\d{4}-\d{2}-\d{2})(?:\/(|\d{4}-\d{2}-\d{2}))?)?$", |
||||
| 111 | * "start"="|\d{4}-\d{2}-\d{2}", |
||||
| 112 | * "end"="|\d{4}-\d{2}-\d{2}", |
||||
| 113 | * }, |
||||
| 114 | * defaults={ |
||||
| 115 | * "start"=false, |
||||
| 116 | * "end"=false, |
||||
| 117 | * } |
||||
| 118 | * ) |
||||
| 119 | * @Route( |
||||
| 120 | * "/articleinfo/{project}/{page}/{start}/{end}", name="PageInfoResultLegacy", |
||||
| 121 | * requirements={ |
||||
| 122 | * "page"="(.+?)(?!\/(?:|\d{4}-\d{2}-\d{2})(?:\/(|\d{4}-\d{2}-\d{2}))?)?$", |
||||
| 123 | * "start"="|\d{4}-\d{2}-\d{2}", |
||||
| 124 | * "end"="|\d{4}-\d{2}-\d{2}", |
||||
| 125 | * }, |
||||
| 126 | * defaults={ |
||||
| 127 | * "start"=false, |
||||
| 128 | * "end"=false, |
||||
| 129 | * } |
||||
| 130 | * ) |
||||
| 131 | * @param PageInfoRepository $pageInfoRepo |
||||
| 132 | * @param AutomatedEditsHelper $autoEditsHelper |
||||
| 133 | * @return Response |
||||
| 134 | * @codeCoverageIgnore |
||||
| 135 | */ |
||||
| 136 | public function resultAction( |
||||
| 137 | PageInfoRepository $pageInfoRepo, |
||||
| 138 | AutomatedEditsHelper $autoEditsHelper |
||||
| 139 | ): Response { |
||||
| 140 | if (!$this->isDateRangeValid($this->page, $this->start, $this->end)) { |
||||
|
0 ignored issues
–
show
It seems like
$this->page can also be of type null; however, parameter $page of App\Controller\PageInfoC...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
Loading history...
|
|||||
| 141 | $this->addFlashMessage('notice', 'date-range-outside-revisions'); |
||||
| 142 | |||||
| 143 | return $this->redirectToRoute('PageInfo', [ |
||||
| 144 | 'project' => $this->request->get('project'), |
||||
| 145 | ]); |
||||
| 146 | } |
||||
| 147 | |||||
| 148 | $this->setupPageInfo($pageInfoRepo, $autoEditsHelper); |
||||
| 149 | $this->pageInfo->prepareData(); |
||||
| 150 | |||||
| 151 | $maxRevisions = $this->getParameter('app.max_page_revisions'); |
||||
| 152 | |||||
| 153 | // Show message if we hit the max revisions. |
||||
| 154 | if ($this->pageInfo->tooManyRevisions()) { |
||||
| 155 | $this->addFlashMessage('notice', 'too-many-revisions', [ |
||||
| 156 | $this->i18n->numberFormat($maxRevisions), |
||||
| 157 | $maxRevisions, |
||||
| 158 | ]); |
||||
| 159 | } |
||||
| 160 | |||||
| 161 | // For when there is very old data (2001 era) which may cause miscalculations. |
||||
| 162 | if ($this->pageInfo->getFirstEdit()->getYear() < 2003) { |
||||
| 163 | $this->addFlashMessage('warning', 'old-page-notice'); |
||||
| 164 | } |
||||
| 165 | |||||
| 166 | // When all username info has been hidden (see T303724). |
||||
| 167 | if (0 === $this->pageInfo->getNumEditors()) { |
||||
| 168 | $this->addFlashMessage('warning', 'error-usernames-missing'); |
||||
| 169 | } elseif ($this->pageInfo->numDeletedRevisions()) { |
||||
| 170 | $link = new Markup( |
||||
| 171 | $this->renderView('flashes/deleted_data.html.twig', [ |
||||
| 172 | 'numRevs' => $this->pageInfo->numDeletedRevisions(), |
||||
| 173 | ]), |
||||
| 174 | 'UTF-8' |
||||
| 175 | ); |
||||
| 176 | $this->addFlashMessage( |
||||
| 177 | 'warning', |
||||
| 178 | $link, |
||||
| 179 | [$this->pageInfo->numDeletedRevisions(), $link] |
||||
| 180 | ); |
||||
| 181 | } |
||||
| 182 | |||||
| 183 | $ret = [ |
||||
| 184 | 'xtPage' => 'PageInfo', |
||||
| 185 | 'xtTitle' => $this->page->getTitle(), |
||||
|
0 ignored issues
–
show
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
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...
|
|||||
| 186 | 'project' => $this->project, |
||||
| 187 | 'editorlimit' => (int)$this->request->query->get('editorlimit', 20), |
||||
| 188 | 'botlimit' => $this->request->query->get('botlimit', 10), |
||||
| 189 | 'pageviewsOffset' => 60, |
||||
| 190 | 'ai' => $this->pageInfo, |
||||
| 191 | 'showAuthorship' => Authorship::isSupportedPage($this->page) && $this->pageInfo->getNumEditors() > 0, |
||||
|
0 ignored issues
–
show
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
Loading history...
|
|||||
| 192 | ]; |
||||
| 193 | |||||
| 194 | // Output the relevant format template. |
||||
| 195 | return $this->getFormattedResponse('pageInfo/result', $ret); |
||||
| 196 | } |
||||
| 197 | |||||
| 198 | /** |
||||
| 199 | * Check if there were any revisions of given page in given date range. |
||||
| 200 | * @param Page $page |
||||
| 201 | * @param false|int $start |
||||
| 202 | * @param false|int $end |
||||
| 203 | * @return bool |
||||
| 204 | */ |
||||
| 205 | private function isDateRangeValid(Page $page, $start, $end): bool |
||||
| 206 | { |
||||
| 207 | return $page->getNumRevisions(null, $start, $end) > 0; |
||||
| 208 | } |
||||
| 209 | |||||
| 210 | /************************ API endpoints ************************/ |
||||
| 211 | |||||
| 212 | /** |
||||
| 213 | * Get basic information about a page. |
||||
| 214 | * @Route( |
||||
| 215 | * "/api/page/pageinfo/{project}/{page}", |
||||
| 216 | * name="PageApiPageInfo", |
||||
| 217 | * requirements={"page"=".+"}, |
||||
| 218 | * methods={"GET"} |
||||
| 219 | * ) |
||||
| 220 | * @Route( |
||||
| 221 | * "/api/page/articleinfo/{project}/{page}", |
||||
| 222 | * name="PageApiPageInfoLegacy", |
||||
| 223 | * requirements={"page"=".+"}, |
||||
| 224 | * methods={"GET"} |
||||
| 225 | * ) |
||||
| 226 | * @OA\Get(description="Get basic information about the history of a page. |
||||
| 227 | See also the [pageviews](https://w.wiki/6o9k) and [edit data](https://w.wiki/6o9m) REST APIs.") |
||||
| 228 | * @OA\Tag(name="Page API") |
||||
| 229 | * @OA\ExternalDocumentation(url="https://www.mediawiki.org/wiki/XTools/API/Page#Page_info") |
||||
| 230 | * @OA\Parameter(ref="#/components/parameters/Project") |
||||
| 231 | * @OA\Parameter(ref="#/components/parameters/Page") |
||||
| 232 | * @OA\Parameter(name="format", in="query", @OA\Schema(default="json", type="string", enum={"json","html"})) |
||||
| 233 | * @OA\Response( |
||||
| 234 | * response=200, |
||||
| 235 | * description="Basic information about the page.", |
||||
| 236 | * @OA\JsonContent( |
||||
| 237 | * @OA\Property(property="project", ref="#/components/parameters/Project/schema"), |
||||
| 238 | * @OA\Property(property="page", ref="#/components/parameters/Page/schema"), |
||||
| 239 | * @OA\Property(property="watchers", type="integer"), |
||||
| 240 | * @OA\Property(property="pageviews", type="integer"), |
||||
| 241 | * @OA\Property(property="pageviews_offset", type="integer"), |
||||
| 242 | * @OA\Property(property="revisions", type="integer"), |
||||
| 243 | * @OA\Property(property="editors", type="integer"), |
||||
| 244 | * @OA\Property(property="minor_edits", type="integer"), |
||||
| 245 | * @OA\Property(property="author", type="string", example="Jimbo Wales"), |
||||
| 246 | * @OA\Property(property="author_editcount", type="integer"), |
||||
| 247 | * @OA\Property(property="created_at", type="date"), |
||||
| 248 | * @OA\Property(property="created_rev_id", type="integer"), |
||||
| 249 | * @OA\Property(property="modified_at", type="date"), |
||||
| 250 | * @OA\Property(property="secs_since_last_edit", type="integer"), |
||||
| 251 | * @OA\Property(property="modified_rev_id", type="integer"), |
||||
| 252 | * @OA\Property(property="assessment", type="object", example={ |
||||
| 253 | * "value":"FA", |
||||
| 254 | * "color": "#9CBDFF", |
||||
| 255 | * "category": "Category:FA-Class articles", |
||||
| 256 | * "badge": "https://upload.wikimedia.org/wikipedia/commons/b/bc/Featured_article_star.svg" |
||||
| 257 | * }), |
||||
| 258 | * @OA\Property(property="elapsed_time", ref="#/components/schemas/elapsed_time") |
||||
| 259 | * ), |
||||
| 260 | * @OA\XmlContent(format="text/html") |
||||
| 261 | * ) |
||||
| 262 | * @OA\Response(response=404, ref="#/components/responses/404") |
||||
| 263 | * @OA\Response(response=503, ref="#/components/responses/503") |
||||
| 264 | * @OA\Response(response=504, ref="#/components/responses/504") |
||||
| 265 | * @param PageInfoRepository $pageInfoRepo |
||||
| 266 | * @param AutomatedEditsHelper $autoEditsHelper |
||||
| 267 | * @return Response|JsonResponse |
||||
| 268 | * See PageInfoControllerTest::testPageInfoApi() |
||||
| 269 | * @codeCoverageIgnore |
||||
| 270 | */ |
||||
| 271 | public function pageInfoApiAction( |
||||
| 272 | PageInfoRepository $pageInfoRepo, |
||||
| 273 | AutomatedEditsHelper $autoEditsHelper |
||||
| 274 | ): Response { |
||||
| 275 | $this->recordApiUsage('page/pageinfo'); |
||||
| 276 | |||||
| 277 | $this->setupPageInfo($pageInfoRepo, $autoEditsHelper); |
||||
| 278 | $data = []; |
||||
|
0 ignored issues
–
show
|
|||||
| 279 | |||||
| 280 | try { |
||||
| 281 | $data = $this->pageInfo->getPageInfoApiData($this->project, $this->page); |
||||
|
0 ignored issues
–
show
It seems like
$this->page can also be of type null; however, parameter $page of App\Model\PageInfoApi::getPageInfoApiData() 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
Loading history...
|
|||||
| 282 | } catch (ServerException $e) { |
||||
| 283 | // The Wikimedia action API can fail for any number of reasons. To our users |
||||
| 284 | // any ServerException means the data could not be fetched, so we capture it here |
||||
| 285 | // to avoid the flood of automated emails when the API goes down, etc. |
||||
| 286 | $data['error'] = $this->i18n->msg('api-error', [$this->project->getDomain()]); |
||||
| 287 | } |
||||
| 288 | |||||
| 289 | if ('html' === $this->request->query->get('format')) { |
||||
| 290 | return $this->getApiHtmlResponse($this->project, $this->page, $data); |
||||
|
0 ignored issues
–
show
It seems like
$this->page can also be of type null; however, parameter $page of App\Controller\PageInfoC...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
Loading history...
|
|||||
| 291 | } |
||||
| 292 | |||||
| 293 | $this->addFlash('warning', 'In XTools 3.21, the last_edit_id property will be removed. ' . |
||||
| 294 | 'Use the modified_rev_id property instead.'); |
||||
| 295 | $data['last_edit_id'] = $data['modified_rev_id']; |
||||
| 296 | $this->addFlash('warning', 'In XTools 3.21, the author and author_editcount properties ' . |
||||
| 297 | 'will be removed. Instead, use creator and creator_editcount, respectively.'); |
||||
| 298 | $data['author'] = $data['creator']; |
||||
| 299 | $data['author_editcount'] = $data['creator_editcount']; |
||||
| 300 | |||||
| 301 | return $this->getFormattedApiResponse($data); |
||||
| 302 | } |
||||
| 303 | |||||
| 304 | /** |
||||
| 305 | * Get the Response for the HTML output of the PageInfo API action. |
||||
| 306 | * @param Project $project |
||||
| 307 | * @param Page $page |
||||
| 308 | * @param string[] $data The pre-fetched data. |
||||
| 309 | * @return Response |
||||
| 310 | * @codeCoverageIgnore |
||||
| 311 | */ |
||||
| 312 | private function getApiHtmlResponse(Project $project, Page $page, array $data): Response |
||||
| 313 | { |
||||
| 314 | $response = $this->render('pageInfo/api.html.twig', [ |
||||
| 315 | 'project' => $project, |
||||
| 316 | 'page' => $page, |
||||
| 317 | 'data' => $data, |
||||
| 318 | ]); |
||||
| 319 | |||||
| 320 | // All /api routes by default respond with a JSON content type. |
||||
| 321 | $response->headers->set('Content-Type', 'text/html'); |
||||
| 322 | |||||
| 323 | // This endpoint is hit constantly and user could be browsing the same page over |
||||
| 324 | // and over (popular noticeboard, for instance), so offload brief caching to browser. |
||||
| 325 | $response->setClientTtl(350); |
||||
| 326 | |||||
| 327 | return $response; |
||||
| 328 | } |
||||
| 329 | |||||
| 330 | /** |
||||
| 331 | * Get prose statistics for the given page. |
||||
| 332 | * @Route( |
||||
| 333 | * "/api/page/prose/{project}/{page}", |
||||
| 334 | * name="PageApiProse", |
||||
| 335 | * requirements={"page"=".+"}, |
||||
| 336 | * methods={"GET"} |
||||
| 337 | * ) |
||||
| 338 | * @OA\Tag(name="Page API") |
||||
| 339 | * @OA\ExternalDocumentation(url="https://www.mediawiki.org/wiki/XTools/Page_History#Prose") |
||||
| 340 | * @OA\Get(description="Get statistics about the [prose](https://en.wiktionary.org/wiki/prose) (characters, |
||||
| 341 | word count, etc.) and referencing of a page. ([more info](https://w.wiki/6oAF))") |
||||
| 342 | * @OA\Parameter(ref="#/components/parameters/Project") |
||||
| 343 | * @OA\Parameter(ref="#/components/parameters/Page", @OA\Schema(example="Metallica")) |
||||
| 344 | * @OA\Response( |
||||
| 345 | * response=200, |
||||
| 346 | * description="Prose stats", |
||||
| 347 | * @OA\JsonContent( |
||||
| 348 | * @OA\Property(property="project", ref="#/components/parameters/Project/schema"), |
||||
| 349 | * @OA\Property(property="page", ref="#/components/parameters/Page/schema"), |
||||
| 350 | * @OA\Property(property="bytes", type="integer"), |
||||
| 351 | * @OA\Property(property="characters", type="integer"), |
||||
| 352 | * @OA\Property(property="words", type="integer"), |
||||
| 353 | * @OA\Property(property="references", type="integer"), |
||||
| 354 | * @OA\Property(property="unique_references", type="integer"), |
||||
| 355 | * @OA\Property(property="sections", type="integer"), |
||||
| 356 | * @OA\Property(property="elapsed_time", ref="#/components/schemas/elapsed_time") |
||||
| 357 | * ) |
||||
| 358 | * ) |
||||
| 359 | * @OA\Response(response=404, ref="#/components/responses/404") |
||||
| 360 | * @param PageInfoRepository $pageInfoRepo |
||||
| 361 | * @param AutomatedEditsHelper $autoEditsHelper |
||||
| 362 | * @return JsonResponse |
||||
| 363 | * @codeCoverageIgnore |
||||
| 364 | */ |
||||
| 365 | public function proseStatsApiAction( |
||||
| 366 | PageInfoRepository $pageInfoRepo, |
||||
| 367 | AutomatedEditsHelper $autoEditsHelper |
||||
| 368 | ): JsonResponse { |
||||
| 369 | $responseCode = Response::HTTP_OK; |
||||
| 370 | $this->recordApiUsage('page/prose'); |
||||
| 371 | $this->setupPageInfo($pageInfoRepo, $autoEditsHelper); |
||||
| 372 | $this->addFlash('info', 'The algorithm used by this API has recently changed. ' . |
||||
| 373 | 'See https://www.mediawiki.org/wiki/XTools/Page_History#Prose for details.'); |
||||
| 374 | $ret = $this->pageInfo->getProseStats(); |
||||
| 375 | if (null === $ret) { |
||||
| 376 | $this->addFlashMessage('error', 'api-error-wikimedia'); |
||||
| 377 | $responseCode = Response::HTTP_BAD_GATEWAY; |
||||
| 378 | $ret = []; |
||||
| 379 | } |
||||
| 380 | return $this->getFormattedApiResponse($ret, $responseCode); |
||||
| 381 | } |
||||
| 382 | |||||
| 383 | /** |
||||
| 384 | * Get the page assessments of one or more pages, along with various related metadata. |
||||
| 385 | * @Route( |
||||
| 386 | * "/api/page/assessments/{project}/{pages}", |
||||
| 387 | * name="PageApiAssessments", |
||||
| 388 | * requirements={"pages"=".+"}, |
||||
| 389 | * methods={"GET"} |
||||
| 390 | * ) |
||||
| 391 | * @OA\Tag(name="Page API") |
||||
| 392 | * @OA\Get(description="Get [assessment data](https://w.wiki/6oAM) of the given pages, including the overall |
||||
| 393 | quality classifications, along with a list of the WikiProjects and their classifications and importance levels.") |
||||
| 394 | * @OA\Parameter(ref="#/components/parameters/Project") |
||||
| 395 | * @OA\Parameter(ref="#/components/parameters/Pages") |
||||
| 396 | * @OA\Parameter(name="classonly", in="query", @OA\Schema(type="boolean"), |
||||
| 397 | * description="Return only the overall quality assessment instead of for each applicable WikiProject." |
||||
| 398 | * ) |
||||
| 399 | * @OA\Response( |
||||
| 400 | * response=200, |
||||
| 401 | * description="Assessmnet data", |
||||
| 402 | * @OA\JsonContent( |
||||
| 403 | * @OA\Property(property="project", ref="#/components/parameters/Project/schema"), |
||||
| 404 | * @OA\Property(property="pages", type="object", |
||||
| 405 | * @OA\Property(property="Page title", type="object", |
||||
| 406 | * @OA\Property(property="assessment", ref="#/components/schemas/PageAssessment"), |
||||
| 407 | * @OA\Property(property="wikiprojects", type="object", |
||||
| 408 | * @OA\Property(property="name of WikiProject", |
||||
| 409 | * ref="#/components/schemas/PageAssessmentWikiProject" |
||||
| 410 | * ) |
||||
| 411 | * ) |
||||
| 412 | * ) |
||||
| 413 | * ), |
||||
| 414 | * @OA\Property(property="elapsed_time", ref="#/components/schemas/elapsed_time") |
||||
| 415 | * ) |
||||
| 416 | * ) |
||||
| 417 | * @OA\Response(response=404, ref="#/components/responses/404") |
||||
| 418 | * @param string $pages May be multiple pages separated by pipes, e.g. Foo|Bar|Baz |
||||
| 419 | * @return JsonResponse |
||||
| 420 | * @codeCoverageIgnore |
||||
| 421 | */ |
||||
| 422 | public function assessmentsApiAction(string $pages): JsonResponse |
||||
| 423 | { |
||||
| 424 | $this->recordApiUsage('page/assessments'); |
||||
| 425 | |||||
| 426 | $pages = explode('|', $pages); |
||||
| 427 | $out = [ |
||||
| 428 | 'pages' => [], |
||||
| 429 | ]; |
||||
| 430 | |||||
| 431 | foreach ($pages as $pageTitle) { |
||||
| 432 | try { |
||||
| 433 | $page = $this->validatePage($pageTitle); |
||||
| 434 | $assessments = $page->getProject() |
||||
| 435 | ->getPageAssessments() |
||||
| 436 | ->getAssessments($page); |
||||
| 437 | |||||
| 438 | $out['pages'][$page->getTitle()] = $this->getBoolVal('classonly') |
||||
| 439 | ? $assessments['assessment'] |
||||
| 440 | : $assessments; |
||||
| 441 | } catch (XtoolsHttpException $e) { |
||||
| 442 | $out['pages'][$pageTitle] = false; |
||||
| 443 | } |
||||
| 444 | } |
||||
| 445 | |||||
| 446 | return $this->getFormattedApiResponse($out); |
||||
| 447 | } |
||||
| 448 | |||||
| 449 | /** |
||||
| 450 | * Get number of in and outgoing links, external links, and redirects to the given page. |
||||
| 451 | * @Route( |
||||
| 452 | * "/api/page/links/{project}/{page}", |
||||
| 453 | * name="PageApiLinks", |
||||
| 454 | * requirements={"page"=".+"}, |
||||
| 455 | * methods={"GET"} |
||||
| 456 | * ) |
||||
| 457 | * @OA\Tag(name="Page API") |
||||
| 458 | * @OA\Parameter(ref="#/components/parameters/Project") |
||||
| 459 | * @OA\Parameter(ref="#/components/parameters/Page") |
||||
| 460 | * @OA\Response( |
||||
| 461 | * response=200, |
||||
| 462 | * description="Counts of in and outgoing links, external links, and redirects.", |
||||
| 463 | * @OA\JsonContent( |
||||
| 464 | * @OA\Property(property="project", ref="#/components/parameters/Project/schema"), |
||||
| 465 | * @OA\Property(property="page", ref="#/components/parameters/Page/schema"), |
||||
| 466 | * @OA\Property(property="links_ext_count", type="integer"), |
||||
| 467 | * @OA\Property(property="links_out_count", type="integer"), |
||||
| 468 | * @OA\Property(property="links_in_count", type="integer"), |
||||
| 469 | * @OA\Property(property="redirects_count", type="integer"), |
||||
| 470 | * @OA\Property(property="elapsed_time", ref="#/components/schemas/elapsed_time") |
||||
| 471 | * ) |
||||
| 472 | * ) |
||||
| 473 | * @OA\Response(response=404, ref="#/components/responses/404") |
||||
| 474 | * @OA\Response(response=503, ref="#/components/responses/503") |
||||
| 475 | * @OA\Response(response=504, ref="#/components/responses/504") |
||||
| 476 | * @return JsonResponse |
||||
| 477 | * @codeCoverageIgnore |
||||
| 478 | */ |
||||
| 479 | public function linksApiAction(): JsonResponse |
||||
| 480 | { |
||||
| 481 | $this->recordApiUsage('page/links'); |
||||
| 482 | return $this->getFormattedApiResponse($this->page->countLinksAndRedirects()); |
||||
| 483 | } |
||||
| 484 | |||||
| 485 | /** |
||||
| 486 | * Get the top editors (by number of edits) of a page. |
||||
| 487 | * @Route( |
||||
| 488 | * "/api/page/top_editors/{project}/{page}/{start}/{end}/{limit}", name="PageApiTopEditors", |
||||
| 489 | * requirements={ |
||||
| 490 | * "page"="(.+?)(?!\/(?:|\d{4}-\d{2}-\d{2})(?:\/(|\d{4}-\d{2}-\d{2}))?(?:\/(\d+))?)?$", |
||||
| 491 | * "start"="|\d{4}-\d{2}-\d{2}", |
||||
| 492 | * "end"="|\d{4}-\d{2}-\d{2}", |
||||
| 493 | * "limit"="\d+" |
||||
| 494 | * }, |
||||
| 495 | * defaults={ |
||||
| 496 | * "start"=false, |
||||
| 497 | * "end"=false, |
||||
| 498 | * "limit"=20, |
||||
| 499 | * }, |
||||
| 500 | * methods={"GET"} |
||||
| 501 | * ) |
||||
| 502 | * @OA\Tag(name="Page API") |
||||
| 503 | * @OA\Parameter(ref="#/components/parameters/Project") |
||||
| 504 | * @OA\Parameter(ref="#/components/parameters/Page") |
||||
| 505 | * @OA\Parameter(ref="#/components/parameters/Start") |
||||
| 506 | * @OA\Parameter(ref="#/components/parameters/End") |
||||
| 507 | * @OA\Parameter(ref="#/components/parameters/Limit") |
||||
| 508 | * @OA\Parameter(name="nobots", in="query", |
||||
| 509 | * description="Exclude bots from the results.", @OA\Schema(type="boolean") |
||||
| 510 | * ) |
||||
| 511 | * @OA\Response( |
||||
| 512 | * response=200, |
||||
| 513 | * description="List of the top editors, sorted by how many edits they've made to the page.", |
||||
| 514 | * @OA\JsonContent( |
||||
| 515 | * @OA\Property(property="project", ref="#/components/parameters/Project/schema"), |
||||
| 516 | * @OA\Property(property="page", ref="#/components/parameters/Page/schema"), |
||||
| 517 | * @OA\Property(property="start", ref="#/components/parameters/Start/schema"), |
||||
| 518 | * @OA\Property(property="end", ref="#/components/parameters/End/schema"), |
||||
| 519 | * @OA\Property(property="limit", ref="#/components/parameters/Limit/schema"), |
||||
| 520 | * @OA\Property(property="top_editors", type="array", @OA\Items(type="object"), example={ |
||||
| 521 | * { |
||||
| 522 | * "rank": 1, |
||||
| 523 | * "username": "Jimbo Wales", |
||||
| 524 | * "count": 50, |
||||
| 525 | * "minor": 15, |
||||
| 526 | * "first_edit": { |
||||
| 527 | * "id": 12345, |
||||
| 528 | * "timestamp": "2020-01-01T12:59:59Z" |
||||
| 529 | * }, |
||||
| 530 | * "last_edit": { |
||||
| 531 | * "id": 54321, |
||||
| 532 | * "timestamp": "2020-01-20T12:59:59Z" |
||||
| 533 | * } |
||||
| 534 | * } |
||||
| 535 | * }), |
||||
| 536 | * @OA\Property(property="elapsed_time", ref="#/components/schemas/elapsed_time") |
||||
| 537 | * ) |
||||
| 538 | * ) |
||||
| 539 | * @OA\Response(response=404, ref="#/components/responses/404") |
||||
| 540 | * @OA\Response(response=503, ref="#/components/responses/503") |
||||
| 541 | * @OA\Response(response=504, ref="#/components/responses/504") |
||||
| 542 | * @param PageInfoRepository $pageInfoRepo |
||||
| 543 | * @param AutomatedEditsHelper $autoEditsHelper |
||||
| 544 | * @return JsonResponse |
||||
| 545 | * @codeCoverageIgnore |
||||
| 546 | */ |
||||
| 547 | public function topEditorsApiAction( |
||||
| 548 | PageInfoRepository $pageInfoRepo, |
||||
| 549 | AutomatedEditsHelper $autoEditsHelper |
||||
| 550 | ): JsonResponse { |
||||
| 551 | $this->recordApiUsage('page/top_editors'); |
||||
| 552 | |||||
| 553 | $this->setupPageInfo($pageInfoRepo, $autoEditsHelper); |
||||
| 554 | $topEditors = $this->pageInfo->getTopEditorsByEditCount( |
||||
| 555 | (int)$this->limit, |
||||
| 556 | $this->getBoolVal('nobots') |
||||
| 557 | ); |
||||
| 558 | |||||
| 559 | return $this->getFormattedApiResponse([ |
||||
| 560 | 'top_editors' => $topEditors, |
||||
| 561 | ]); |
||||
| 562 | } |
||||
| 563 | |||||
| 564 | /** |
||||
| 565 | * Get data about bots that have edited a page. |
||||
| 566 | * @Route( |
||||
| 567 | * "/api/page/bot_data/{project}/{page}/{start}/{end}", name="PageApiBotData", |
||||
| 568 | * requirements={ |
||||
| 569 | * "page"="(.+?)(?!\/(?:|\d{4}-\d{2}-\d{2})(?:\/(|\d{4}-\d{2}-\d{2}))?)?$", |
||||
| 570 | * "start"="|\d{4}-\d{2}-\d{2}", |
||||
| 571 | * "end"="|\d{4}-\d{2}-\d{2}", |
||||
| 572 | * }, |
||||
| 573 | * defaults={ |
||||
| 574 | * "start"=false, |
||||
| 575 | * "end"=false, |
||||
| 576 | * }, |
||||
| 577 | * methods={"GET"} |
||||
| 578 | * ) |
||||
| 579 | * @OA\Tag(name="Page API") |
||||
| 580 | * @OA\Get(description="List bots that have edited a page, with edit counts and whether the account |
||||
| 581 | is still in the `bot` user group.") |
||||
| 582 | * @OA\Parameter(ref="#/components/parameters/Project") |
||||
| 583 | * @OA\Parameter(ref="#/components/parameters/Page") |
||||
| 584 | * @OA\Parameter(ref="#/components/parameters/Start") |
||||
| 585 | * @OA\Parameter(ref="#/components/parameters/End") |
||||
| 586 | * @OA\Response( |
||||
| 587 | * response=200, |
||||
| 588 | * description="List of bots", |
||||
| 589 | * @OA\JsonContent( |
||||
| 590 | * @OA\Property(property="project", ref="#/components/parameters/Project/schema"), |
||||
| 591 | * @OA\Property(property="page", ref="#/components/parameters/Page/schema"), |
||||
| 592 | * @OA\Property(property="start", ref="#/components/parameters/Start/schema"), |
||||
| 593 | * @OA\Property(property="end", ref="#/components/parameters/End/schema"), |
||||
| 594 | * @OA\Property(property="bots", type="object", |
||||
| 595 | * @OA\Property(property="Page title", type="object", |
||||
| 596 | * @OA\Property(property="count", type="integer", description="Number of edits to the page."), |
||||
| 597 | * @OA\Property(property="current", type="boolean", |
||||
| 598 | * description="Whether the account currently has the bot flag" |
||||
| 599 | * ) |
||||
| 600 | * ) |
||||
| 601 | * ), |
||||
| 602 | * @OA\Property(property="elapsed_time", ref="#/components/schemas/elapsed_time") |
||||
| 603 | * ) |
||||
| 604 | * ) |
||||
| 605 | * @OA\Response(response=404, ref="#/components/responses/404") |
||||
| 606 | * @OA\Response(response=503, ref="#/components/responses/503") |
||||
| 607 | * @OA\Response(response=504, ref="#/components/responses/504") |
||||
| 608 | * @param PageInfoRepository $pageInfoRepo |
||||
| 609 | * @param AutomatedEditsHelper $autoEditsHelper |
||||
| 610 | * @return JsonResponse |
||||
| 611 | * @codeCoverageIgnore |
||||
| 612 | */ |
||||
| 613 | public function botDataApiAction( |
||||
| 614 | PageInfoRepository $pageInfoRepo, |
||||
| 615 | AutomatedEditsHelper $autoEditsHelper |
||||
| 616 | ): JsonResponse { |
||||
| 617 | $this->recordApiUsage('page/bot_data'); |
||||
| 618 | |||||
| 619 | $this->setupPageInfo($pageInfoRepo, $autoEditsHelper); |
||||
| 620 | $bots = $this->pageInfo->getBots(); |
||||
| 621 | |||||
| 622 | return $this->getFormattedApiResponse([ |
||||
| 623 | 'bots' => $bots, |
||||
| 624 | ]); |
||||
| 625 | } |
||||
| 626 | |||||
| 627 | /** |
||||
| 628 | * Get counts of (semi-)automated tools that were used to edit the page. |
||||
| 629 | * @Route( |
||||
| 630 | * "/api/page/automated_edits/{project}/{page}/{start}/{end}", name="PageApiAutoEdits", |
||||
| 631 | * requirements={ |
||||
| 632 | * "page"="(.+?)(?!\/(?:|\d{4}-\d{2}-\d{2})(?:\/(|\d{4}-\d{2}-\d{2}))?)?$", |
||||
| 633 | * "start"="|\d{4}-\d{2}-\d{2}", |
||||
| 634 | * "end"="|\d{4}-\d{2}-\d{2}", |
||||
| 635 | * }, |
||||
| 636 | * defaults={ |
||||
| 637 | * "start"=false, |
||||
| 638 | * "end"=false, |
||||
| 639 | * }, |
||||
| 640 | * methods={"GET"} |
||||
| 641 | * ) |
||||
| 642 | * @OA\Tag(name="Page API") |
||||
| 643 | * @OA\Get(description="Get counts of the number of times known (semi-)automated tools were used to edit the page.") |
||||
| 644 | * @OA\Parameter(ref="#/components/parameters/Project") |
||||
| 645 | * @OA\Parameter(ref="#/components/parameters/Page") |
||||
| 646 | * @OA\Parameter(ref="#/components/parameters/Start") |
||||
| 647 | * @OA\Parameter(ref="#/components/parameters/End") |
||||
| 648 | * @OA\Response( |
||||
| 649 | * response=200, |
||||
| 650 | * description="List of tools", |
||||
| 651 | * @OA\JsonContent( |
||||
| 652 | * @OA\Property(property="project", ref="#/components/parameters/Project/schema"), |
||||
| 653 | * @OA\Property(property="page", ref="#/components/parameters/Page/schema"), |
||||
| 654 | * @OA\Property(property="start", ref="#/components/parameters/Start/schema"), |
||||
| 655 | * @OA\Property(property="end", ref="#/components/parameters/End/schema"), |
||||
| 656 | * @OA\Property(property="automated_tools", ref="#/components/schemas/AutomatedTools"), |
||||
| 657 | * @OA\Property(property="elapsed_time", ref="#/components/schemas/elapsed_time") |
||||
| 658 | * ) |
||||
| 659 | * ) |
||||
| 660 | * @OA\Response(response=404, ref="#/components/responses/404") |
||||
| 661 | * @OA\Response(response=503, ref="#/components/responses/503") |
||||
| 662 | * @OA\Response(response=504, ref="#/components/responses/504") |
||||
| 663 | * @param PageInfoRepository $pageInfoRepo |
||||
| 664 | * @param AutomatedEditsHelper $autoEditsHelper |
||||
| 665 | * @return JsonResponse |
||||
| 666 | * @codeCoverageIgnore |
||||
| 667 | */ |
||||
| 668 | public function getAutoEdits( |
||||
| 669 | PageInfoRepository $pageInfoRepo, |
||||
| 670 | AutomatedEditsHelper $autoEditsHelper |
||||
| 671 | ): JsonResponse { |
||||
| 672 | $this->recordApiUsage('page/auto_edits'); |
||||
| 673 | |||||
| 674 | $this->setupPageInfo($pageInfoRepo, $autoEditsHelper); |
||||
| 675 | return $this->getFormattedApiResponse([ |
||||
| 676 | 'automated_tools' => $this->pageInfo->getAutoEditsCounts(), |
||||
| 677 | ]); |
||||
| 678 | } |
||||
| 679 | } |
||||
| 680 |