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