ApiQueryWatchlist.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  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. /**
  25. * This query action allows clients to retrieve a list of recently modified pages
  26. * that are part of the logged-in user's watchlist.
  27. *
  28. * @ingroup API
  29. */
  30. class ApiQueryWatchlist extends ApiQueryGeneratorBase {
  31. /** @var CommentStore */
  32. private $commentStore;
  33. public function __construct( ApiQuery $query, $moduleName ) {
  34. parent::__construct( $query, $moduleName, 'wl' );
  35. }
  36. public function execute() {
  37. $this->run();
  38. }
  39. public function executeGenerator( $resultPageSet ) {
  40. $this->run( $resultPageSet );
  41. }
  42. private $fld_ids = false, $fld_title = false, $fld_patrol = false,
  43. $fld_flags = false, $fld_timestamp = false, $fld_user = false,
  44. $fld_comment = false, $fld_parsedcomment = false, $fld_sizes = false,
  45. $fld_notificationtimestamp = false, $fld_userid = false,
  46. $fld_loginfo = false, $fld_tags;
  47. /**
  48. * @param ApiPageSet $resultPageSet
  49. * @return void
  50. */
  51. private function run( $resultPageSet = null ) {
  52. $this->selectNamedDB( 'watchlist', DB_REPLICA, 'watchlist' );
  53. $params = $this->extractRequestParams();
  54. $user = $this->getUser();
  55. $wlowner = $this->getWatchlistUser( $params );
  56. if ( !is_null( $params['prop'] ) && is_null( $resultPageSet ) ) {
  57. $prop = array_flip( $params['prop'] );
  58. $this->fld_ids = isset( $prop['ids'] );
  59. $this->fld_title = isset( $prop['title'] );
  60. $this->fld_flags = isset( $prop['flags'] );
  61. $this->fld_user = isset( $prop['user'] );
  62. $this->fld_userid = isset( $prop['userid'] );
  63. $this->fld_comment = isset( $prop['comment'] );
  64. $this->fld_parsedcomment = isset( $prop['parsedcomment'] );
  65. $this->fld_timestamp = isset( $prop['timestamp'] );
  66. $this->fld_sizes = isset( $prop['sizes'] );
  67. $this->fld_patrol = isset( $prop['patrol'] );
  68. $this->fld_notificationtimestamp = isset( $prop['notificationtimestamp'] );
  69. $this->fld_loginfo = isset( $prop['loginfo'] );
  70. $this->fld_tags = isset( $prop['tags'] );
  71. if ( $this->fld_patrol && !$user->useRCPatrol() && !$user->useNPPatrol() ) {
  72. $this->dieWithError( 'apierror-permissiondenied-patrolflag', 'patrol' );
  73. }
  74. if ( $this->fld_comment || $this->fld_parsedcomment ) {
  75. $this->commentStore = CommentStore::getStore();
  76. }
  77. }
  78. $options = [
  79. 'dir' => $params['dir'] === 'older'
  80. ? WatchedItemQueryService::DIR_OLDER
  81. : WatchedItemQueryService::DIR_NEWER,
  82. ];
  83. if ( is_null( $resultPageSet ) ) {
  84. $options['includeFields'] = $this->getFieldsToInclude();
  85. } else {
  86. $options['usedInGenerator'] = true;
  87. }
  88. if ( $params['start'] ) {
  89. $options['start'] = $params['start'];
  90. }
  91. if ( $params['end'] ) {
  92. $options['end'] = $params['end'];
  93. }
  94. $startFrom = null;
  95. if ( !is_null( $params['continue'] ) ) {
  96. $cont = explode( '|', $params['continue'] );
  97. $this->dieContinueUsageIf( count( $cont ) != 2 );
  98. $continueTimestamp = $cont[0];
  99. $continueId = (int)$cont[1];
  100. $this->dieContinueUsageIf( $continueId != $cont[1] );
  101. $startFrom = [ $continueTimestamp, $continueId ];
  102. }
  103. if ( $wlowner !== $user ) {
  104. $options['watchlistOwner'] = $wlowner;
  105. $options['watchlistOwnerToken'] = $params['token'];
  106. }
  107. if ( !is_null( $params['namespace'] ) ) {
  108. $options['namespaceIds'] = $params['namespace'];
  109. }
  110. if ( $params['allrev'] ) {
  111. $options['allRevisions'] = true;
  112. }
  113. if ( !is_null( $params['show'] ) ) {
  114. $show = array_flip( $params['show'] );
  115. /* Check for conflicting parameters. */
  116. if ( $this->showParamsConflicting( $show ) ) {
  117. $this->dieWithError( 'apierror-show' );
  118. }
  119. // Check permissions.
  120. if ( isset( $show[WatchedItemQueryService::FILTER_PATROLLED] )
  121. || isset( $show[WatchedItemQueryService::FILTER_NOT_PATROLLED] )
  122. ) {
  123. if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) {
  124. $this->dieWithError( 'apierror-permissiondenied-patrolflag', 'permissiondenied' );
  125. }
  126. }
  127. $options['filters'] = array_keys( $show );
  128. }
  129. if ( !is_null( $params['type'] ) ) {
  130. try {
  131. $rcTypes = RecentChange::parseToRCType( $params['type'] );
  132. if ( $rcTypes ) {
  133. $options['rcTypes'] = $rcTypes;
  134. }
  135. } catch ( Exception $e ) {
  136. ApiBase::dieDebug( __METHOD__, $e->getMessage() );
  137. }
  138. }
  139. $this->requireMaxOneParameter( $params, 'user', 'excludeuser' );
  140. if ( !is_null( $params['user'] ) ) {
  141. $options['onlyByUser'] = $params['user'];
  142. }
  143. if ( !is_null( $params['excludeuser'] ) ) {
  144. $options['notByUser'] = $params['excludeuser'];
  145. }
  146. $options['limit'] = $params['limit'];
  147. Hooks::run( 'ApiQueryWatchlistPrepareWatchedItemQueryServiceOptions', [
  148. $this, $params, &$options
  149. ] );
  150. $ids = [];
  151. $watchedItemQuery = MediaWikiServices::getInstance()->getWatchedItemQueryService();
  152. $items = $watchedItemQuery->getWatchedItemsWithRecentChangeInfo( $wlowner, $options, $startFrom );
  153. foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
  154. /** @var WatchedItem $watchedItem */
  155. if ( is_null( $resultPageSet ) ) {
  156. $vals = $this->extractOutputData( $watchedItem, $recentChangeInfo );
  157. $fit = $this->getResult()->addValue( [ 'query', $this->getModuleName() ], null, $vals );
  158. if ( !$fit ) {
  159. $startFrom = [ $recentChangeInfo['rc_timestamp'], $recentChangeInfo['rc_id'] ];
  160. break;
  161. }
  162. } elseif ( $params['allrev'] ) {
  163. $ids[] = (int)$recentChangeInfo['rc_this_oldid'];
  164. } else {
  165. $ids[] = (int)$recentChangeInfo['rc_cur_id'];
  166. }
  167. }
  168. if ( $startFrom !== null ) {
  169. $this->setContinueEnumParameter( 'continue', implode( '|', $startFrom ) );
  170. }
  171. if ( is_null( $resultPageSet ) ) {
  172. $this->getResult()->addIndexedTagName(
  173. [ 'query', $this->getModuleName() ],
  174. 'item'
  175. );
  176. } elseif ( $params['allrev'] ) {
  177. $resultPageSet->populateFromRevisionIDs( $ids );
  178. } else {
  179. $resultPageSet->populateFromPageIDs( $ids );
  180. }
  181. }
  182. private function getFieldsToInclude() {
  183. $includeFields = [];
  184. if ( $this->fld_flags ) {
  185. $includeFields[] = WatchedItemQueryService::INCLUDE_FLAGS;
  186. }
  187. if ( $this->fld_user || $this->fld_userid ) {
  188. $includeFields[] = WatchedItemQueryService::INCLUDE_USER_ID;
  189. }
  190. if ( $this->fld_user ) {
  191. $includeFields[] = WatchedItemQueryService::INCLUDE_USER;
  192. }
  193. if ( $this->fld_comment || $this->fld_parsedcomment ) {
  194. $includeFields[] = WatchedItemQueryService::INCLUDE_COMMENT;
  195. }
  196. if ( $this->fld_patrol ) {
  197. $includeFields[] = WatchedItemQueryService::INCLUDE_PATROL_INFO;
  198. $includeFields[] = WatchedItemQueryService::INCLUDE_AUTOPATROL_INFO;
  199. }
  200. if ( $this->fld_sizes ) {
  201. $includeFields[] = WatchedItemQueryService::INCLUDE_SIZES;
  202. }
  203. if ( $this->fld_loginfo ) {
  204. $includeFields[] = WatchedItemQueryService::INCLUDE_LOG_INFO;
  205. }
  206. if ( $this->fld_tags ) {
  207. $includeFields[] = WatchedItemQueryService::INCLUDE_TAGS;
  208. }
  209. return $includeFields;
  210. }
  211. private function showParamsConflicting( array $show ) {
  212. return ( isset( $show[WatchedItemQueryService::FILTER_MINOR] )
  213. && isset( $show[WatchedItemQueryService::FILTER_NOT_MINOR] ) )
  214. || ( isset( $show[WatchedItemQueryService::FILTER_BOT] )
  215. && isset( $show[WatchedItemQueryService::FILTER_NOT_BOT] ) )
  216. || ( isset( $show[WatchedItemQueryService::FILTER_ANON] )
  217. && isset( $show[WatchedItemQueryService::FILTER_NOT_ANON] ) )
  218. || ( isset( $show[WatchedItemQueryService::FILTER_PATROLLED] )
  219. && isset( $show[WatchedItemQueryService::FILTER_NOT_PATROLLED] ) )
  220. || ( isset( $show[WatchedItemQueryService::FILTER_AUTOPATROLLED] )
  221. && isset( $show[WatchedItemQueryService::FILTER_NOT_AUTOPATROLLED] ) )
  222. || ( isset( $show[WatchedItemQueryService::FILTER_AUTOPATROLLED] )
  223. && isset( $show[WatchedItemQueryService::FILTER_NOT_PATROLLED] ) )
  224. || ( isset( $show[WatchedItemQueryService::FILTER_UNREAD] )
  225. && isset( $show[WatchedItemQueryService::FILTER_NOT_UNREAD] ) );
  226. }
  227. private function extractOutputData( WatchedItem $watchedItem, array $recentChangeInfo ) {
  228. /* Determine the title of the page that has been changed. */
  229. $title = Title::newFromLinkTarget( $watchedItem->getLinkTarget() );
  230. $user = $this->getUser();
  231. /* Our output data. */
  232. $vals = [];
  233. $type = (int)$recentChangeInfo['rc_type'];
  234. $vals['type'] = RecentChange::parseFromRCType( $type );
  235. $anyHidden = false;
  236. /* Create a new entry in the result for the title. */
  237. if ( $this->fld_title || $this->fld_ids ) {
  238. // These should already have been filtered out of the query, but just in case.
  239. if ( $type === RC_LOG && ( $recentChangeInfo['rc_deleted'] & LogPage::DELETED_ACTION ) ) {
  240. $vals['actionhidden'] = true;
  241. $anyHidden = true;
  242. }
  243. if ( $type !== RC_LOG ||
  244. LogEventsList::userCanBitfield(
  245. $recentChangeInfo['rc_deleted'],
  246. LogPage::DELETED_ACTION,
  247. $user
  248. )
  249. ) {
  250. if ( $this->fld_title ) {
  251. ApiQueryBase::addTitleInfo( $vals, $title );
  252. }
  253. if ( $this->fld_ids ) {
  254. $vals['pageid'] = (int)$recentChangeInfo['rc_cur_id'];
  255. $vals['revid'] = (int)$recentChangeInfo['rc_this_oldid'];
  256. $vals['old_revid'] = (int)$recentChangeInfo['rc_last_oldid'];
  257. }
  258. }
  259. }
  260. /* Add user data and 'anon' flag, if user is anonymous. */
  261. if ( $this->fld_user || $this->fld_userid ) {
  262. if ( $recentChangeInfo['rc_deleted'] & RevisionRecord::DELETED_USER ) {
  263. $vals['userhidden'] = true;
  264. $anyHidden = true;
  265. }
  266. if ( RevisionRecord::userCanBitfield(
  267. $recentChangeInfo['rc_deleted'],
  268. RevisionRecord::DELETED_USER,
  269. $user
  270. ) ) {
  271. if ( $this->fld_userid ) {
  272. $vals['userid'] = (int)$recentChangeInfo['rc_user'];
  273. // for backwards compatibility
  274. $vals['user'] = (int)$recentChangeInfo['rc_user'];
  275. }
  276. if ( $this->fld_user ) {
  277. $vals['user'] = $recentChangeInfo['rc_user_text'];
  278. }
  279. if ( !$recentChangeInfo['rc_user'] ) {
  280. $vals['anon'] = true;
  281. }
  282. }
  283. }
  284. /* Add flags, such as new, minor, bot. */
  285. if ( $this->fld_flags ) {
  286. $vals['bot'] = (bool)$recentChangeInfo['rc_bot'];
  287. $vals['new'] = $recentChangeInfo['rc_type'] == RC_NEW;
  288. $vals['minor'] = (bool)$recentChangeInfo['rc_minor'];
  289. }
  290. /* Add sizes of each revision. (Only available on 1.10+) */
  291. if ( $this->fld_sizes ) {
  292. $vals['oldlen'] = (int)$recentChangeInfo['rc_old_len'];
  293. $vals['newlen'] = (int)$recentChangeInfo['rc_new_len'];
  294. }
  295. /* Add the timestamp. */
  296. if ( $this->fld_timestamp ) {
  297. $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $recentChangeInfo['rc_timestamp'] );
  298. }
  299. if ( $this->fld_notificationtimestamp ) {
  300. $vals['notificationtimestamp'] = ( $watchedItem->getNotificationTimestamp() == null )
  301. ? ''
  302. : wfTimestamp( TS_ISO_8601, $watchedItem->getNotificationTimestamp() );
  303. }
  304. /* Add edit summary / log summary. */
  305. if ( $this->fld_comment || $this->fld_parsedcomment ) {
  306. if ( $recentChangeInfo['rc_deleted'] & RevisionRecord::DELETED_COMMENT ) {
  307. $vals['commenthidden'] = true;
  308. $anyHidden = true;
  309. }
  310. if ( RevisionRecord::userCanBitfield(
  311. $recentChangeInfo['rc_deleted'],
  312. RevisionRecord::DELETED_COMMENT,
  313. $user
  314. ) ) {
  315. $comment = $this->commentStore->getComment( 'rc_comment', $recentChangeInfo )->text;
  316. if ( $this->fld_comment ) {
  317. $vals['comment'] = $comment;
  318. }
  319. if ( $this->fld_parsedcomment ) {
  320. $vals['parsedcomment'] = Linker::formatComment( $comment, $title );
  321. }
  322. }
  323. }
  324. /* Add the patrolled flag */
  325. if ( $this->fld_patrol ) {
  326. $vals['patrolled'] = $recentChangeInfo['rc_patrolled'] != RecentChange::PRC_UNPATROLLED;
  327. $vals['unpatrolled'] = ChangesList::isUnpatrolled( (object)$recentChangeInfo, $user );
  328. $vals['autopatrolled'] = $recentChangeInfo['rc_patrolled'] == RecentChange::PRC_AUTOPATROLLED;
  329. }
  330. if ( $this->fld_loginfo && $recentChangeInfo['rc_type'] == RC_LOG ) {
  331. if ( $recentChangeInfo['rc_deleted'] & LogPage::DELETED_ACTION ) {
  332. $vals['actionhidden'] = true;
  333. $anyHidden = true;
  334. }
  335. if ( LogEventsList::userCanBitfield(
  336. $recentChangeInfo['rc_deleted'],
  337. LogPage::DELETED_ACTION,
  338. $user
  339. ) ) {
  340. $vals['logid'] = (int)$recentChangeInfo['rc_logid'];
  341. $vals['logtype'] = $recentChangeInfo['rc_log_type'];
  342. $vals['logaction'] = $recentChangeInfo['rc_log_action'];
  343. $vals['logparams'] = LogFormatter::newFromRow( $recentChangeInfo )->formatParametersForApi();
  344. }
  345. }
  346. if ( $this->fld_tags ) {
  347. if ( $recentChangeInfo['rc_tags'] ) {
  348. $tags = explode( ',', $recentChangeInfo['rc_tags'] );
  349. ApiResult::setIndexedTagName( $tags, 'tag' );
  350. $vals['tags'] = $tags;
  351. } else {
  352. $vals['tags'] = [];
  353. }
  354. }
  355. if ( $anyHidden && ( $recentChangeInfo['rc_deleted'] & RevisionRecord::DELETED_RESTRICTED ) ) {
  356. $vals['suppressed'] = true;
  357. }
  358. Hooks::run( 'ApiQueryWatchlistExtractOutputData', [
  359. $this, $watchedItem, $recentChangeInfo, &$vals
  360. ] );
  361. return $vals;
  362. }
  363. public function getAllowedParams() {
  364. return [
  365. 'allrev' => false,
  366. 'start' => [
  367. ApiBase::PARAM_TYPE => 'timestamp'
  368. ],
  369. 'end' => [
  370. ApiBase::PARAM_TYPE => 'timestamp'
  371. ],
  372. 'namespace' => [
  373. ApiBase::PARAM_ISMULTI => true,
  374. ApiBase::PARAM_TYPE => 'namespace'
  375. ],
  376. 'user' => [
  377. ApiBase::PARAM_TYPE => 'user',
  378. ],
  379. 'excludeuser' => [
  380. ApiBase::PARAM_TYPE => 'user',
  381. ],
  382. 'dir' => [
  383. ApiBase::PARAM_DFLT => 'older',
  384. ApiBase::PARAM_TYPE => [
  385. 'newer',
  386. 'older'
  387. ],
  388. ApiHelp::PARAM_HELP_MSG => 'api-help-param-direction',
  389. ],
  390. 'limit' => [
  391. ApiBase::PARAM_DFLT => 10,
  392. ApiBase::PARAM_TYPE => 'limit',
  393. ApiBase::PARAM_MIN => 1,
  394. ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1,
  395. ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2
  396. ],
  397. 'prop' => [
  398. ApiBase::PARAM_ISMULTI => true,
  399. ApiBase::PARAM_DFLT => 'ids|title|flags',
  400. ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
  401. ApiBase::PARAM_TYPE => [
  402. 'ids',
  403. 'title',
  404. 'flags',
  405. 'user',
  406. 'userid',
  407. 'comment',
  408. 'parsedcomment',
  409. 'timestamp',
  410. 'patrol',
  411. 'sizes',
  412. 'notificationtimestamp',
  413. 'loginfo',
  414. 'tags',
  415. ]
  416. ],
  417. 'show' => [
  418. ApiBase::PARAM_ISMULTI => true,
  419. ApiBase::PARAM_TYPE => [
  420. WatchedItemQueryService::FILTER_MINOR,
  421. WatchedItemQueryService::FILTER_NOT_MINOR,
  422. WatchedItemQueryService::FILTER_BOT,
  423. WatchedItemQueryService::FILTER_NOT_BOT,
  424. WatchedItemQueryService::FILTER_ANON,
  425. WatchedItemQueryService::FILTER_NOT_ANON,
  426. WatchedItemQueryService::FILTER_PATROLLED,
  427. WatchedItemQueryService::FILTER_NOT_PATROLLED,
  428. WatchedItemQueryService::FILTER_AUTOPATROLLED,
  429. WatchedItemQueryService::FILTER_NOT_AUTOPATROLLED,
  430. WatchedItemQueryService::FILTER_UNREAD,
  431. WatchedItemQueryService::FILTER_NOT_UNREAD,
  432. ]
  433. ],
  434. 'type' => [
  435. ApiBase::PARAM_DFLT => 'edit|new|log|categorize',
  436. ApiBase::PARAM_ISMULTI => true,
  437. ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
  438. ApiBase::PARAM_TYPE => RecentChange::getChangeTypes()
  439. ],
  440. 'owner' => [
  441. ApiBase::PARAM_TYPE => 'user'
  442. ],
  443. 'token' => [
  444. ApiBase::PARAM_TYPE => 'string',
  445. ApiBase::PARAM_SENSITIVE => true,
  446. ],
  447. 'continue' => [
  448. ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
  449. ],
  450. ];
  451. }
  452. protected function getExamplesMessages() {
  453. return [
  454. 'action=query&list=watchlist'
  455. => 'apihelp-query+watchlist-example-simple',
  456. 'action=query&list=watchlist&wlprop=ids|title|timestamp|user|comment'
  457. => 'apihelp-query+watchlist-example-props',
  458. 'action=query&list=watchlist&wlallrev=&wlprop=ids|title|timestamp|user|comment'
  459. => 'apihelp-query+watchlist-example-allrev',
  460. 'action=query&generator=watchlist&prop=info'
  461. => 'apihelp-query+watchlist-example-generator',
  462. 'action=query&generator=watchlist&gwlallrev=&prop=revisions&rvprop=timestamp|user'
  463. => 'apihelp-query+watchlist-example-generator-rev',
  464. 'action=query&list=watchlist&wlowner=Example&wltoken=123ABC'
  465. => 'apihelp-query+watchlist-example-wlowner',
  466. ];
  467. }
  468. public function getHelpUrls() {
  469. return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Watchlist';
  470. }
  471. }