ApiQueryAllRevisions.php 10 KB

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