Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 193
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiQueryDeletedRevisions
0.00% covered (danger)
0.00%
0 / 192
0.00% covered (danger)
0.00%
0 / 5
2756
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
2
 run
0.00% covered (danger)
0.00%
0 / 129
0.00% covered (danger)
0.00%
0 / 1
2256
 getAllowedParams
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
2
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 12
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 © 2014 Wikimedia Foundation and contributors
4 *
5 * Heavily based on ApiQueryDeletedrevs,
6 * Copyright © 2007 Roan Kattouw <roan.kattouw@gmail.com>
7 *
8 * @license GPL-2.0-or-later
9 * @file
10 */
11
12namespace MediaWiki\Api;
13
14use MediaWiki\ChangeTags\ChangeTagsStore;
15use MediaWiki\CommentFormatter\CommentFormatter;
16use MediaWiki\Content\IContentHandlerFactory;
17use MediaWiki\Content\Renderer\ContentRenderer;
18use MediaWiki\Content\Transform\ContentTransformer;
19use MediaWiki\Page\LinkBatchFactory;
20use MediaWiki\ParamValidator\TypeDef\UserDef;
21use MediaWiki\Parser\ParserFactory;
22use MediaWiki\Revision\RevisionRecord;
23use MediaWiki\Revision\RevisionStore;
24use MediaWiki\Revision\SlotRoleRegistry;
25use MediaWiki\Storage\NameTableAccessException;
26use MediaWiki\Storage\NameTableStore;
27use MediaWiki\Title\Title;
28use MediaWiki\User\TempUser\TempUserCreator;
29use MediaWiki\User\UserFactory;
30use Wikimedia\ParamValidator\ParamValidator;
31
32/**
33 * Query module to enumerate deleted revisions for pages.
34 *
35 * @ingroup API
36 */
37class ApiQueryDeletedRevisions extends ApiQueryRevisionsBase {
38
39    private RevisionStore $revisionStore;
40    private NameTableStore $changeTagDefStore;
41    private ChangeTagsStore $changeTagsStore;
42    private LinkBatchFactory $linkBatchFactory;
43
44    public function __construct(
45        ApiQuery $query,
46        string $moduleName,
47        RevisionStore $revisionStore,
48        IContentHandlerFactory $contentHandlerFactory,
49        ParserFactory $parserFactory,
50        SlotRoleRegistry $slotRoleRegistry,
51        NameTableStore $changeTagDefStore,
52        ChangeTagsStore $changeTagsStore,
53        LinkBatchFactory $linkBatchFactory,
54        ContentRenderer $contentRenderer,
55        ContentTransformer $contentTransformer,
56        CommentFormatter $commentFormatter,
57        TempUserCreator $tempUserCreator,
58        UserFactory $userFactory
59    ) {
60        parent::__construct(
61            $query,
62            $moduleName,
63            'drv',
64            $revisionStore,
65            $contentHandlerFactory,
66            $parserFactory,
67            $slotRoleRegistry,
68            $contentRenderer,
69            $contentTransformer,
70            $commentFormatter,
71            $tempUserCreator,
72            $userFactory
73        );
74        $this->revisionStore = $revisionStore;
75        $this->changeTagDefStore = $changeTagDefStore;
76        $this->changeTagsStore = $changeTagsStore;
77        $this->linkBatchFactory = $linkBatchFactory;
78    }
79
80    protected function run( ?ApiPageSet $resultPageSet = null ) {
81        $pageSet = $this->getPageSet();
82        $pageMap = $pageSet->getGoodAndMissingTitlesByNamespace();
83        $pageCount = count( $pageSet->getGoodAndMissingPages() );
84        $revCount = $pageSet->getRevisionCount();
85        if ( $revCount === 0 && $pageCount === 0 ) {
86            // Nothing to do
87            return;
88        }
89        if ( $revCount !== 0 && count( $pageSet->getDeletedRevisionIDs() ) === 0 ) {
90            // Nothing to do, revisions were supplied but none are deleted
91            return;
92        }
93
94        $params = $this->extractRequestParams( false );
95
96        $db = $this->getDB();
97
98        $this->requireMaxOneParameter( $params, 'user', 'excludeuser' );
99
100        if ( $resultPageSet === null ) {
101            $this->parseParameters( $params );
102            $arQuery = $this->revisionStore->getArchiveQueryInfo();
103            $this->addTables( $arQuery['tables'] );
104            $this->addFields( $arQuery['fields'] );
105            $this->addJoinConds( $arQuery['joins'] );
106            $this->addFields( [ 'ar_title', 'ar_namespace' ] );
107        } else {
108            $this->limit = $this->getParameter( 'limit' ) ?: 10;
109            $this->addTables( 'archive' );
110            $this->addFields( [ 'ar_title', 'ar_namespace', 'ar_timestamp', 'ar_rev_id', 'ar_id' ] );
111        }
112
113        if ( $this->fld_tags ) {
114            $this->addFields( [
115                'ts_tags' => $this->changeTagsStore->makeTagSummarySubquery( 'archive' )
116            ] );
117        }
118
119        if ( $params['tag'] !== null ) {
120            $this->addTables( 'change_tag' );
121            $this->addJoinConds(
122                [ 'change_tag' => [ 'JOIN', [ 'ar_rev_id=ct_rev_id' ] ] ]
123            );
124            try {
125                $this->addWhereFld( 'ct_tag_id', $this->changeTagDefStore->getId( $params['tag'] ) );
126            } catch ( NameTableAccessException ) {
127                // Return nothing.
128                $this->addWhere( '1=0' );
129            }
130        }
131
132        // This means stricter restrictions
133        if ( ( $this->fld_comment || $this->fld_parsedcomment ) &&
134            !$this->getAuthority()->isAllowed( 'deletedhistory' )
135        ) {
136            $this->dieWithError( 'apierror-cantview-deleted-comment', 'permissiondenied' );
137        }
138        if ( $this->fetchContent && !$this->getAuthority()->isAllowedAny( 'deletedtext', 'undelete' ) ) {
139            $this->dieWithError( 'apierror-cantview-deleted-revision-content', 'permissiondenied' );
140        }
141
142        $dir = $params['dir'];
143
144        if ( $revCount !== 0 ) {
145            $this->addWhere( [
146                'ar_rev_id' => array_keys( $pageSet->getDeletedRevisionIDs() )
147            ] );
148        } else {
149            // We need a custom WHERE clause that matches all titles.
150            $lb = $this->linkBatchFactory->newLinkBatch( $pageSet->getGoodAndMissingPages() );
151            $where = $lb->constructSet( 'ar', $db );
152            $this->addWhere( $where );
153        }
154
155        if ( $params['user'] !== null || $params['excludeuser'] !== null ) {
156            // In the non-generator case, the actor join will already be present.
157            if ( $resultPageSet !== null ) {
158                $this->addTables( 'actor' );
159                $this->addJoinConds( [ 'actor' => [ 'JOIN', 'actor_id=ar_actor' ] ] );
160            }
161            if ( $params['user'] !== null ) {
162                $this->addWhereFld( 'actor_name', $params['user'] );
163            } elseif ( $params['excludeuser'] !== null ) {
164                $this->addWhere( $db->expr( 'actor_name', '!=', $params['excludeuser'] ) );
165            }
166        }
167
168        if ( $params['user'] !== null || $params['excludeuser'] !== null ) {
169            // Paranoia: avoid brute force searches (T19342)
170            if ( !$this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
171                $bitmask = RevisionRecord::DELETED_USER;
172            } elseif ( !$this->getAuthority()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
173                $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
174            } else {
175                $bitmask = 0;
176            }
177            if ( $bitmask ) {
178                $this->addWhere( $db->bitAnd( 'ar_deleted', $bitmask ) . " != $bitmask" );
179            }
180        }
181
182        if ( $params['continue'] !== null ) {
183            $op = ( $dir == 'newer' ? '>=' : '<=' );
184            if ( $revCount !== 0 ) {
185                $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'int', 'int' ] );
186                $this->addWhere( $db->buildComparison( $op, [
187                    'ar_rev_id' => $cont[0],
188                    'ar_id' => $cont[1],
189                ] ) );
190            } else {
191                $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'int', 'string', 'timestamp', 'int' ] );
192                $this->addWhere( $db->buildComparison( $op, [
193                    'ar_namespace' => $cont[0],
194                    'ar_title' => $cont[1],
195                    'ar_timestamp' => $db->timestamp( $cont[2] ),
196                    'ar_id' => $cont[3],
197                ] ) );
198            }
199        }
200
201        $this->addOption( 'LIMIT', $this->limit + 1 );
202
203        if ( $revCount !== 0 ) {
204            // Sort by ar_rev_id when querying by ar_rev_id
205            $this->addWhereRange( 'ar_rev_id', $dir, null, null );
206        } else {
207            // Sort by ns and title in the same order as timestamp for efficiency
208            // But only when not already unique in the query
209            if ( count( $pageMap ) > 1 ) {
210                $this->addWhereRange( 'ar_namespace', $dir, null, null );
211            }
212            $oneTitle = key( reset( $pageMap ) );
213            foreach ( $pageMap as $pages ) {
214                if ( count( $pages ) > 1 || key( $pages ) !== $oneTitle ) {
215                    $this->addWhereRange( 'ar_title', $dir, null, null );
216                    break;
217                }
218            }
219            $this->addTimestampWhereRange( 'ar_timestamp', $dir, $params['start'], $params['end'] );
220        }
221        // Include in ORDER BY for uniqueness
222        $this->addWhereRange( 'ar_id', $dir, null, null );
223
224        $res = $this->select( __METHOD__ );
225        $count = 0;
226        $generated = [];
227        foreach ( $res as $row ) {
228            if ( ++$count > $this->limit ) {
229                // We've had enough
230                $this->setContinueEnumParameter( 'continue',
231                    $revCount
232                        ? "$row->ar_rev_id|$row->ar_id"
233                        : "$row->ar_namespace|$row->ar_title|$row->ar_timestamp|$row->ar_id"
234                );
235                break;
236            }
237
238            if ( $resultPageSet !== null ) {
239                $generated[] = $row->ar_rev_id;
240            } else {
241                if ( !isset( $pageMap[$row->ar_namespace][$row->ar_title] ) ) {
242                    // Was it converted?
243                    $title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
244                    $converted = $pageSet->getConvertedTitles();
245                    if ( $title && isset( $converted[$title->getPrefixedText()] ) ) {
246                        $title = Title::newFromText( $converted[$title->getPrefixedText()] );
247                        if ( $title && isset( $pageMap[$title->getNamespace()][$title->getDBkey()] ) ) {
248                            $pageMap[$row->ar_namespace][$row->ar_title] =
249                                $pageMap[$title->getNamespace()][$title->getDBkey()];
250                        }
251                    }
252                }
253                if ( !isset( $pageMap[$row->ar_namespace][$row->ar_title] ) ) {
254                    ApiBase::dieDebug(
255                        __METHOD__,
256                        "Found row in archive (ar_id={$row->ar_id}) that didn't get processed by ApiPageSet"
257                    );
258                }
259
260                $fit = $this->addPageSubItem(
261                    $pageMap[$row->ar_namespace][$row->ar_title],
262                    $this->extractRevisionInfo( $this->revisionStore->newRevisionFromArchiveRow( $row ), $row ),
263                    'rev'
264                );
265                if ( !$fit ) {
266                    $this->setContinueEnumParameter( 'continue',
267                        $revCount
268                            ? "$row->ar_rev_id|$row->ar_id"
269                            : "$row->ar_namespace|$row->ar_title|$row->ar_timestamp|$row->ar_id"
270                    );
271                    break;
272                }
273            }
274        }
275
276        if ( $resultPageSet !== null ) {
277            $resultPageSet->populateFromRevisionIDs( $generated );
278        }
279    }
280
281    /** @inheritDoc */
282    public function getAllowedParams() {
283        return parent::getAllowedParams() + [
284            'start' => [
285                ParamValidator::PARAM_TYPE => 'timestamp',
286            ],
287            'end' => [
288                ParamValidator::PARAM_TYPE => 'timestamp',
289            ],
290            'dir' => [
291                ParamValidator::PARAM_TYPE => [
292                    'newer',
293                    'older'
294                ],
295                ParamValidator::PARAM_DEFAULT => 'older',
296                ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
297                ApiBase::PARAM_HELP_MSG_PER_VALUE => [
298                    'newer' => 'api-help-paramvalue-direction-newer',
299                    'older' => 'api-help-paramvalue-direction-older',
300                ],
301            ],
302            'tag' => null,
303            'user' => [
304                ParamValidator::PARAM_TYPE => 'user',
305                UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'id', 'interwiki' ],
306            ],
307            'excludeuser' => [
308                ParamValidator::PARAM_TYPE => 'user',
309                UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'id', 'interwiki' ],
310            ],
311            'continue' => [
312                ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
313            ],
314        ];
315    }
316
317    /** @inheritDoc */
318    protected function getExamplesMessages() {
319        $title = Title::newMainPage();
320        $talkTitle = $title->getTalkPageIfDefined();
321        $examples = [
322            'action=query&prop=deletedrevisions&revids=123456'
323                => 'apihelp-query+deletedrevisions-example-revids',
324        ];
325
326        if ( $talkTitle ) {
327            $title = rawurlencode( $title->getPrefixedText() );
328            $talkTitle = rawurlencode( $talkTitle->getPrefixedText() );
329            $examples["action=query&prop=deletedrevisions&titles={$title}|{$talkTitle}&" .
330                'drvslots=*&drvprop=user|comment|content'] = 'apihelp-query+deletedrevisions-example-titles';
331        }
332
333        return $examples;
334    }
335
336    /** @inheritDoc */
337    public function getHelpUrls() {
338        return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Deletedrevisions';
339    }
340}
341
342/** @deprecated class alias since 1.43 */
343class_alias( ApiQueryDeletedRevisions::class, 'ApiQueryDeletedRevisions' );