Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 365
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiQueryDeletedrevs
0.00% covered (danger)
0.00%
0 / 365
0.00% covered (danger)
0.00%
0 / 6
7656
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 252
0.00% covered (danger)
0.00%
0 / 1
6480
 isDeprecated
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getAllowedParams
0.00% covered (danger)
0.00%
0 / 86
0.00% covered (danger)
0.00%
0 / 1
6
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
6
 getHelpUrls
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Copyright © 2007 Roan Kattouw <roan.kattouw@gmail.com>
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 */
22
23use MediaWiki\Cache\LinkBatchFactory;
24use MediaWiki\CommentFormatter\RowCommentFormatter;
25use MediaWiki\CommentStore\CommentStore;
26use MediaWiki\ParamValidator\TypeDef\UserDef;
27use MediaWiki\Revision\RevisionRecord;
28use MediaWiki\Revision\RevisionStore;
29use MediaWiki\Revision\SlotRecord;
30use MediaWiki\Storage\NameTableAccessException;
31use MediaWiki\Storage\NameTableStore;
32use MediaWiki\Title\Title;
33use Wikimedia\ParamValidator\ParamValidator;
34use Wikimedia\ParamValidator\TypeDef\EnumDef;
35use Wikimedia\ParamValidator\TypeDef\IntegerDef;
36use Wikimedia\Rdbms\IExpression;
37use Wikimedia\Rdbms\LikeValue;
38
39/**
40 * Query module to enumerate all deleted revisions.
41 *
42 * @ingroup API
43 * @deprecated since 1.25
44 */
45class ApiQueryDeletedrevs extends ApiQueryBase {
46
47    private CommentStore $commentStore;
48    private RowCommentFormatter $commentFormatter;
49    private RevisionStore $revisionStore;
50    private NameTableStore $changeTagDefStore;
51    private LinkBatchFactory $linkBatchFactory;
52
53    /**
54     * @param ApiQuery $query
55     * @param string $moduleName
56     * @param CommentStore $commentStore
57     * @param RowCommentFormatter $commentFormatter
58     * @param RevisionStore $revisionStore
59     * @param NameTableStore $changeTagDefStore
60     * @param LinkBatchFactory $linkBatchFactory
61     */
62    public function __construct(
63        ApiQuery $query,
64        $moduleName,
65        CommentStore $commentStore,
66        RowCommentFormatter $commentFormatter,
67        RevisionStore $revisionStore,
68        NameTableStore $changeTagDefStore,
69        LinkBatchFactory $linkBatchFactory
70    ) {
71        parent::__construct( $query, $moduleName, 'dr' );
72        $this->commentStore = $commentStore;
73        $this->commentFormatter = $commentFormatter;
74        $this->revisionStore = $revisionStore;
75        $this->changeTagDefStore = $changeTagDefStore;
76        $this->linkBatchFactory = $linkBatchFactory;
77    }
78
79    public function execute() {
80        // Before doing anything at all, let's check permissions
81        $this->checkUserRightsAny( 'deletedhistory' );
82
83        $this->addDeprecation( 'apiwarn-deprecation-deletedrevs', 'action=query&list=deletedrevs' );
84
85        $user = $this->getUser();
86        $db = $this->getDB();
87        $params = $this->extractRequestParams( false );
88        $prop = array_fill_keys( $params['prop'], true );
89        $fld_parentid = isset( $prop['parentid'] );
90        $fld_revid = isset( $prop['revid'] );
91        $fld_user = isset( $prop['user'] );
92        $fld_userid = isset( $prop['userid'] );
93        $fld_comment = isset( $prop['comment'] );
94        $fld_parsedcomment = isset( $prop['parsedcomment'] );
95        $fld_minor = isset( $prop['minor'] );
96        $fld_len = isset( $prop['len'] );
97        $fld_sha1 = isset( $prop['sha1'] );
98        $fld_content = isset( $prop['content'] );
99        $fld_token = isset( $prop['token'] );
100        $fld_tags = isset( $prop['tags'] );
101
102        // If we're in a mode that breaks the same-origin policy, no tokens can
103        // be obtained
104        if ( $this->lacksSameOriginSecurity() ||
105            // If user can't undelete, no tokens
106            !$this->getAuthority()->isAllowed( 'undelete' )
107        ) {
108            $fld_token = false;
109        }
110
111        $result = $this->getResult();
112        $pageSet = $this->getPageSet();
113        $titles = $pageSet->getPages();
114
115        // This module operates in three modes:
116        // 'revs': List deleted revs for certain titles (1)
117        // 'user': List deleted revs by a certain user (2)
118        // 'all': List all deleted revs in NS (3)
119        $mode = 'all';
120        if ( count( $titles ) > 0 ) {
121            $mode = 'revs';
122        } elseif ( $params['user'] !== null ) {
123            $mode = 'user';
124        }
125
126        if ( $mode == 'revs' || $mode == 'user' ) {
127            // Ignore namespace and unique due to inability to know whether they were purposely set
128            foreach ( [ 'from', 'to', 'prefix', /*'namespace', 'unique'*/ ] as $p ) {
129                if ( $params[$p] !== null ) {
130                    $this->dieWithError( [ 'apierror-deletedrevs-param-not-1-2', $p ], 'badparams' );
131                }
132            }
133        } else {
134            foreach ( [ 'start', 'end' ] as $p ) {
135                if ( $params[$p] !== null ) {
136                    $this->dieWithError( [ 'apierror-deletedrevs-param-not-3', $p ], 'badparams' );
137                }
138            }
139        }
140
141        if ( $params['user'] !== null && $params['excludeuser'] !== null ) {
142            $this->dieWithError( 'user and excludeuser cannot be used together', 'badparams' );
143        }
144
145        $arQuery = $this->revisionStore->getArchiveQueryInfo();
146        $this->addTables( $arQuery['tables'] );
147        $this->addFields( $arQuery['fields'] );
148        $this->addJoinConds( $arQuery['joins'] );
149        $this->addFields( [ 'ar_title', 'ar_namespace' ] );
150
151        if ( $fld_tags ) {
152            $this->addFields( [ 'ts_tags' => ChangeTags::makeTagSummarySubquery( 'archive' ) ] );
153        }
154
155        if ( $params['tag'] !== null ) {
156            $this->addTables( 'change_tag' );
157            $this->addJoinConds(
158                [ 'change_tag' => [ 'JOIN', [ 'ar_rev_id=ct_rev_id' ] ] ]
159            );
160            try {
161                $this->addWhereFld( 'ct_tag_id', $this->changeTagDefStore->getId( $params['tag'] ) );
162            } catch ( NameTableAccessException $exception ) {
163                // Return nothing.
164                $this->addWhere( '1=0' );
165            }
166        }
167
168        // This means stricter restrictions
169        if ( $fld_content ) {
170            $this->checkUserRightsAny( [ 'deletedtext', 'undelete' ] );
171        }
172        // Check limits
173        $userMax = $fld_content ? ApiBase::LIMIT_SML1 : ApiBase::LIMIT_BIG1;
174        $botMax = $fld_content ? ApiBase::LIMIT_SML2 : ApiBase::LIMIT_BIG2;
175
176        $limit = $params['limit'];
177
178        if ( $limit == 'max' ) {
179            $limit = $this->getMain()->canApiHighLimits() ? $botMax : $userMax;
180            $this->getResult()->addParsedLimit( $this->getModuleName(), $limit );
181        }
182
183        $limit = $this->getMain()->getParamValidator()->validateValue(
184            $this, 'limit', $limit, [
185                ParamValidator::PARAM_TYPE => 'limit',
186                IntegerDef::PARAM_MIN => 1,
187                IntegerDef::PARAM_MAX => $userMax,
188                IntegerDef::PARAM_MAX2 => $botMax,
189                IntegerDef::PARAM_IGNORE_RANGE => true,
190            ]
191        );
192
193        if ( $fld_token ) {
194            // Undelete tokens are identical for all pages, so we cache one here
195            $token = $user->getEditToken( '', $this->getMain()->getRequest() );
196        }
197
198        $dir = $params['dir'];
199
200        // We need a custom WHERE clause that matches all titles.
201        if ( $mode == 'revs' ) {
202            $lb = $this->linkBatchFactory->newLinkBatch( $titles );
203            $where = $lb->constructSet( 'ar', $db );
204            $this->addWhere( $where );
205        } elseif ( $mode == 'all' ) {
206            $this->addWhereFld( 'ar_namespace', $params['namespace'] );
207
208            $from = $params['from'] === null
209                ? null
210                : $this->titlePartToKey( $params['from'], $params['namespace'] );
211            $to = $params['to'] === null
212                ? null
213                : $this->titlePartToKey( $params['to'], $params['namespace'] );
214            $this->addWhereRange( 'ar_title', $dir, $from, $to );
215
216            if ( isset( $params['prefix'] ) ) {
217                $this->addWhere(
218                    $db->expr(
219                        'ar_title',
220                        IExpression::LIKE,
221                        new LikeValue(
222                            $this->titlePartToKey( $params['prefix'], $params['namespace'] ),
223                            $db->anyString()
224                        )
225                    )
226                );
227            }
228        }
229
230        if ( $params['user'] !== null ) {
231            // We already join on actor due to getArchiveQueryInfo()
232            $this->addWhereFld( 'actor_name', $params['user'] );
233        } elseif ( $params['excludeuser'] !== null ) {
234            $this->addWhere( $db->expr( 'actor_name', '!=', $params['excludeuser'] ) );
235        }
236
237        if ( $params['user'] !== null || $params['excludeuser'] !== null ) {
238            // Paranoia: avoid brute force searches (T19342)
239            // (shouldn't be able to get here without 'deletedhistory', but
240            // check it again just in case)
241            if ( !$this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
242                $bitmask = RevisionRecord::DELETED_USER;
243            } elseif ( !$this->getAuthority()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
244                $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
245            } else {
246                $bitmask = 0;
247            }
248            if ( $bitmask ) {
249                $this->addWhere( $db->bitAnd( 'ar_deleted', $bitmask ) . " != $bitmask" );
250            }
251        }
252
253        if ( $params['continue'] !== null ) {
254            $op = ( $dir == 'newer' ? '>=' : '<=' );
255            if ( $mode == 'all' || $mode == 'revs' ) {
256                $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'int', 'string', 'timestamp', 'int' ] );
257                $this->addWhere( $db->buildComparison( $op, [
258                    'ar_namespace' => $cont[0],
259                    'ar_title' => $cont[1],
260                    'ar_timestamp' => $db->timestamp( $cont[2] ),
261                    'ar_id' => $cont[3],
262                ] ) );
263            } else {
264                $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'timestamp', 'int' ] );
265                $this->addWhere( $db->buildComparison( $op, [
266                    'ar_timestamp' => $db->timestamp( $cont[0] ),
267                    'ar_id' => $cont[1],
268                ] ) );
269            }
270        }
271
272        $this->addOption( 'LIMIT', $limit + 1 );
273        if ( $mode == 'all' ) {
274            if ( $params['unique'] ) {
275                // @todo Does this work on non-MySQL?
276                $this->addOption( 'GROUP BY', 'ar_title' );
277            } else {
278                $sort = ( $dir == 'newer' ? '' : ' DESC' );
279                $this->addOption( 'ORDER BY', [
280                    'ar_title' . $sort,
281                    'ar_timestamp' . $sort,
282                    'ar_id' . $sort,
283                ] );
284            }
285        } else {
286            if ( $mode == 'revs' ) {
287                // Sort by ns and title in the same order as timestamp for efficiency
288                $this->addWhereRange( 'ar_namespace', $dir, null, null );
289                $this->addWhereRange( 'ar_title', $dir, null, null );
290            }
291            $this->addTimestampWhereRange( 'ar_timestamp', $dir, $params['start'], $params['end'] );
292            // Include in ORDER BY for uniqueness
293            $this->addWhereRange( 'ar_id', $dir, null, null );
294        }
295        $res = $this->select( __METHOD__ );
296
297        $formattedComments = [];
298        if ( $fld_parsedcomment ) {
299            $formattedComments = $this->commentFormatter->formatItems(
300                $this->commentFormatter->rows( $res )
301                    ->indexField( 'ar_id' )
302                    ->commentKey( 'ar_comment' )
303                    ->namespaceField( 'ar_namespace' )
304                    ->titleField( 'ar_title' )
305            );
306        }
307
308        $pageMap = []; // Maps ns&title to (fake) pageid
309        $count = 0;
310        $newPageID = 0;
311        foreach ( $res as $row ) {
312            if ( ++$count > $limit ) {
313                // We've had enough
314                if ( $mode == 'all' || $mode == 'revs' ) {
315                    $this->setContinueEnumParameter( 'continue',
316                        "$row->ar_namespace|$row->ar_title|$row->ar_timestamp|$row->ar_id"
317                    );
318                } else {
319                    $this->setContinueEnumParameter( 'continue', "$row->ar_timestamp|$row->ar_id" );
320                }
321                break;
322            }
323
324            $rev = [];
325            $anyHidden = false;
326
327            $rev['timestamp'] = wfTimestamp( TS_ISO_8601, $row->ar_timestamp );
328            if ( $fld_revid ) {
329                $rev['revid'] = (int)$row->ar_rev_id;
330            }
331            if ( $fld_parentid && $row->ar_parent_id !== null ) {
332                $rev['parentid'] = (int)$row->ar_parent_id;
333            }
334            if ( $fld_user || $fld_userid ) {
335                if ( $row->ar_deleted & RevisionRecord::DELETED_USER ) {
336                    $rev['userhidden'] = true;
337                    $anyHidden = true;
338                }
339                if ( RevisionRecord::userCanBitfield(
340                    $row->ar_deleted,
341                    RevisionRecord::DELETED_USER,
342                    $user
343                ) ) {
344                    if ( $fld_user ) {
345                        $rev['user'] = $row->ar_user_text;
346                    }
347                    if ( $fld_userid ) {
348                        $rev['userid'] = (int)$row->ar_user;
349                    }
350                }
351            }
352
353            if ( $fld_comment || $fld_parsedcomment ) {
354                if ( $row->ar_deleted & RevisionRecord::DELETED_COMMENT ) {
355                    $rev['commenthidden'] = true;
356                    $anyHidden = true;
357                }
358                if ( RevisionRecord::userCanBitfield(
359                    $row->ar_deleted,
360                    RevisionRecord::DELETED_COMMENT,
361                    $user
362                ) ) {
363                    $comment = $this->commentStore->getComment( 'ar_comment', $row )->text;
364                    if ( $fld_comment ) {
365                        $rev['comment'] = $comment;
366                    }
367                    if ( $fld_parsedcomment ) {
368                        $rev['parsedcomment'] = $formattedComments[$row->ar_id];
369                    }
370                }
371            }
372
373            if ( $fld_minor ) {
374                $rev['minor'] = $row->ar_minor_edit == 1;
375            }
376            if ( $fld_len ) {
377                $rev['len'] = $row->ar_len;
378            }
379            if ( $fld_sha1 ) {
380                if ( $row->ar_deleted & RevisionRecord::DELETED_TEXT ) {
381                    $rev['sha1hidden'] = true;
382                    $anyHidden = true;
383                }
384                if ( RevisionRecord::userCanBitfield(
385                    $row->ar_deleted,
386                    RevisionRecord::DELETED_TEXT,
387                    $user
388                ) ) {
389                    if ( $row->ar_sha1 != '' ) {
390                        $rev['sha1'] = Wikimedia\base_convert( $row->ar_sha1, 36, 16, 40 );
391                    } else {
392                        $rev['sha1'] = '';
393                    }
394                }
395            }
396            if ( $fld_content ) {
397                if ( $row->ar_deleted & RevisionRecord::DELETED_TEXT ) {
398                    $rev['texthidden'] = true;
399                    $anyHidden = true;
400                }
401                if ( RevisionRecord::userCanBitfield(
402                    $row->ar_deleted,
403                    RevisionRecord::DELETED_TEXT,
404                    $user
405                ) ) {
406                    ApiResult::setContentValue( $rev, 'text',
407                        $this->revisionStore->newRevisionFromArchiveRow( $row )
408                            ->getContent( SlotRecord::MAIN )->serialize() );
409                }
410            }
411
412            if ( $fld_tags ) {
413                if ( $row->ts_tags ) {
414                    $tags = explode( ',', $row->ts_tags );
415                    ApiResult::setIndexedTagName( $tags, 'tag' );
416                    $rev['tags'] = $tags;
417                } else {
418                    $rev['tags'] = [];
419                }
420            }
421
422            if ( $anyHidden && ( $row->ar_deleted & RevisionRecord::DELETED_RESTRICTED ) ) {
423                $rev['suppressed'] = true;
424            }
425
426            if ( !isset( $pageMap[$row->ar_namespace][$row->ar_title] ) ) {
427                $pageID = $newPageID++;
428                $pageMap[$row->ar_namespace][$row->ar_title] = $pageID;
429                $a = [ 'revisions' => [ $rev ] ];
430                ApiResult::setIndexedTagName( $a['revisions'], 'rev' );
431                $title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
432                ApiQueryBase::addTitleInfo( $a, $title );
433                if ( $fld_token ) {
434                    // @phan-suppress-next-line PhanPossiblyUndeclaredVariable token is set when used
435                    $a['token'] = $token;
436                }
437                $fit = $result->addValue( [ 'query', $this->getModuleName() ], $pageID, $a );
438            } else {
439                $pageID = $pageMap[$row->ar_namespace][$row->ar_title];
440                $fit = $result->addValue(
441                    [ 'query', $this->getModuleName(), $pageID, 'revisions' ],
442                    null, $rev );
443            }
444            if ( !$fit ) {
445                if ( $mode == 'all' || $mode == 'revs' ) {
446                    $this->setContinueEnumParameter( 'continue',
447                        "$row->ar_namespace|$row->ar_title|$row->ar_timestamp|$row->ar_id"
448                    );
449                } else {
450                    $this->setContinueEnumParameter( 'continue', "$row->ar_timestamp|$row->ar_id" );
451                }
452                break;
453            }
454        }
455        $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'page' );
456    }
457
458    public function isDeprecated() {
459        return true;
460    }
461
462    public function getAllowedParams() {
463        $smallLimit = $this->getMain()->canApiHighLimits() ? ApiBase::LIMIT_SML2 : ApiBase::LIMIT_SML1;
464        return [
465            'start' => [
466                ParamValidator::PARAM_TYPE => 'timestamp',
467                ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 1, 2 ] ],
468            ],
469            'end' => [
470                ParamValidator::PARAM_TYPE => 'timestamp',
471                ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 1, 2 ] ],
472            ],
473            'dir' => [
474                ParamValidator::PARAM_TYPE => [
475                    'newer',
476                    'older'
477                ],
478                ParamValidator::PARAM_DEFAULT => 'older',
479                ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
480                ApiBase::PARAM_HELP_MSG_PER_VALUE => [
481                    'newer' => 'api-help-paramvalue-direction-newer',
482                    'older' => 'api-help-paramvalue-direction-older',
483                ],
484                ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 1, 3 ] ],
485            ],
486            'from' => [
487                ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 3 ] ],
488            ],
489            'to' => [
490                ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 3 ] ],
491            ],
492            'prefix' => [
493                ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 3 ] ],
494            ],
495            'unique' => [
496                ParamValidator::PARAM_DEFAULT => false,
497                ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 3 ] ],
498            ],
499            'namespace' => [
500                ParamValidator::PARAM_TYPE => 'namespace',
501                ParamValidator::PARAM_DEFAULT => NS_MAIN,
502                ApiBase::PARAM_HELP_MSG_INFO => [ [ 'modes', 3 ] ],
503            ],
504            'tag' => null,
505            'user' => [
506                ParamValidator::PARAM_TYPE => 'user',
507                UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'id', 'interwiki' ],
508            ],
509            'excludeuser' => [
510                ParamValidator::PARAM_TYPE => 'user',
511                UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'id', 'interwiki' ],
512            ],
513            'prop' => [
514                ParamValidator::PARAM_DEFAULT => 'user|comment',
515                ParamValidator::PARAM_TYPE => [
516                    'revid',
517                    'parentid',
518                    'user',
519                    'userid',
520                    'comment',
521                    'parsedcomment',
522                    'minor',
523                    'len',
524                    'sha1',
525                    'content',
526                    'token',
527                    'tags'
528                ],
529                ParamValidator::PARAM_ISMULTI => true,
530                ApiBase::PARAM_HELP_MSG_PER_VALUE => [
531                    'content' => [ 'apihelp-query+deletedrevs-paramvalue-prop-content', $smallLimit ],
532                ],
533                EnumDef::PARAM_DEPRECATED_VALUES => [
534                    'token' => true,
535                ],
536            ],
537            'limit' => [
538                ParamValidator::PARAM_DEFAULT => 10,
539                ParamValidator::PARAM_TYPE => 'limit',
540                IntegerDef::PARAM_MIN => 1,
541                IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1,
542                IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2,
543                ApiBase::PARAM_HELP_MSG => [ 'apihelp-query+deletedrevs-param-limit', $smallLimit ],
544            ],
545            'continue' => [
546                ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
547            ],
548        ];
549    }
550
551    protected function getExamplesMessages() {
552        $title = Title::newMainPage();
553        $talkTitle = $title->getTalkPageIfDefined();
554        $examples = [];
555
556        if ( $talkTitle ) {
557            $title = rawurlencode( $title->getPrefixedText() );
558            $talkTitle = rawurlencode( $talkTitle->getPrefixedText() );
559            $examples = [
560                "action=query&list=deletedrevs&titles={$title}|{$talkTitle}&" .
561                    'drprop=user|comment|content'
562                    => 'apihelp-query+deletedrevs-example-mode1',
563            ];
564        }
565
566        return array_merge( $examples, [
567            'action=query&list=deletedrevs&druser=Bob&drlimit=50'
568                => 'apihelp-query+deletedrevs-example-mode2',
569            'action=query&list=deletedrevs&drdir=newer&drlimit=50'
570                => 'apihelp-query+deletedrevs-example-mode3-main',
571            'action=query&list=deletedrevs&drdir=newer&drlimit=50&drnamespace=1&drunique='
572                => 'apihelp-query+deletedrevs-example-mode3-talk',
573        ] );
574    }
575
576    public function getHelpUrls() {
577        return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Deletedrevs';
578    }
579}