1<?php 2/** 3 * Copyright © 2015 Wikimedia Foundation and contributors 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\MediaWikiServices; 24use MediaWiki\ParamValidator\TypeDef\UserDef; 25use MediaWiki\Revision\RevisionRecord; 26 27/** 28 * Query module to enumerate all revisions. 29 * 30 * @ingroup API 31 * @since 1.27 32 */ 33class ApiQueryAllRevisions extends ApiQueryRevisionsBase { 34 35 public function __construct( ApiQuery $query, $moduleName ) { 36 parent::__construct( $query, $moduleName, 'arv' ); 37 } 38 39 /** 40 * @param ApiPageSet|null $resultPageSet 41 * @return void 42 */ 43 protected function run( ApiPageSet $resultPageSet = null ) { 44 $db = $this->getDB(); 45 $params = $this->extractRequestParams( false ); 46 $services = MediaWikiServices::getInstance(); 47 $revisionStore = $services->getRevisionStore(); 48 49 $result = $this->getResult(); 50 51 $this->requireMaxOneParameter( $params, 'user', 'excludeuser' ); 52 53 $tsField = 'rev_timestamp'; 54 $idField = 'rev_id'; 55 $pageField = 'rev_page'; 56 if ( $params['user'] !== null ) { 57 // The query is probably best done using the actor_timestamp index on 58 // revision_actor_temp. Use the denormalized fields from that table. 59 $tsField = 'revactor_timestamp'; 60 $idField = 'revactor_rev'; 61 $pageField = 'revactor_page'; 62 } 63 64 // Namespace check is likely to be desired, but can't be done 65 // efficiently in SQL. 66 $miser_ns = null; 67 $needPageTable = false; 68 if ( $params['namespace'] !== null ) { 69 $params['namespace'] = array_unique( $params['namespace'] ); 70 sort( $params['namespace'] ); 71 if ( $params['namespace'] != $services->getNamespaceInfo()->getValidNamespaces() ) { 72 $needPageTable = true; 73 if ( $this->getConfig()->get( 'MiserMode' ) ) { 74 $miser_ns = $params['namespace']; 75 } else { 76 $this->addWhere( [ 'page_namespace' => $params['namespace'] ] ); 77 } 78 } 79 } 80 81 if ( $resultPageSet === null ) { 82 $this->parseParameters( $params ); 83 $revQuery = $revisionStore->getQueryInfo( [ 'page' ] ); 84 } else { 85 $this->limit = $this->getParameter( 'limit' ) ?: 10; 86 $revQuery = [ 87 'tables' => [ 'revision' ], 88 'fields' => [ 'rev_timestamp', 'rev_id' ], 89 'joins' => [], 90 ]; 91 92 if ( $params['generatetitles'] ) { 93 $revQuery['fields'][] = 'rev_page'; 94 } 95 96 if ( $params['user'] !== null || $params['excludeuser'] !== null ) { 97 $actorQuery = ActorMigration::newMigration()->getJoin( 'rev_user' ); 98 $revQuery['tables'] += $actorQuery['tables']; 99 $revQuery['joins'] += $actorQuery['joins']; 100 } 101 102 if ( $needPageTable ) { 103 $revQuery['tables'][] = 'page'; 104 $revQuery['joins']['page'] = [ 'JOIN', [ "$pageField = page_id" ] ]; 105 if ( (bool)$miser_ns ) { 106 $revQuery['fields'][] = 'page_namespace'; 107 } 108 } 109 } 110 111 // If we're going to be using actor_timestamp, we need to swap the order of `revision` 112 // and `revision_actor_temp` in the query (for the straight join) and adjust some field aliases. 113 if ( $idField !== 'rev_id' && isset( $revQuery['tables']['temp_rev_user'] ) ) { 114 $aliasFields = [ 'rev_id' => $idField, 'rev_timestamp' => $tsField, 'rev_page' => $pageField ]; 115 $revQuery['fields'] = array_merge( 116 $aliasFields, 117 array_diff( $revQuery['fields'], array_keys( $aliasFields ) ) 118 ); 119 unset( $revQuery['tables']['temp_rev_user'] ); 120 $revQuery['tables'] = array_merge( 121 [ 'temp_rev_user' => 'revision_actor_temp' ], 122 $revQuery['tables'] 123 ); 124 $revQuery['joins']['revision'] = $revQuery['joins']['temp_rev_user']; 125 unset( $revQuery['joins']['temp_rev_user'] ); 126 } 127 128 $this->addTables( $revQuery['tables'] ); 129 $this->addFields( $revQuery['fields'] ); 130 $this->addJoinConds( $revQuery['joins'] ); 131 132 // Seems to be needed to avoid a planner bug (T113901) 133 $this->addOption( 'STRAIGHT_JOIN' ); 134 135 $dir = $params['dir']; 136 $this->addTimestampWhereRange( $tsField, $dir, $params['start'], $params['end'] ); 137 138 if ( $this->fld_tags ) { 139 $this->addFields( [ 'ts_tags' => ChangeTags::makeTagSummarySubquery( 'revision' ) ] ); 140 } 141 142 if ( $params['user'] !== null ) { 143 $actorQuery = ActorMigration::newMigration() 144 ->getWhere( $db, 'rev_user', $params['user'] ); 145 $this->addWhere( $actorQuery['conds'] ); 146 } elseif ( $params['excludeuser'] !== null ) { 147 $actorQuery = ActorMigration::newMigration() 148 ->getWhere( $db, 'rev_user', $params['excludeuser'] ); 149 $this->addWhere( 'NOT(' . $actorQuery['conds'] . ')' ); 150 } 151 152 if ( $params['user'] !== null || $params['excludeuser'] !== null ) { 153 // Paranoia: avoid brute force searches (T19342) 154 if ( !$this->getPermissionManager()->userHasRight( $this->getUser(), 'deletedhistory' ) ) { 155 $bitmask = RevisionRecord::DELETED_USER; 156 } elseif ( !$this->getPermissionManager() 157 ->userHasAnyRight( $this->getUser(), 'suppressrevision', 'viewsuppressed' ) 158 ) { 159 $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED; 160 } else { 161 $bitmask = 0; 162 } 163 if ( $bitmask ) { 164 $this->addWhere( $db->bitAnd( 'rev_deleted', $bitmask ) . " != $bitmask" ); 165 } 166 } 167 168 if ( $params['continue'] !== null ) { 169 $op = ( $dir == 'newer' ? '>' : '<' ); 170 $cont = explode( '|', $params['continue'] ); 171 $this->dieContinueUsageIf( count( $cont ) != 2 ); 172 $ts = $db->addQuotes( $db->timestamp( $cont[0] ) ); 173 $rev_id = (int)$cont[1]; 174 $this->dieContinueUsageIf( strval( $rev_id ) !== $cont[1] ); 175 $this->addWhere( "$tsField $op $ts OR " . 176 "($tsField = $ts AND " . 177 "$idField $op= $rev_id)" ); 178 } 179 180 $this->addOption( 'LIMIT', $this->limit + 1 ); 181 182 $sort = ( $dir == 'newer' ? '' : ' DESC' ); 183 $orderby = []; 184 // Targeting index rev_timestamp, user_timestamp, usertext_timestamp, or actor_timestamp. 185 // But 'user' is always constant for the latter three, so it doesn't matter here. 186 $orderby[] = "rev_timestamp $sort"; 187 $orderby[] = "rev_id $sort"; 188 $this->addOption( 'ORDER BY', $orderby ); 189 190 $hookData = []; 191 $res = $this->select( __METHOD__, [], $hookData ); 192 193 if ( $resultPageSet === null ) { 194 $this->executeGenderCacheFromResultWrapper( $res, __METHOD__ ); 195 } 196 197 $pageMap = []; // Maps rev_page to array index 198 $count = 0; 199 $nextIndex = 0; 200 $generated = []; 201 foreach ( $res as $row ) { 202 if ( $count === 0 && $resultPageSet !== null ) { 203 // Set the non-continue since the list of all revisions is 204 // prone to having entries added at the start frequently. 205 $this->getContinuationManager()->addGeneratorNonContinueParam( 206 $this, 'continue', "$row->rev_timestamp|$row->rev_id" 207 ); 208 } 209 if ( ++$count > $this->limit ) { 210 // We've had enough 211 $this->setContinueEnumParameter( 'continue', "$row->rev_timestamp|$row->rev_id" ); 212 break; 213 } 214 215 // Miser mode namespace check 216 if ( $miser_ns !== null && !in_array( $row->page_namespace, $miser_ns ) ) { 217 continue; 218 } 219 220 if ( $resultPageSet !== null ) { 221 if ( $params['generatetitles'] ) { 222 $generated[$row->rev_page] = $row->rev_page; 223 } else { 224 $generated[] = $row->rev_id; 225 } 226 } else { 227 $revision = $revisionStore->newRevisionFromRow( $row, 0, Title::newFromRow( $row ) ); 228 $rev = $this->extractRevisionInfo( $revision, $row ); 229 230 if ( !isset( $pageMap[$row->rev_page] ) ) { 231 $index = $nextIndex++; 232 $pageMap[$row->rev_page] = $index; 233 $title = Title::newFromLinkTarget( $revision->getPageAsLinkTarget() ); 234 $a = [ 235 'pageid' => $title->getArticleID(), 236 'revisions' => [ $rev ], 237 ]; 238 ApiResult::setIndexedTagName( $a['revisions'], 'rev' ); 239 ApiQueryBase::addTitleInfo( $a, $title ); 240 $fit = $this->processRow( $row, $a['revisions'][0], $hookData ) && 241 $result->addValue( [ 'query', $this->getModuleName() ], $index, $a ); 242 } else { 243 $index = $pageMap[$row->rev_page]; 244 $fit = $this->processRow( $row, $rev, $hookData ) && 245 $result->addValue( [ 'query', $this->getModuleName(), $index, 'revisions' ], null, $rev ); 246 } 247 if ( !$fit ) { 248 $this->setContinueEnumParameter( 'continue', "$row->rev_timestamp|$row->rev_id" ); 249 break; 250 } 251 } 252 } 253 254 if ( $resultPageSet !== null ) { 255 if ( $params['generatetitles'] ) { 256 $resultPageSet->populateFromPageIDs( $generated ); 257 } else { 258 $resultPageSet->populateFromRevisionIDs( $generated ); 259 } 260 } else { 261 $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'page' ); 262 } 263 } 264 265 public function getAllowedParams() { 266 $ret = parent::getAllowedParams() + [ 267 'user' => [ 268 ApiBase::PARAM_TYPE => 'user', 269 UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ], 270 UserDef::PARAM_RETURN_OBJECT => true, 271 ], 272 'namespace' => [ 273 ApiBase::PARAM_ISMULTI => true, 274 ApiBase::PARAM_TYPE => 'namespace', 275 ApiBase::PARAM_DFLT => null, 276 ], 277 'start' => [ 278 ApiBase::PARAM_TYPE => 'timestamp', 279 ], 280 'end' => [ 281 ApiBase::PARAM_TYPE => 'timestamp', 282 ], 283 'dir' => [ 284 ApiBase::PARAM_TYPE => [ 285 'newer', 286 'older' 287 ], 288 ApiBase::PARAM_DFLT => 'older', 289 ApiBase::PARAM_HELP_MSG => 'api-help-param-direction', 290 ], 291 'excludeuser' => [ 292 ApiBase::PARAM_TYPE => 'user', 293 UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ], 294 UserDef::PARAM_RETURN_OBJECT => true, 295 ], 296 'continue' => [ 297 ApiBase::PARAM_HELP_MSG => 'api-help-param-continue', 298 ], 299 'generatetitles' => [ 300 ApiBase::PARAM_DFLT => false, 301 ], 302 ]; 303 304 if ( $this->getConfig()->get( 'MiserMode' ) ) { 305 $ret['namespace'][ApiBase::PARAM_HELP_MSG_APPEND] = [ 306 'api-help-param-limited-in-miser-mode', 307 ]; 308 } 309 310 return $ret; 311 } 312 313 protected function getExamplesMessages() { 314 return [ 315 'action=query&list=allrevisions&arvuser=Example&arvlimit=50' 316 => 'apihelp-query+allrevisions-example-user', 317 'action=query&list=allrevisions&arvdir=newer&arvlimit=50' 318 => 'apihelp-query+allrevisions-example-ns-any', 319 ]; 320 } 321 322 public function getHelpUrls() { 323 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Allrevisions'; 324 } 325} 326