ApiQueryUserContribs.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672
  1. <?php
  2. /**
  3. * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
  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. use MediaWiki\Storage\NameTableAccessException;
  25. /**
  26. * This query action adds a list of a specified user's contributions to the output.
  27. *
  28. * @ingroup API
  29. */
  30. class ApiQueryUserContribs extends ApiQueryBase {
  31. public function __construct( ApiQuery $query, $moduleName ) {
  32. parent::__construct( $query, $moduleName, 'uc' );
  33. }
  34. private $params, $multiUserMode, $orderBy, $parentLens, $commentStore;
  35. private $fld_ids = false, $fld_title = false, $fld_timestamp = false,
  36. $fld_comment = false, $fld_parsedcomment = false, $fld_flags = false,
  37. $fld_patrolled = false, $fld_tags = false, $fld_size = false, $fld_sizediff = false;
  38. public function execute() {
  39. // Parse some parameters
  40. $this->params = $this->extractRequestParams();
  41. $this->commentStore = CommentStore::getStore();
  42. $prop = array_flip( $this->params['prop'] );
  43. $this->fld_ids = isset( $prop['ids'] );
  44. $this->fld_title = isset( $prop['title'] );
  45. $this->fld_comment = isset( $prop['comment'] );
  46. $this->fld_parsedcomment = isset( $prop['parsedcomment'] );
  47. $this->fld_size = isset( $prop['size'] );
  48. $this->fld_sizediff = isset( $prop['sizediff'] );
  49. $this->fld_flags = isset( $prop['flags'] );
  50. $this->fld_timestamp = isset( $prop['timestamp'] );
  51. $this->fld_patrolled = isset( $prop['patrolled'] );
  52. $this->fld_tags = isset( $prop['tags'] );
  53. // The main query may use the 'contributions' group DB, which can map to replica DBs
  54. // with extra user based indexes or partioning by user. The additional metadata
  55. // queries should use a regular replica DB since the lookup pattern is not all by user.
  56. $dbSecondary = $this->getDB(); // any random replica DB
  57. $sort = ( $this->params['dir'] == 'newer' ? '' : ' DESC' );
  58. $op = ( $this->params['dir'] == 'older' ? '<' : '>' );
  59. // Create an Iterator that produces the UserIdentity objects we need, depending
  60. // on which of the 'userprefix', 'userids', or 'user' params was
  61. // specified.
  62. $this->requireOnlyOneParameter( $this->params, 'userprefix', 'userids', 'user' );
  63. if ( isset( $this->params['userprefix'] ) ) {
  64. $this->multiUserMode = true;
  65. $this->orderBy = 'name';
  66. $fname = __METHOD__;
  67. // Because 'userprefix' might produce a huge number of users (e.g.
  68. // a wiki with users "Test00000001" to "Test99999999"), use a
  69. // generator with batched lookup and continuation.
  70. $userIter = call_user_func( function () use ( $dbSecondary, $sort, $op, $fname ) {
  71. $fromName = false;
  72. if ( !is_null( $this->params['continue'] ) ) {
  73. $continue = explode( '|', $this->params['continue'] );
  74. $this->dieContinueUsageIf( count( $continue ) != 4 );
  75. $this->dieContinueUsageIf( $continue[0] !== 'name' );
  76. $fromName = $continue[1];
  77. }
  78. $like = $dbSecondary->buildLike( $this->params['userprefix'], $dbSecondary->anyString() );
  79. $limit = 501;
  80. do {
  81. $from = $fromName ? "$op= " . $dbSecondary->addQuotes( $fromName ) : false;
  82. $res = $dbSecondary->select(
  83. 'actor',
  84. [ 'actor_id', 'user_id' => 'COALESCE(actor_user,0)', 'user_name' => 'actor_name' ],
  85. array_merge( [ "actor_name$like" ], $from ? [ "actor_name $from" ] : [] ),
  86. $fname,
  87. [ 'ORDER BY' => [ "user_name $sort" ], 'LIMIT' => $limit ]
  88. );
  89. $count = 0;
  90. $fromName = false;
  91. foreach ( $res as $row ) {
  92. if ( ++$count >= $limit ) {
  93. $fromName = $row->user_name;
  94. break;
  95. }
  96. yield User::newFromRow( $row );
  97. }
  98. } while ( $fromName !== false );
  99. } );
  100. // Do the actual sorting client-side, because otherwise
  101. // prepareQuery might try to sort by actor and confuse everything.
  102. $batchSize = 1;
  103. } elseif ( isset( $this->params['userids'] ) ) {
  104. if ( $this->params['userids'] === [] ) {
  105. $encParamName = $this->encodeParamName( 'userids' );
  106. $this->dieWithError( [ 'apierror-paramempty', $encParamName ], "paramempty_$encParamName" );
  107. }
  108. $ids = [];
  109. foreach ( $this->params['userids'] as $uid ) {
  110. if ( $uid <= 0 ) {
  111. $this->dieWithError( [ 'apierror-invaliduserid', $uid ], 'invaliduserid' );
  112. }
  113. $ids[] = $uid;
  114. }
  115. $this->orderBy = 'id';
  116. $this->multiUserMode = count( $ids ) > 1;
  117. $from = $fromId = false;
  118. if ( $this->multiUserMode && !is_null( $this->params['continue'] ) ) {
  119. $continue = explode( '|', $this->params['continue'] );
  120. $this->dieContinueUsageIf( count( $continue ) != 4 );
  121. $this->dieContinueUsageIf( $continue[0] !== 'id' && $continue[0] !== 'actor' );
  122. $fromId = (int)$continue[1];
  123. $this->dieContinueUsageIf( $continue[1] !== (string)$fromId );
  124. $from = "$op= $fromId";
  125. }
  126. $res = $dbSecondary->select(
  127. 'actor',
  128. [ 'actor_id', 'user_id' => 'actor_user', 'user_name' => 'actor_name' ],
  129. array_merge( [ 'actor_user' => $ids ], $from ? [ "actor_id $from" ] : [] ),
  130. __METHOD__,
  131. [ 'ORDER BY' => "user_id $sort" ]
  132. );
  133. $userIter = UserArray::newFromResult( $res );
  134. $batchSize = count( $ids );
  135. } else {
  136. $names = [];
  137. if ( !count( $this->params['user'] ) ) {
  138. $encParamName = $this->encodeParamName( 'user' );
  139. $this->dieWithError(
  140. [ 'apierror-paramempty', $encParamName ], "paramempty_$encParamName"
  141. );
  142. }
  143. foreach ( $this->params['user'] as $u ) {
  144. if ( $u === '' ) {
  145. $encParamName = $this->encodeParamName( 'user' );
  146. $this->dieWithError(
  147. [ 'apierror-paramempty', $encParamName ], "paramempty_$encParamName"
  148. );
  149. }
  150. if ( User::isIP( $u ) || ExternalUserNames::isExternal( $u ) ) {
  151. $names[$u] = null;
  152. } else {
  153. $name = User::getCanonicalName( $u, 'valid' );
  154. if ( $name === false ) {
  155. $encParamName = $this->encodeParamName( 'user' );
  156. $this->dieWithError(
  157. [ 'apierror-baduser', $encParamName, wfEscapeWikiText( $u ) ], "baduser_$encParamName"
  158. );
  159. }
  160. $names[$name] = null;
  161. }
  162. }
  163. $this->orderBy = 'name';
  164. $this->multiUserMode = count( $names ) > 1;
  165. $from = $fromName = false;
  166. if ( $this->multiUserMode && !is_null( $this->params['continue'] ) ) {
  167. $continue = explode( '|', $this->params['continue'] );
  168. $this->dieContinueUsageIf( count( $continue ) != 4 );
  169. $this->dieContinueUsageIf( $continue[0] !== 'name' && $continue[0] !== 'actor' );
  170. $fromName = $continue[1];
  171. $from = "$op= " . $dbSecondary->addQuotes( $fromName );
  172. }
  173. $res = $dbSecondary->select(
  174. 'actor',
  175. [ 'actor_id', 'user_id' => 'actor_user', 'user_name' => 'actor_name' ],
  176. array_merge( [ 'actor_name' => array_keys( $names ) ], $from ? [ "actor_id $from" ] : [] ),
  177. __METHOD__,
  178. [ 'ORDER BY' => "actor_name $sort" ]
  179. );
  180. $userIter = UserArray::newFromResult( $res );
  181. $batchSize = count( $names );
  182. }
  183. // The DB query will order by actor so update $this->orderBy to match.
  184. if ( $batchSize > 1 ) {
  185. $this->orderBy = 'actor';
  186. }
  187. $count = 0;
  188. $limit = $this->params['limit'];
  189. $userIter->rewind();
  190. while ( $userIter->valid() ) {
  191. $users = [];
  192. while ( count( $users ) < $batchSize && $userIter->valid() ) {
  193. $users[] = $userIter->current();
  194. $userIter->next();
  195. }
  196. $hookData = [];
  197. $this->prepareQuery( $users, $limit - $count );
  198. $res = $this->select( __METHOD__, [], $hookData );
  199. if ( $this->fld_sizediff ) {
  200. $revIds = [];
  201. foreach ( $res as $row ) {
  202. if ( $row->rev_parent_id ) {
  203. $revIds[] = $row->rev_parent_id;
  204. }
  205. }
  206. $this->parentLens = MediaWikiServices::getInstance()->getRevisionStore()
  207. ->listRevisionSizes( $dbSecondary, $revIds );
  208. }
  209. foreach ( $res as $row ) {
  210. if ( ++$count > $limit ) {
  211. // We've reached the one extra which shows that there are
  212. // additional pages to be had. Stop here...
  213. $this->setContinueEnumParameter( 'continue', $this->continueStr( $row ) );
  214. break 2;
  215. }
  216. $vals = $this->extractRowInfo( $row );
  217. $fit = $this->processRow( $row, $vals, $hookData ) &&
  218. $this->getResult()->addValue( [ 'query', $this->getModuleName() ], null, $vals );
  219. if ( !$fit ) {
  220. $this->setContinueEnumParameter( 'continue', $this->continueStr( $row ) );
  221. break 2;
  222. }
  223. }
  224. }
  225. $this->getResult()->addIndexedTagName( [ 'query', $this->getModuleName() ], 'item' );
  226. }
  227. /**
  228. * Prepares the query and returns the limit of rows requested
  229. * @param User[] $users
  230. * @param int $limit
  231. */
  232. private function prepareQuery( array $users, $limit ) {
  233. $this->resetQueryParams();
  234. $db = $this->getDB();
  235. $revQuery = MediaWikiServices::getInstance()->getRevisionStore()->getQueryInfo( [ 'page' ] );
  236. $revWhere = ActorMigration::newMigration()->getWhere( $db, 'rev_user', $users );
  237. $orderUserField = 'rev_actor';
  238. $userField = $this->orderBy === 'actor' ? 'revactor_actor' : 'actor_name';
  239. $tsField = 'revactor_timestamp';
  240. $idField = 'revactor_rev';
  241. // T221511: MySQL/MariaDB (10.1.37) can sometimes irrationally decide that querying `actor`
  242. // before `revision_actor_temp` and filesorting is somehow better than querying $limit+1 rows
  243. // from `revision_actor_temp`. Tell it not to reorder the query (and also reorder it ourselves
  244. // because as generated by RevisionStore it'll have `revision` first rather than
  245. // `revision_actor_temp`). But not when uctag is used, as it seems as likely to be harmed as
  246. // helped in that case, and not when there's only one User because in that case it fetches
  247. // the one `actor` row as a constant and doesn't filesort.
  248. if ( count( $users ) > 1 && !isset( $this->params['tag'] ) ) {
  249. $revQuery['joins']['revision'] = $revQuery['joins']['temp_rev_user'];
  250. unset( $revQuery['joins']['temp_rev_user'] );
  251. $this->addOption( 'STRAIGHT_JOIN' );
  252. // It isn't actually necesssary to reorder $revQuery['tables'] as Database does the right thing
  253. // when join conditions are given for all joins, but Gergő is wary of relying on that so pull
  254. // `revision_actor_temp` to the start.
  255. $revQuery['tables'] =
  256. [ 'temp_rev_user' => $revQuery['tables']['temp_rev_user'] ] + $revQuery['tables'];
  257. }
  258. $this->addTables( $revQuery['tables'] );
  259. $this->addJoinConds( $revQuery['joins'] );
  260. $this->addFields( $revQuery['fields'] );
  261. $this->addWhere( $revWhere['conds'] );
  262. // Handle continue parameter
  263. if ( !is_null( $this->params['continue'] ) ) {
  264. $continue = explode( '|', $this->params['continue'] );
  265. if ( $this->multiUserMode ) {
  266. $this->dieContinueUsageIf( count( $continue ) != 4 );
  267. $modeFlag = array_shift( $continue );
  268. $this->dieContinueUsageIf( $modeFlag !== $this->orderBy );
  269. $encUser = $db->addQuotes( array_shift( $continue ) );
  270. } else {
  271. $this->dieContinueUsageIf( count( $continue ) != 2 );
  272. }
  273. $encTS = $db->addQuotes( $db->timestamp( $continue[0] ) );
  274. $encId = (int)$continue[1];
  275. $this->dieContinueUsageIf( $encId != $continue[1] );
  276. $op = ( $this->params['dir'] == 'older' ? '<' : '>' );
  277. if ( $this->multiUserMode ) {
  278. $this->addWhere(
  279. "$userField $op $encUser OR " .
  280. "($userField = $encUser AND " .
  281. "($tsField $op $encTS OR " .
  282. "($tsField = $encTS AND " .
  283. "$idField $op= $encId)))"
  284. );
  285. } else {
  286. $this->addWhere(
  287. "$tsField $op $encTS OR " .
  288. "($tsField = $encTS AND " .
  289. "$idField $op= $encId)"
  290. );
  291. }
  292. }
  293. // Don't include any revisions where we're not supposed to be able to
  294. // see the username.
  295. $user = $this->getUser();
  296. if ( !$this->getPermissionManager()->userHasRight( $user, 'deletedhistory' ) ) {
  297. $bitmask = RevisionRecord::DELETED_USER;
  298. } elseif ( !$this->getPermissionManager()
  299. ->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' )
  300. ) {
  301. $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
  302. } else {
  303. $bitmask = 0;
  304. }
  305. if ( $bitmask ) {
  306. $this->addWhere( $db->bitAnd( 'rev_deleted', $bitmask ) . " != $bitmask" );
  307. }
  308. // Add the user field to ORDER BY if there are multiple users
  309. if ( count( $users ) > 1 ) {
  310. $this->addWhereRange( $orderUserField, $this->params['dir'], null, null );
  311. }
  312. // Then timestamp
  313. $this->addTimestampWhereRange( $tsField,
  314. $this->params['dir'], $this->params['start'], $this->params['end'] );
  315. // Then rev_id for a total ordering
  316. $this->addWhereRange( $idField, $this->params['dir'], null, null );
  317. $this->addWhereFld( 'page_namespace', $this->params['namespace'] );
  318. $show = $this->params['show'];
  319. if ( $this->params['toponly'] ) { // deprecated/old param
  320. $show[] = 'top';
  321. }
  322. if ( !is_null( $show ) ) {
  323. $show = array_flip( $show );
  324. if ( ( isset( $show['minor'] ) && isset( $show['!minor'] ) )
  325. || ( isset( $show['patrolled'] ) && isset( $show['!patrolled'] ) )
  326. || ( isset( $show['autopatrolled'] ) && isset( $show['!autopatrolled'] ) )
  327. || ( isset( $show['autopatrolled'] ) && isset( $show['!patrolled'] ) )
  328. || ( isset( $show['top'] ) && isset( $show['!top'] ) )
  329. || ( isset( $show['new'] ) && isset( $show['!new'] ) )
  330. ) {
  331. $this->dieWithError( 'apierror-show' );
  332. }
  333. $this->addWhereIf( 'rev_minor_edit = 0', isset( $show['!minor'] ) );
  334. $this->addWhereIf( 'rev_minor_edit != 0', isset( $show['minor'] ) );
  335. $this->addWhereIf(
  336. 'rc_patrolled = ' . RecentChange::PRC_UNPATROLLED,
  337. isset( $show['!patrolled'] )
  338. );
  339. $this->addWhereIf(
  340. 'rc_patrolled != ' . RecentChange::PRC_UNPATROLLED,
  341. isset( $show['patrolled'] )
  342. );
  343. $this->addWhereIf(
  344. 'rc_patrolled != ' . RecentChange::PRC_AUTOPATROLLED,
  345. isset( $show['!autopatrolled'] )
  346. );
  347. $this->addWhereIf(
  348. 'rc_patrolled = ' . RecentChange::PRC_AUTOPATROLLED,
  349. isset( $show['autopatrolled'] )
  350. );
  351. $this->addWhereIf( $idField . ' != page_latest', isset( $show['!top'] ) );
  352. $this->addWhereIf( $idField . ' = page_latest', isset( $show['top'] ) );
  353. $this->addWhereIf( 'rev_parent_id != 0', isset( $show['!new'] ) );
  354. $this->addWhereIf( 'rev_parent_id = 0', isset( $show['new'] ) );
  355. }
  356. $this->addOption( 'LIMIT', $limit + 1 );
  357. if ( isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) ||
  358. isset( $show['autopatrolled'] ) || isset( $show['!autopatrolled'] ) || $this->fld_patrolled
  359. ) {
  360. if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) {
  361. $this->dieWithError( 'apierror-permissiondenied-patrolflag', 'permissiondenied' );
  362. }
  363. $isFilterset = isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) ||
  364. isset( $show['autopatrolled'] ) || isset( $show['!autopatrolled'] );
  365. $this->addTables( 'recentchanges' );
  366. $this->addJoinConds( [ 'recentchanges' => [
  367. $isFilterset ? 'JOIN' : 'LEFT JOIN',
  368. [
  369. // This is a crazy hack. recentchanges has no index on rc_this_oldid, so instead of adding
  370. // one T19237 did a join using rc_user_text and rc_timestamp instead. Now rc_user_text is
  371. // probably unavailable, so just do rc_timestamp.
  372. 'rc_timestamp = ' . $tsField,
  373. 'rc_this_oldid = ' . $idField,
  374. ]
  375. ] ] );
  376. }
  377. $this->addFieldsIf( 'rc_patrolled', $this->fld_patrolled );
  378. if ( $this->fld_tags ) {
  379. $this->addFields( [ 'ts_tags' => ChangeTags::makeTagSummarySubquery( 'revision' ) ] );
  380. }
  381. if ( isset( $this->params['tag'] ) ) {
  382. $this->addTables( 'change_tag' );
  383. $this->addJoinConds(
  384. [ 'change_tag' => [ 'JOIN', [ $idField . ' = ct_rev_id' ] ] ]
  385. );
  386. $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
  387. try {
  388. $this->addWhereFld( 'ct_tag_id', $changeTagDefStore->getId( $this->params['tag'] ) );
  389. } catch ( NameTableAccessException $exception ) {
  390. // Return nothing.
  391. $this->addWhere( '1=0' );
  392. }
  393. }
  394. }
  395. /**
  396. * Extract fields from the database row and append them to a result array
  397. *
  398. * @param stdClass $row
  399. * @return array
  400. */
  401. private function extractRowInfo( $row ) {
  402. $vals = [];
  403. $anyHidden = false;
  404. if ( $row->rev_deleted & RevisionRecord::DELETED_TEXT ) {
  405. $vals['texthidden'] = true;
  406. $anyHidden = true;
  407. }
  408. // Any rows where we can't view the user were filtered out in the query.
  409. $vals['userid'] = (int)$row->rev_user;
  410. $vals['user'] = $row->rev_user_text;
  411. if ( $row->rev_deleted & RevisionRecord::DELETED_USER ) {
  412. $vals['userhidden'] = true;
  413. $anyHidden = true;
  414. }
  415. if ( $this->fld_ids ) {
  416. $vals['pageid'] = (int)$row->rev_page;
  417. $vals['revid'] = (int)$row->rev_id;
  418. if ( !is_null( $row->rev_parent_id ) ) {
  419. $vals['parentid'] = (int)$row->rev_parent_id;
  420. }
  421. }
  422. $title = Title::makeTitle( $row->page_namespace, $row->page_title );
  423. if ( $this->fld_title ) {
  424. ApiQueryBase::addTitleInfo( $vals, $title );
  425. }
  426. if ( $this->fld_timestamp ) {
  427. $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $row->rev_timestamp );
  428. }
  429. if ( $this->fld_flags ) {
  430. $vals['new'] = $row->rev_parent_id == 0 && !is_null( $row->rev_parent_id );
  431. $vals['minor'] = (bool)$row->rev_minor_edit;
  432. $vals['top'] = $row->page_latest == $row->rev_id;
  433. }
  434. if ( $this->fld_comment || $this->fld_parsedcomment ) {
  435. if ( $row->rev_deleted & RevisionRecord::DELETED_COMMENT ) {
  436. $vals['commenthidden'] = true;
  437. $anyHidden = true;
  438. }
  439. $userCanView = RevisionRecord::userCanBitfield(
  440. $row->rev_deleted,
  441. RevisionRecord::DELETED_COMMENT, $this->getUser()
  442. );
  443. if ( $userCanView ) {
  444. $comment = $this->commentStore->getComment( 'rev_comment', $row )->text;
  445. if ( $this->fld_comment ) {
  446. $vals['comment'] = $comment;
  447. }
  448. if ( $this->fld_parsedcomment ) {
  449. $vals['parsedcomment'] = Linker::formatComment( $comment, $title );
  450. }
  451. }
  452. }
  453. if ( $this->fld_patrolled ) {
  454. $vals['patrolled'] = $row->rc_patrolled != RecentChange::PRC_UNPATROLLED;
  455. $vals['autopatrolled'] = $row->rc_patrolled == RecentChange::PRC_AUTOPATROLLED;
  456. }
  457. if ( $this->fld_size && !is_null( $row->rev_len ) ) {
  458. $vals['size'] = (int)$row->rev_len;
  459. }
  460. if ( $this->fld_sizediff
  461. && !is_null( $row->rev_len )
  462. && !is_null( $row->rev_parent_id )
  463. ) {
  464. $parentLen = $this->parentLens[$row->rev_parent_id] ?? 0;
  465. $vals['sizediff'] = (int)$row->rev_len - $parentLen;
  466. }
  467. if ( $this->fld_tags ) {
  468. if ( $row->ts_tags ) {
  469. $tags = explode( ',', $row->ts_tags );
  470. ApiResult::setIndexedTagName( $tags, 'tag' );
  471. $vals['tags'] = $tags;
  472. } else {
  473. $vals['tags'] = [];
  474. }
  475. }
  476. if ( $anyHidden && ( $row->rev_deleted & RevisionRecord::DELETED_RESTRICTED ) ) {
  477. $vals['suppressed'] = true;
  478. }
  479. return $vals;
  480. }
  481. private function continueStr( $row ) {
  482. if ( $this->multiUserMode ) {
  483. switch ( $this->orderBy ) {
  484. case 'id':
  485. return "id|$row->rev_user|$row->rev_timestamp|$row->rev_id";
  486. case 'name':
  487. return "name|$row->rev_user_text|$row->rev_timestamp|$row->rev_id";
  488. case 'actor':
  489. return "actor|$row->rev_actor|$row->rev_timestamp|$row->rev_id";
  490. }
  491. } else {
  492. return "$row->rev_timestamp|$row->rev_id";
  493. }
  494. }
  495. public function getCacheMode( $params ) {
  496. // This module provides access to deleted revisions and patrol flags if
  497. // the requester is logged in
  498. return 'anon-public-user-private';
  499. }
  500. public function getAllowedParams() {
  501. return [
  502. 'limit' => [
  503. ApiBase::PARAM_DFLT => 10,
  504. ApiBase::PARAM_TYPE => 'limit',
  505. ApiBase::PARAM_MIN => 1,
  506. ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1,
  507. ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2
  508. ],
  509. 'start' => [
  510. ApiBase::PARAM_TYPE => 'timestamp'
  511. ],
  512. 'end' => [
  513. ApiBase::PARAM_TYPE => 'timestamp'
  514. ],
  515. 'continue' => [
  516. ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
  517. ],
  518. 'user' => [
  519. ApiBase::PARAM_TYPE => 'user',
  520. ApiBase::PARAM_ISMULTI => true
  521. ],
  522. 'userids' => [
  523. ApiBase::PARAM_TYPE => 'integer',
  524. ApiBase::PARAM_ISMULTI => true
  525. ],
  526. 'userprefix' => null,
  527. 'dir' => [
  528. ApiBase::PARAM_DFLT => 'older',
  529. ApiBase::PARAM_TYPE => [
  530. 'newer',
  531. 'older'
  532. ],
  533. ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
  534. ],
  535. 'namespace' => [
  536. ApiBase::PARAM_ISMULTI => true,
  537. ApiBase::PARAM_TYPE => 'namespace'
  538. ],
  539. 'prop' => [
  540. ApiBase::PARAM_ISMULTI => true,
  541. ApiBase::PARAM_DFLT => 'ids|title|timestamp|comment|size|flags',
  542. ApiBase::PARAM_TYPE => [
  543. 'ids',
  544. 'title',
  545. 'timestamp',
  546. 'comment',
  547. 'parsedcomment',
  548. 'size',
  549. 'sizediff',
  550. 'flags',
  551. 'patrolled',
  552. 'tags'
  553. ],
  554. ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
  555. ],
  556. 'show' => [
  557. ApiBase::PARAM_ISMULTI => true,
  558. ApiBase::PARAM_TYPE => [
  559. 'minor',
  560. '!minor',
  561. 'patrolled',
  562. '!patrolled',
  563. 'autopatrolled',
  564. '!autopatrolled',
  565. 'top',
  566. '!top',
  567. 'new',
  568. '!new',
  569. ],
  570. ApiBase::PARAM_HELP_MSG => [
  571. 'apihelp-query+usercontribs-param-show',
  572. $this->getConfig()->get( 'RCMaxAge' )
  573. ],
  574. ],
  575. 'tag' => null,
  576. 'toponly' => [
  577. ApiBase::PARAM_DFLT => false,
  578. ApiBase::PARAM_DEPRECATED => true,
  579. ],
  580. ];
  581. }
  582. protected function getExamplesMessages() {
  583. return [
  584. 'action=query&list=usercontribs&ucuser=Example'
  585. => 'apihelp-query+usercontribs-example-user',
  586. 'action=query&list=usercontribs&ucuserprefix=192.0.2.'
  587. => 'apihelp-query+usercontribs-example-ipprefix',
  588. ];
  589. }
  590. public function getHelpUrls() {
  591. return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Usercontribs';
  592. }
  593. }
  594. /**
  595. * @since 1.9
  596. * @deprecated since 1.32
  597. */
  598. class_alias( ApiQueryUserContribs::class, 'ApiQueryContributions' );