]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/api/ApiQueryDeletedRevisions.php
MediaWiki 1.30.2-scripts2
[autoinstalls/mediawiki.git] / includes / api / ApiQueryDeletedRevisions.php
1 <?php
2 /**
3  * Created on Oct 3, 2014
4  *
5  * Copyright © 2014 Wikimedia Foundation and contributors
6  *
7  * Heavily based on ApiQueryDeletedrevs,
8  * Copyright © 2007 Roan Kattouw "<Firstname>.<Lastname>@gmail.com"
9  *
10  * This program is free software; you can redistribute it and/or modify
11  * it under the terms of the GNU General Public License as published by
12  * the Free Software Foundation; either version 2 of the License, or
13  * (at your option) any later version.
14  *
15  * This program is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18  * GNU General Public License for more details.
19  *
20  * You should have received a copy of the GNU General Public License along
21  * with this program; if not, write to the Free Software Foundation, Inc.,
22  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
23  * http://www.gnu.org/copyleft/gpl.html
24  *
25  * @file
26  */
27
28 /**
29  * Query module to enumerate deleted revisions for pages.
30  *
31  * @ingroup API
32  */
33 class ApiQueryDeletedRevisions extends ApiQueryRevisionsBase {
34
35         public function __construct( ApiQuery $query, $moduleName ) {
36                 parent::__construct( $query, $moduleName, 'drv' );
37         }
38
39         protected function run( ApiPageSet $resultPageSet = null ) {
40                 $user = $this->getUser();
41                 // Before doing anything at all, let's check permissions
42                 $this->checkUserRightsAny( 'deletedhistory' );
43
44                 $pageSet = $this->getPageSet();
45                 $pageMap = $pageSet->getGoodAndMissingTitlesByNamespace();
46                 $pageCount = count( $pageSet->getGoodAndMissingTitles() );
47                 $revCount = $pageSet->getRevisionCount();
48                 if ( $revCount === 0 && $pageCount === 0 ) {
49                         // Nothing to do
50                         return;
51                 }
52                 if ( $revCount !== 0 && count( $pageSet->getDeletedRevisionIDs() ) === 0 ) {
53                         // Nothing to do, revisions were supplied but none are deleted
54                         return;
55                 }
56
57                 $params = $this->extractRequestParams( false );
58
59                 $db = $this->getDB();
60
61                 $this->requireMaxOneParameter( $params, 'user', 'excludeuser' );
62
63                 $this->addTables( 'archive' );
64                 if ( $resultPageSet === null ) {
65                         $this->parseParameters( $params );
66                         $this->addFields( Revision::selectArchiveFields() );
67                         $this->addFields( [ 'ar_title', 'ar_namespace' ] );
68                 } else {
69                         $this->limit = $this->getParameter( 'limit' ) ?: 10;
70                         $this->addFields( [ 'ar_title', 'ar_namespace', 'ar_timestamp', 'ar_rev_id', 'ar_id' ] );
71                 }
72
73                 if ( $this->fld_tags ) {
74                         $this->addTables( 'tag_summary' );
75                         $this->addJoinConds(
76                                 [ 'tag_summary' => [ 'LEFT JOIN', [ 'ar_rev_id=ts_rev_id' ] ] ]
77                         );
78                         $this->addFields( 'ts_tags' );
79                 }
80
81                 if ( !is_null( $params['tag'] ) ) {
82                         $this->addTables( 'change_tag' );
83                         $this->addJoinConds(
84                                 [ 'change_tag' => [ 'INNER JOIN', [ 'ar_rev_id=ct_rev_id' ] ] ]
85                         );
86                         $this->addWhereFld( 'ct_tag', $params['tag'] );
87                 }
88
89                 if ( $this->fetchContent ) {
90                         // Modern MediaWiki has the content for deleted revs in the 'text'
91                         // table using fields old_text and old_flags. But revisions deleted
92                         // pre-1.5 store the content in the 'archive' table directly using
93                         // fields ar_text and ar_flags, and no corresponding 'text' row. So
94                         // we have to LEFT JOIN and fetch all four fields.
95                         $this->addTables( 'text' );
96                         $this->addJoinConds(
97                                 [ 'text' => [ 'LEFT JOIN', [ 'ar_text_id=old_id' ] ] ]
98                         );
99                         $this->addFields( [ 'ar_text', 'ar_flags', 'old_text', 'old_flags' ] );
100
101                         // This also means stricter restrictions
102                         $this->checkUserRightsAny( [ 'deletedtext', 'undelete' ] );
103                 }
104
105                 $dir = $params['dir'];
106
107                 if ( $revCount !== 0 ) {
108                         $this->addWhere( [
109                                 'ar_rev_id' => array_keys( $pageSet->getDeletedRevisionIDs() )
110                         ] );
111                 } else {
112                         // We need a custom WHERE clause that matches all titles.
113                         $lb = new LinkBatch( $pageSet->getGoodAndMissingTitles() );
114                         $where = $lb->constructSet( 'ar', $db );
115                         $this->addWhere( $where );
116                 }
117
118                 if ( !is_null( $params['user'] ) ) {
119                         $this->addWhereFld( 'ar_user_text', $params['user'] );
120                 } elseif ( !is_null( $params['excludeuser'] ) ) {
121                         $this->addWhere( 'ar_user_text != ' .
122                                 $db->addQuotes( $params['excludeuser'] ) );
123                 }
124
125                 if ( !is_null( $params['user'] ) || !is_null( $params['excludeuser'] ) ) {
126                         // Paranoia: avoid brute force searches (T19342)
127                         // (shouldn't be able to get here without 'deletedhistory', but
128                         // check it again just in case)
129                         if ( !$user->isAllowed( 'deletedhistory' ) ) {
130                                 $bitmask = Revision::DELETED_USER;
131                         } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
132                                 $bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED;
133                         } else {
134                                 $bitmask = 0;
135                         }
136                         if ( $bitmask ) {
137                                 $this->addWhere( $db->bitAnd( 'ar_deleted', $bitmask ) . " != $bitmask" );
138                         }
139                 }
140
141                 if ( !is_null( $params['continue'] ) ) {
142                         $cont = explode( '|', $params['continue'] );
143                         $op = ( $dir == 'newer' ? '>' : '<' );
144                         if ( $revCount !== 0 ) {
145                                 $this->dieContinueUsageIf( count( $cont ) != 2 );
146                                 $rev = intval( $cont[0] );
147                                 $this->dieContinueUsageIf( strval( $rev ) !== $cont[0] );
148                                 $ar_id = (int)$cont[1];
149                                 $this->dieContinueUsageIf( strval( $ar_id ) !== $cont[1] );
150                                 $this->addWhere( "ar_rev_id $op $rev OR " .
151                                         "(ar_rev_id = $rev AND " .
152                                         "ar_id $op= $ar_id)" );
153                         } else {
154                                 $this->dieContinueUsageIf( count( $cont ) != 4 );
155                                 $ns = intval( $cont[0] );
156                                 $this->dieContinueUsageIf( strval( $ns ) !== $cont[0] );
157                                 $title = $db->addQuotes( $cont[1] );
158                                 $ts = $db->addQuotes( $db->timestamp( $cont[2] ) );
159                                 $ar_id = (int)$cont[3];
160                                 $this->dieContinueUsageIf( strval( $ar_id ) !== $cont[3] );
161                                 $this->addWhere( "ar_namespace $op $ns OR " .
162                                         "(ar_namespace = $ns AND " .
163                                         "(ar_title $op $title OR " .
164                                         "(ar_title = $title AND " .
165                                         "(ar_timestamp $op $ts OR " .
166                                         "(ar_timestamp = $ts AND " .
167                                         "ar_id $op= $ar_id)))))" );
168                         }
169                 }
170
171                 $this->addOption( 'LIMIT', $this->limit + 1 );
172
173                 if ( $revCount !== 0 ) {
174                         // Sort by ar_rev_id when querying by ar_rev_id
175                         $this->addWhereRange( 'ar_rev_id', $dir, null, null );
176                 } else {
177                         // Sort by ns and title in the same order as timestamp for efficiency
178                         // But only when not already unique in the query
179                         if ( count( $pageMap ) > 1 ) {
180                                 $this->addWhereRange( 'ar_namespace', $dir, null, null );
181                         }
182                         $oneTitle = key( reset( $pageMap ) );
183                         foreach ( $pageMap as $pages ) {
184                                 if ( count( $pages ) > 1 || key( $pages ) !== $oneTitle ) {
185                                         $this->addWhereRange( 'ar_title', $dir, null, null );
186                                         break;
187                                 }
188                         }
189                         $this->addTimestampWhereRange( 'ar_timestamp', $dir, $params['start'], $params['end'] );
190                 }
191                 // Include in ORDER BY for uniqueness
192                 $this->addWhereRange( 'ar_id', $dir, null, null );
193
194                 $res = $this->select( __METHOD__ );
195                 $count = 0;
196                 $generated = [];
197                 foreach ( $res as $row ) {
198                         if ( ++$count > $this->limit ) {
199                                 // We've had enough
200                                 $this->setContinueEnumParameter( 'continue',
201                                         $revCount
202                                                 ? "$row->ar_rev_id|$row->ar_id"
203                                                 : "$row->ar_namespace|$row->ar_title|$row->ar_timestamp|$row->ar_id"
204                                 );
205                                 break;
206                         }
207
208                         if ( $resultPageSet !== null ) {
209                                 $generated[] = $row->ar_rev_id;
210                         } else {
211                                 if ( !isset( $pageMap[$row->ar_namespace][$row->ar_title] ) ) {
212                                         // Was it converted?
213                                         $title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
214                                         $converted = $pageSet->getConvertedTitles();
215                                         if ( $title && isset( $converted[$title->getPrefixedText()] ) ) {
216                                                 $title = Title::newFromText( $converted[$title->getPrefixedText()] );
217                                                 if ( $title && isset( $pageMap[$title->getNamespace()][$title->getDBkey()] ) ) {
218                                                         $pageMap[$row->ar_namespace][$row->ar_title] =
219                                                                 $pageMap[$title->getNamespace()][$title->getDBkey()];
220                                                 }
221                                         }
222                                 }
223                                 if ( !isset( $pageMap[$row->ar_namespace][$row->ar_title] ) ) {
224                                         ApiBase::dieDebug(
225                                                 __METHOD__,
226                                                 "Found row in archive (ar_id={$row->ar_id}) that didn't get processed by ApiPageSet"
227                                         );
228                                 }
229
230                                 $fit = $this->addPageSubItem(
231                                         $pageMap[$row->ar_namespace][$row->ar_title],
232                                         $this->extractRevisionInfo( Revision::newFromArchiveRow( $row ), $row ),
233                                         'rev'
234                                 );
235                                 if ( !$fit ) {
236                                         $this->setContinueEnumParameter( 'continue',
237                                                 $revCount
238                                                         ? "$row->ar_rev_id|$row->ar_id"
239                                                         : "$row->ar_namespace|$row->ar_title|$row->ar_timestamp|$row->ar_id"
240                                         );
241                                         break;
242                                 }
243                         }
244                 }
245
246                 if ( $resultPageSet !== null ) {
247                         $resultPageSet->populateFromRevisionIDs( $generated );
248                 }
249         }
250
251         public function getAllowedParams() {
252                 return parent::getAllowedParams() + [
253                         'start' => [
254                                 ApiBase::PARAM_TYPE => 'timestamp',
255                         ],
256                         'end' => [
257                                 ApiBase::PARAM_TYPE => 'timestamp',
258                         ],
259                         'dir' => [
260                                 ApiBase::PARAM_TYPE => [
261                                         'newer',
262                                         'older'
263                                 ],
264                                 ApiBase::PARAM_DFLT => 'older',
265                                 ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
266                         ],
267                         'tag' => null,
268                         'user' => [
269                                 ApiBase::PARAM_TYPE => 'user'
270                         ],
271                         'excludeuser' => [
272                                 ApiBase::PARAM_TYPE => 'user'
273                         ],
274                         'continue' => [
275                                 ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
276                         ],
277                 ];
278         }
279
280         protected function getExamplesMessages() {
281                 return [
282                         'action=query&prop=deletedrevisions&titles=Main%20Page|Talk:Main%20Page&' .
283                                 'drvprop=user|comment|content'
284                                 => 'apihelp-query+deletedrevisions-example-titles',
285                         'action=query&prop=deletedrevisions&revids=123456'
286                                 => 'apihelp-query+deletedrevisions-example-revids',
287                 ];
288         }
289
290         public function getHelpUrls() {
291                 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Deletedrevisions';
292         }
293 }