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