ApiQueryRecentChanges.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783
  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. * A query action to enumerate the recent changes that were done to the wiki.
  27. * Various filters are supported.
  28. *
  29. * @ingroup API
  30. */
  31. class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
  32. public function __construct( ApiQuery $query, $moduleName ) {
  33. parent::__construct( $query, $moduleName, 'rc' );
  34. }
  35. private $commentStore;
  36. private $fld_comment = false, $fld_parsedcomment = false, $fld_user = false, $fld_userid = false,
  37. $fld_flags = false, $fld_timestamp = false, $fld_title = false, $fld_ids = false,
  38. $fld_sizes = false, $fld_redirect = false, $fld_patrolled = false, $fld_loginfo = false,
  39. $fld_tags = false, $fld_sha1 = false, $token = [];
  40. private $tokenFunctions;
  41. /**
  42. * Get an array mapping token names to their handler functions.
  43. * The prototype for a token function is func($pageid, $title, $rc)
  44. * it should return a token or false (permission denied)
  45. * @deprecated since 1.24
  46. * @return array [ tokenname => function ]
  47. */
  48. protected function getTokenFunctions() {
  49. // Don't call the hooks twice
  50. if ( isset( $this->tokenFunctions ) ) {
  51. return $this->tokenFunctions;
  52. }
  53. // If we're in a mode that breaks the same-origin policy, no tokens can
  54. // be obtained
  55. if ( $this->lacksSameOriginSecurity() ) {
  56. return [];
  57. }
  58. $this->tokenFunctions = [
  59. 'patrol' => [ self::class, 'getPatrolToken' ]
  60. ];
  61. Hooks::run( 'APIQueryRecentChangesTokens', [ &$this->tokenFunctions ] );
  62. return $this->tokenFunctions;
  63. }
  64. /**
  65. * @deprecated since 1.24
  66. * @param int $pageid
  67. * @param Title $title
  68. * @param RecentChange|null $rc
  69. * @return bool|string
  70. */
  71. public static function getPatrolToken( $pageid, $title, $rc = null ) {
  72. global $wgUser;
  73. $validTokenUser = false;
  74. if ( $rc ) {
  75. if ( ( $wgUser->useRCPatrol() && $rc->getAttribute( 'rc_type' ) == RC_EDIT ) ||
  76. ( $wgUser->useNPPatrol() && $rc->getAttribute( 'rc_type' ) == RC_NEW )
  77. ) {
  78. $validTokenUser = true;
  79. }
  80. } elseif ( $wgUser->useRCPatrol() || $wgUser->useNPPatrol() ) {
  81. $validTokenUser = true;
  82. }
  83. if ( $validTokenUser ) {
  84. // The patrol token is always the same, let's exploit that
  85. static $cachedPatrolToken = null;
  86. if ( is_null( $cachedPatrolToken ) ) {
  87. $cachedPatrolToken = $wgUser->getEditToken( 'patrol' );
  88. }
  89. return $cachedPatrolToken;
  90. }
  91. return false;
  92. }
  93. /**
  94. * Sets internal state to include the desired properties in the output.
  95. * @param array $prop Associative array of properties, only keys are used here
  96. */
  97. public function initProperties( $prop ) {
  98. $this->fld_comment = isset( $prop['comment'] );
  99. $this->fld_parsedcomment = isset( $prop['parsedcomment'] );
  100. $this->fld_user = isset( $prop['user'] );
  101. $this->fld_userid = isset( $prop['userid'] );
  102. $this->fld_flags = isset( $prop['flags'] );
  103. $this->fld_timestamp = isset( $prop['timestamp'] );
  104. $this->fld_title = isset( $prop['title'] );
  105. $this->fld_ids = isset( $prop['ids'] );
  106. $this->fld_sizes = isset( $prop['sizes'] );
  107. $this->fld_redirect = isset( $prop['redirect'] );
  108. $this->fld_patrolled = isset( $prop['patrolled'] );
  109. $this->fld_loginfo = isset( $prop['loginfo'] );
  110. $this->fld_tags = isset( $prop['tags'] );
  111. $this->fld_sha1 = isset( $prop['sha1'] );
  112. }
  113. public function execute() {
  114. $this->run();
  115. }
  116. public function executeGenerator( $resultPageSet ) {
  117. $this->run( $resultPageSet );
  118. }
  119. /**
  120. * Generates and outputs the result of this query based upon the provided parameters.
  121. *
  122. * @param ApiPageSet|null $resultPageSet
  123. */
  124. public function run( $resultPageSet = null ) {
  125. $user = $this->getUser();
  126. /* Get the parameters of the request. */
  127. $params = $this->extractRequestParams();
  128. /* Build our basic query. Namely, something along the lines of:
  129. * SELECT * FROM recentchanges WHERE rc_timestamp > $start
  130. * AND rc_timestamp < $end AND rc_namespace = $namespace
  131. */
  132. $this->addTables( 'recentchanges' );
  133. $this->addTimestampWhereRange( 'rc_timestamp', $params['dir'], $params['start'], $params['end'] );
  134. if ( !is_null( $params['continue'] ) ) {
  135. $cont = explode( '|', $params['continue'] );
  136. $this->dieContinueUsageIf( count( $cont ) != 2 );
  137. $db = $this->getDB();
  138. $timestamp = $db->addQuotes( $db->timestamp( $cont[0] ) );
  139. $id = (int)$cont[1];
  140. $this->dieContinueUsageIf( $id != $cont[1] );
  141. $op = $params['dir'] === 'older' ? '<' : '>';
  142. $this->addWhere(
  143. "rc_timestamp $op $timestamp OR " .
  144. "(rc_timestamp = $timestamp AND " .
  145. "rc_id $op= $id)"
  146. );
  147. }
  148. $order = $params['dir'] === 'older' ? 'DESC' : 'ASC';
  149. $this->addOption( 'ORDER BY', [
  150. "rc_timestamp $order",
  151. "rc_id $order",
  152. ] );
  153. $this->addWhereFld( 'rc_namespace', $params['namespace'] );
  154. if ( !is_null( $params['type'] ) ) {
  155. try {
  156. $this->addWhereFld( 'rc_type', RecentChange::parseToRCType( $params['type'] ) );
  157. } catch ( Exception $e ) {
  158. ApiBase::dieDebug( __METHOD__, $e->getMessage() );
  159. }
  160. }
  161. $title = $params['title'];
  162. if ( !is_null( $title ) ) {
  163. $titleObj = Title::newFromText( $title );
  164. if ( is_null( $titleObj ) ) {
  165. $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $title ) ] );
  166. }
  167. $this->addWhereFld( 'rc_namespace', $titleObj->getNamespace() );
  168. $this->addWhereFld( 'rc_title', $titleObj->getDBkey() );
  169. }
  170. if ( !is_null( $params['show'] ) ) {
  171. $show = array_flip( $params['show'] );
  172. /* Check for conflicting parameters. */
  173. if ( ( isset( $show['minor'] ) && isset( $show['!minor'] ) )
  174. || ( isset( $show['bot'] ) && isset( $show['!bot'] ) )
  175. || ( isset( $show['anon'] ) && isset( $show['!anon'] ) )
  176. || ( isset( $show['redirect'] ) && isset( $show['!redirect'] ) )
  177. || ( isset( $show['patrolled'] ) && isset( $show['!patrolled'] ) )
  178. || ( isset( $show['patrolled'] ) && isset( $show['unpatrolled'] ) )
  179. || ( isset( $show['!patrolled'] ) && isset( $show['unpatrolled'] ) )
  180. || ( isset( $show['autopatrolled'] ) && isset( $show['!autopatrolled'] ) )
  181. || ( isset( $show['autopatrolled'] ) && isset( $show['unpatrolled'] ) )
  182. || ( isset( $show['autopatrolled'] ) && isset( $show['!patrolled'] ) )
  183. ) {
  184. $this->dieWithError( 'apierror-show' );
  185. }
  186. // Check permissions
  187. if ( $this->includesPatrollingFlags( $show ) ) {
  188. if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) {
  189. $this->dieWithError( 'apierror-permissiondenied-patrolflag', 'permissiondenied' );
  190. }
  191. }
  192. /* Add additional conditions to query depending upon parameters. */
  193. $this->addWhereIf( 'rc_minor = 0', isset( $show['!minor'] ) );
  194. $this->addWhereIf( 'rc_minor != 0', isset( $show['minor'] ) );
  195. $this->addWhereIf( 'rc_bot = 0', isset( $show['!bot'] ) );
  196. $this->addWhereIf( 'rc_bot != 0', isset( $show['bot'] ) );
  197. if ( isset( $show['anon'] ) || isset( $show['!anon'] ) ) {
  198. $actorMigration = ActorMigration::newMigration();
  199. $actorQuery = $actorMigration->getJoin( 'rc_user' );
  200. $this->addTables( $actorQuery['tables'] );
  201. $this->addJoinConds( $actorQuery['joins'] );
  202. $this->addWhereIf(
  203. $actorMigration->isAnon( $actorQuery['fields']['rc_user'] ), isset( $show['anon'] )
  204. );
  205. $this->addWhereIf(
  206. $actorMigration->isNotAnon( $actorQuery['fields']['rc_user'] ), isset( $show['!anon'] )
  207. );
  208. }
  209. $this->addWhereIf( 'rc_patrolled = 0', isset( $show['!patrolled'] ) );
  210. $this->addWhereIf( 'rc_patrolled != 0', isset( $show['patrolled'] ) );
  211. $this->addWhereIf( 'page_is_redirect = 1', isset( $show['redirect'] ) );
  212. if ( isset( $show['unpatrolled'] ) ) {
  213. // See ChangesList::isUnpatrolled
  214. if ( $user->useRCPatrol() ) {
  215. $this->addWhere( 'rc_patrolled = ' . RecentChange::PRC_UNPATROLLED );
  216. } elseif ( $user->useNPPatrol() ) {
  217. $this->addWhere( 'rc_patrolled = ' . RecentChange::PRC_UNPATROLLED );
  218. $this->addWhereFld( 'rc_type', RC_NEW );
  219. }
  220. }
  221. $this->addWhereIf(
  222. 'rc_patrolled != ' . RecentChange::PRC_AUTOPATROLLED,
  223. isset( $show['!autopatrolled'] )
  224. );
  225. $this->addWhereIf(
  226. 'rc_patrolled = ' . RecentChange::PRC_AUTOPATROLLED,
  227. isset( $show['autopatrolled'] )
  228. );
  229. // Don't throw log entries out the window here
  230. $this->addWhereIf(
  231. 'page_is_redirect = 0 OR page_is_redirect IS NULL',
  232. isset( $show['!redirect'] )
  233. );
  234. }
  235. $this->requireMaxOneParameter( $params, 'user', 'excludeuser' );
  236. if ( !is_null( $params['user'] ) ) {
  237. // Don't query by user ID here, it might be able to use the rc_user_text index.
  238. $actorQuery = ActorMigration::newMigration()
  239. ->getWhere( $this->getDB(), 'rc_user', User::newFromName( $params['user'], false ), false );
  240. $this->addTables( $actorQuery['tables'] );
  241. $this->addJoinConds( $actorQuery['joins'] );
  242. $this->addWhere( $actorQuery['conds'] );
  243. }
  244. if ( !is_null( $params['excludeuser'] ) ) {
  245. // Here there's no chance to use the rc_user_text index, so allow ID to be used.
  246. $actorQuery = ActorMigration::newMigration()
  247. ->getWhere( $this->getDB(), 'rc_user', User::newFromName( $params['excludeuser'], false ) );
  248. $this->addTables( $actorQuery['tables'] );
  249. $this->addJoinConds( $actorQuery['joins'] );
  250. $this->addWhere( 'NOT(' . $actorQuery['conds'] . ')' );
  251. }
  252. /* Add the fields we're concerned with to our query. */
  253. $this->addFields( [
  254. 'rc_id',
  255. 'rc_timestamp',
  256. 'rc_namespace',
  257. 'rc_title',
  258. 'rc_cur_id',
  259. 'rc_type',
  260. 'rc_deleted'
  261. ] );
  262. $showRedirects = false;
  263. /* Determine what properties we need to display. */
  264. if ( !is_null( $params['prop'] ) ) {
  265. $prop = array_flip( $params['prop'] );
  266. /* Set up internal members based upon params. */
  267. $this->initProperties( $prop );
  268. if ( $this->fld_patrolled && !$user->useRCPatrol() && !$user->useNPPatrol() ) {
  269. $this->dieWithError( 'apierror-permissiondenied-patrolflag', 'permissiondenied' );
  270. }
  271. /* Add fields to our query if they are specified as a needed parameter. */
  272. $this->addFieldsIf( [ 'rc_this_oldid', 'rc_last_oldid' ], $this->fld_ids );
  273. $this->addFieldsIf( [ 'rc_minor', 'rc_type', 'rc_bot' ], $this->fld_flags );
  274. $this->addFieldsIf( [ 'rc_old_len', 'rc_new_len' ], $this->fld_sizes );
  275. $this->addFieldsIf( [ 'rc_patrolled', 'rc_log_type' ], $this->fld_patrolled );
  276. $this->addFieldsIf(
  277. [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ],
  278. $this->fld_loginfo
  279. );
  280. $showRedirects = $this->fld_redirect || isset( $show['redirect'] )
  281. || isset( $show['!redirect'] );
  282. }
  283. $this->addFieldsIf( [ 'rc_this_oldid' ],
  284. $resultPageSet && $params['generaterevisions'] );
  285. if ( $this->fld_tags ) {
  286. $this->addFields( [ 'ts_tags' => ChangeTags::makeTagSummarySubquery( 'recentchanges' ) ] );
  287. }
  288. if ( $this->fld_sha1 ) {
  289. $this->addTables( 'revision' );
  290. $this->addJoinConds( [ 'revision' => [ 'LEFT JOIN',
  291. [ 'rc_this_oldid=rev_id' ] ] ] );
  292. $this->addFields( [ 'rev_sha1', 'rev_deleted' ] );
  293. }
  294. if ( $params['toponly'] || $showRedirects ) {
  295. $this->addTables( 'page' );
  296. $this->addJoinConds( [ 'page' => [ 'LEFT JOIN',
  297. [ 'rc_namespace=page_namespace', 'rc_title=page_title' ] ] ] );
  298. $this->addFields( 'page_is_redirect' );
  299. if ( $params['toponly'] ) {
  300. $this->addWhere( 'rc_this_oldid = page_latest' );
  301. }
  302. }
  303. if ( !is_null( $params['tag'] ) ) {
  304. $this->addTables( 'change_tag' );
  305. $this->addJoinConds( [ 'change_tag' => [ 'JOIN', [ 'rc_id=ct_rc_id' ] ] ] );
  306. $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
  307. try {
  308. $this->addWhereFld( 'ct_tag_id', $changeTagDefStore->getId( $params['tag'] ) );
  309. } catch ( NameTableAccessException $exception ) {
  310. // Return nothing.
  311. $this->addWhere( '1=0' );
  312. }
  313. }
  314. // Paranoia: avoid brute force searches (T19342)
  315. if ( !is_null( $params['user'] ) || !is_null( $params['excludeuser'] ) ) {
  316. if ( !$this->getPermissionManager()->userHasRight( $user, 'deletedhistory' ) ) {
  317. $bitmask = RevisionRecord::DELETED_USER;
  318. } elseif ( !$this->getPermissionManager()
  319. ->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' )
  320. ) {
  321. $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
  322. } else {
  323. $bitmask = 0;
  324. }
  325. if ( $bitmask ) {
  326. $this->addWhere( $this->getDB()->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask" );
  327. }
  328. }
  329. if ( $this->getRequest()->getCheck( 'namespace' ) ) {
  330. // LogPage::DELETED_ACTION hides the affected page, too.
  331. if ( !$this->getPermissionManager()->userHasRight( $user, 'deletedhistory' ) ) {
  332. $bitmask = LogPage::DELETED_ACTION;
  333. } elseif ( !$this->getPermissionManager()
  334. ->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' )
  335. ) {
  336. $bitmask = LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED;
  337. } else {
  338. $bitmask = 0;
  339. }
  340. if ( $bitmask ) {
  341. $this->addWhere( $this->getDB()->makeList( [
  342. 'rc_type != ' . RC_LOG,
  343. $this->getDB()->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask",
  344. ], LIST_OR ) );
  345. }
  346. }
  347. $this->token = $params['token'];
  348. if ( $this->fld_comment || $this->fld_parsedcomment || $this->token ) {
  349. $this->commentStore = CommentStore::getStore();
  350. $commentQuery = $this->commentStore->getJoin( 'rc_comment' );
  351. $this->addTables( $commentQuery['tables'] );
  352. $this->addFields( $commentQuery['fields'] );
  353. $this->addJoinConds( $commentQuery['joins'] );
  354. }
  355. if ( $this->fld_user || $this->fld_userid || !is_null( $this->token ) ) {
  356. // Token needs rc_user for RecentChange::newFromRow/User::newFromAnyId (T228425)
  357. $actorQuery = ActorMigration::newMigration()->getJoin( 'rc_user' );
  358. $this->addTables( $actorQuery['tables'] );
  359. $this->addFields( $actorQuery['fields'] );
  360. $this->addJoinConds( $actorQuery['joins'] );
  361. }
  362. $this->addOption( 'LIMIT', $params['limit'] + 1 );
  363. $hookData = [];
  364. $count = 0;
  365. /* Perform the actual query. */
  366. $res = $this->select( __METHOD__, [], $hookData );
  367. $revids = [];
  368. $titles = [];
  369. $result = $this->getResult();
  370. /* Iterate through the rows, adding data extracted from them to our query result. */
  371. foreach ( $res as $row ) {
  372. if ( $count === 0 && $resultPageSet !== null ) {
  373. // Set the non-continue since the list of recentchanges is
  374. // prone to having entries added at the start frequently.
  375. $this->getContinuationManager()->addGeneratorNonContinueParam(
  376. $this, 'continue', "$row->rc_timestamp|$row->rc_id"
  377. );
  378. }
  379. if ( ++$count > $params['limit'] ) {
  380. // We've reached the one extra which shows that there are
  381. // additional pages to be had. Stop here...
  382. $this->setContinueEnumParameter( 'continue', "$row->rc_timestamp|$row->rc_id" );
  383. break;
  384. }
  385. if ( is_null( $resultPageSet ) ) {
  386. /* Extract the data from a single row. */
  387. $vals = $this->extractRowInfo( $row );
  388. /* Add that row's data to our final output. */
  389. $fit = $this->processRow( $row, $vals, $hookData ) &&
  390. $result->addValue( [ 'query', $this->getModuleName() ], null, $vals );
  391. if ( !$fit ) {
  392. $this->setContinueEnumParameter( 'continue', "$row->rc_timestamp|$row->rc_id" );
  393. break;
  394. }
  395. } elseif ( $params['generaterevisions'] ) {
  396. $revid = (int)$row->rc_this_oldid;
  397. if ( $revid > 0 ) {
  398. $revids[] = $revid;
  399. }
  400. } else {
  401. $titles[] = Title::makeTitle( $row->rc_namespace, $row->rc_title );
  402. }
  403. }
  404. if ( is_null( $resultPageSet ) ) {
  405. /* Format the result */
  406. $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'rc' );
  407. } elseif ( $params['generaterevisions'] ) {
  408. $resultPageSet->populateFromRevisionIDs( $revids );
  409. } else {
  410. $resultPageSet->populateFromTitles( $titles );
  411. }
  412. }
  413. /**
  414. * Extracts from a single sql row the data needed to describe one recent change.
  415. *
  416. * @param stdClass $row The row from which to extract the data.
  417. * @return array An array mapping strings (descriptors) to their respective string values.
  418. */
  419. public function extractRowInfo( $row ) {
  420. /* Determine the title of the page that has been changed. */
  421. $title = Title::makeTitle( $row->rc_namespace, $row->rc_title );
  422. $user = $this->getUser();
  423. /* Our output data. */
  424. $vals = [];
  425. $type = (int)$row->rc_type;
  426. $vals['type'] = RecentChange::parseFromRCType( $type );
  427. $anyHidden = false;
  428. /* Create a new entry in the result for the title. */
  429. if ( $this->fld_title || $this->fld_ids ) {
  430. if ( $type === RC_LOG && ( $row->rc_deleted & LogPage::DELETED_ACTION ) ) {
  431. $vals['actionhidden'] = true;
  432. $anyHidden = true;
  433. }
  434. if ( $type !== RC_LOG ||
  435. LogEventsList::userCanBitfield( $row->rc_deleted, LogPage::DELETED_ACTION, $user )
  436. ) {
  437. if ( $this->fld_title ) {
  438. ApiQueryBase::addTitleInfo( $vals, $title );
  439. }
  440. if ( $this->fld_ids ) {
  441. $vals['pageid'] = (int)$row->rc_cur_id;
  442. $vals['revid'] = (int)$row->rc_this_oldid;
  443. $vals['old_revid'] = (int)$row->rc_last_oldid;
  444. }
  445. }
  446. }
  447. if ( $this->fld_ids ) {
  448. $vals['rcid'] = (int)$row->rc_id;
  449. }
  450. /* Add user data and 'anon' flag, if user is anonymous. */
  451. if ( $this->fld_user || $this->fld_userid ) {
  452. if ( $row->rc_deleted & RevisionRecord::DELETED_USER ) {
  453. $vals['userhidden'] = true;
  454. $anyHidden = true;
  455. }
  456. if ( RevisionRecord::userCanBitfield( $row->rc_deleted, RevisionRecord::DELETED_USER, $user ) ) {
  457. if ( $this->fld_user ) {
  458. $vals['user'] = $row->rc_user_text;
  459. }
  460. if ( $this->fld_userid ) {
  461. $vals['userid'] = (int)$row->rc_user;
  462. }
  463. if ( !$row->rc_user ) {
  464. $vals['anon'] = true;
  465. }
  466. }
  467. }
  468. /* Add flags, such as new, minor, bot. */
  469. if ( $this->fld_flags ) {
  470. $vals['bot'] = (bool)$row->rc_bot;
  471. $vals['new'] = $row->rc_type == RC_NEW;
  472. $vals['minor'] = (bool)$row->rc_minor;
  473. }
  474. /* Add sizes of each revision. (Only available on 1.10+) */
  475. if ( $this->fld_sizes ) {
  476. $vals['oldlen'] = (int)$row->rc_old_len;
  477. $vals['newlen'] = (int)$row->rc_new_len;
  478. }
  479. /* Add the timestamp. */
  480. if ( $this->fld_timestamp ) {
  481. $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $row->rc_timestamp );
  482. }
  483. /* Add edit summary / log summary. */
  484. if ( $this->fld_comment || $this->fld_parsedcomment ) {
  485. if ( $row->rc_deleted & RevisionRecord::DELETED_COMMENT ) {
  486. $vals['commenthidden'] = true;
  487. $anyHidden = true;
  488. }
  489. if ( RevisionRecord::userCanBitfield(
  490. $row->rc_deleted, RevisionRecord::DELETED_COMMENT, $user
  491. ) ) {
  492. $comment = $this->commentStore->getComment( 'rc_comment', $row )->text;
  493. if ( $this->fld_comment ) {
  494. $vals['comment'] = $comment;
  495. }
  496. if ( $this->fld_parsedcomment ) {
  497. $vals['parsedcomment'] = Linker::formatComment( $comment, $title );
  498. }
  499. }
  500. }
  501. if ( $this->fld_redirect ) {
  502. $vals['redirect'] = (bool)$row->page_is_redirect;
  503. }
  504. /* Add the patrolled flag */
  505. if ( $this->fld_patrolled ) {
  506. $vals['patrolled'] = $row->rc_patrolled != RecentChange::PRC_UNPATROLLED;
  507. $vals['unpatrolled'] = ChangesList::isUnpatrolled( $row, $user );
  508. $vals['autopatrolled'] = $row->rc_patrolled == RecentChange::PRC_AUTOPATROLLED;
  509. }
  510. if ( $this->fld_loginfo && $row->rc_type == RC_LOG ) {
  511. if ( $row->rc_deleted & LogPage::DELETED_ACTION ) {
  512. $vals['actionhidden'] = true;
  513. $anyHidden = true;
  514. }
  515. if ( LogEventsList::userCanBitfield( $row->rc_deleted, LogPage::DELETED_ACTION, $user ) ) {
  516. $vals['logid'] = (int)$row->rc_logid;
  517. $vals['logtype'] = $row->rc_log_type;
  518. $vals['logaction'] = $row->rc_log_action;
  519. $vals['logparams'] = LogFormatter::newFromRow( $row )->formatParametersForApi();
  520. }
  521. }
  522. if ( $this->fld_tags ) {
  523. if ( $row->ts_tags ) {
  524. $tags = explode( ',', $row->ts_tags );
  525. ApiResult::setIndexedTagName( $tags, 'tag' );
  526. $vals['tags'] = $tags;
  527. } else {
  528. $vals['tags'] = [];
  529. }
  530. }
  531. if ( $this->fld_sha1 && $row->rev_sha1 !== null ) {
  532. if ( $row->rev_deleted & RevisionRecord::DELETED_TEXT ) {
  533. $vals['sha1hidden'] = true;
  534. $anyHidden = true;
  535. }
  536. if ( RevisionRecord::userCanBitfield(
  537. $row->rev_deleted, RevisionRecord::DELETED_TEXT, $user
  538. ) ) {
  539. if ( $row->rev_sha1 !== '' ) {
  540. $vals['sha1'] = Wikimedia\base_convert( $row->rev_sha1, 36, 16, 40 );
  541. } else {
  542. $vals['sha1'] = '';
  543. }
  544. }
  545. }
  546. if ( !is_null( $this->token ) ) {
  547. $tokenFunctions = $this->getTokenFunctions();
  548. foreach ( $this->token as $t ) {
  549. $val = call_user_func( $tokenFunctions[$t], $row->rc_cur_id,
  550. $title, RecentChange::newFromRow( $row ) );
  551. if ( $val === false ) {
  552. $this->addWarning( [ 'apiwarn-tokennotallowed', $t ] );
  553. } else {
  554. $vals[$t . 'token'] = $val;
  555. }
  556. }
  557. }
  558. if ( $anyHidden && ( $row->rc_deleted & RevisionRecord::DELETED_RESTRICTED ) ) {
  559. $vals['suppressed'] = true;
  560. }
  561. return $vals;
  562. }
  563. /**
  564. * @param array $flagsArray flipped array (string flags are keys)
  565. * @return bool
  566. */
  567. private function includesPatrollingFlags( array $flagsArray ) {
  568. return isset( $flagsArray['patrolled'] ) ||
  569. isset( $flagsArray['!patrolled'] ) ||
  570. isset( $flagsArray['unpatrolled'] ) ||
  571. isset( $flagsArray['autopatrolled'] ) ||
  572. isset( $flagsArray['!autopatrolled'] );
  573. }
  574. public function getCacheMode( $params ) {
  575. if ( isset( $params['show'] ) &&
  576. $this->includesPatrollingFlags( array_flip( $params['show'] ) )
  577. ) {
  578. return 'private';
  579. }
  580. if ( isset( $params['token'] ) ) {
  581. return 'private';
  582. }
  583. if ( $this->userCanSeeRevDel() ) {
  584. return 'private';
  585. }
  586. if ( !is_null( $params['prop'] ) && in_array( 'parsedcomment', $params['prop'] ) ) {
  587. // formatComment() calls wfMessage() among other things
  588. return 'anon-public-user-private';
  589. }
  590. return 'public';
  591. }
  592. public function getAllowedParams() {
  593. return [
  594. 'start' => [
  595. ApiBase::PARAM_TYPE => 'timestamp'
  596. ],
  597. 'end' => [
  598. ApiBase::PARAM_TYPE => 'timestamp'
  599. ],
  600. 'dir' => [
  601. ApiBase::PARAM_DFLT => 'older',
  602. ApiBase::PARAM_TYPE => [
  603. 'newer',
  604. 'older'
  605. ],
  606. ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
  607. ],
  608. 'namespace' => [
  609. ApiBase::PARAM_ISMULTI => true,
  610. ApiBase::PARAM_TYPE => 'namespace',
  611. ApiBase::PARAM_EXTRA_NAMESPACES => [ NS_MEDIA, NS_SPECIAL ],
  612. ],
  613. 'user' => [
  614. ApiBase::PARAM_TYPE => 'user'
  615. ],
  616. 'excludeuser' => [
  617. ApiBase::PARAM_TYPE => 'user'
  618. ],
  619. 'tag' => null,
  620. 'prop' => [
  621. ApiBase::PARAM_ISMULTI => true,
  622. ApiBase::PARAM_DFLT => 'title|timestamp|ids',
  623. ApiBase::PARAM_TYPE => [
  624. 'user',
  625. 'userid',
  626. 'comment',
  627. 'parsedcomment',
  628. 'flags',
  629. 'timestamp',
  630. 'title',
  631. 'ids',
  632. 'sizes',
  633. 'redirect',
  634. 'patrolled',
  635. 'loginfo',
  636. 'tags',
  637. 'sha1',
  638. ],
  639. ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
  640. ],
  641. 'token' => [
  642. ApiBase::PARAM_DEPRECATED => true,
  643. ApiBase::PARAM_TYPE => array_keys( $this->getTokenFunctions() ),
  644. ApiBase::PARAM_ISMULTI => true
  645. ],
  646. 'show' => [
  647. ApiBase::PARAM_ISMULTI => true,
  648. ApiBase::PARAM_TYPE => [
  649. 'minor',
  650. '!minor',
  651. 'bot',
  652. '!bot',
  653. 'anon',
  654. '!anon',
  655. 'redirect',
  656. '!redirect',
  657. 'patrolled',
  658. '!patrolled',
  659. 'unpatrolled',
  660. 'autopatrolled',
  661. '!autopatrolled',
  662. ]
  663. ],
  664. 'limit' => [
  665. ApiBase::PARAM_DFLT => 10,
  666. ApiBase::PARAM_TYPE => 'limit',
  667. ApiBase::PARAM_MIN => 1,
  668. ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1,
  669. ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2
  670. ],
  671. 'type' => [
  672. ApiBase::PARAM_DFLT => 'edit|new|log|categorize',
  673. ApiBase::PARAM_ISMULTI => true,
  674. ApiBase::PARAM_TYPE => RecentChange::getChangeTypes()
  675. ],
  676. 'toponly' => false,
  677. 'title' => null,
  678. 'continue' => [
  679. ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
  680. ],
  681. 'generaterevisions' => false,
  682. ];
  683. }
  684. protected function getExamplesMessages() {
  685. return [
  686. 'action=query&list=recentchanges'
  687. => 'apihelp-query+recentchanges-example-simple',
  688. 'action=query&generator=recentchanges&grcshow=!patrolled&prop=info'
  689. => 'apihelp-query+recentchanges-example-generator',
  690. ];
  691. }
  692. public function getHelpUrls() {
  693. return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Recentchanges';
  694. }
  695. }