MediaWiki fundraising/REL1_35
ApiQueryWatchlist.php
Go to the documentation of this file.
1<?php
26
34
37
38 public function __construct( ApiQuery $query, $moduleName ) {
39 parent::__construct( $query, $moduleName, 'wl' );
40 }
41
42 public function execute() {
43 $this->run();
44 }
45
46 public function executeGenerator( $resultPageSet ) {
47 $this->run( $resultPageSet );
48 }
49
50 private $fld_ids = false, $fld_title = false, $fld_patrol = false,
51 $fld_flags = false, $fld_timestamp = false, $fld_user = false,
52 $fld_comment = false, $fld_parsedcomment = false, $fld_sizes = false,
55
60 private function run( $resultPageSet = null ) {
61 $this->selectNamedDB( 'watchlist', DB_REPLICA, 'watchlist' );
62
63 $params = $this->extractRequestParams();
64
65 $user = $this->getUser();
66 $wlowner = $this->getWatchlistUser( $params );
67
68 if ( $params['prop'] !== null && $resultPageSet === null ) {
69 $prop = array_flip( $params['prop'] );
70
71 $this->fld_ids = isset( $prop['ids'] );
72 $this->fld_title = isset( $prop['title'] );
73 $this->fld_flags = isset( $prop['flags'] );
74 $this->fld_user = isset( $prop['user'] );
75 $this->fld_userid = isset( $prop['userid'] );
76 $this->fld_comment = isset( $prop['comment'] );
77 $this->fld_parsedcomment = isset( $prop['parsedcomment'] );
78 $this->fld_timestamp = isset( $prop['timestamp'] );
79 $this->fld_sizes = isset( $prop['sizes'] );
80 $this->fld_patrol = isset( $prop['patrol'] );
81 $this->fld_notificationtimestamp = isset( $prop['notificationtimestamp'] );
82 $this->fld_loginfo = isset( $prop['loginfo'] );
83 $this->fld_tags = isset( $prop['tags'] );
84
85 if ( $this->fld_patrol && !$user->useRCPatrol() && !$user->useNPPatrol() ) {
86 $this->dieWithError( 'apierror-permissiondenied-patrolflag', 'patrol' );
87 }
88
89 if ( $this->fld_comment || $this->fld_parsedcomment ) {
90 $this->commentStore = CommentStore::getStore();
91 }
92 }
93
94 $options = [
95 'dir' => $params['dir'] === 'older'
96 ? WatchedItemQueryService::DIR_OLDER
97 : WatchedItemQueryService::DIR_NEWER,
98 ];
99
100 if ( $resultPageSet === null ) {
101 $options['includeFields'] = $this->getFieldsToInclude();
102 } else {
103 $options['usedInGenerator'] = true;
104 }
105
106 if ( $params['start'] ) {
107 $options['start'] = $params['start'];
108 }
109 if ( $params['end'] ) {
110 $options['end'] = $params['end'];
111 }
112
113 $startFrom = null;
114 if ( $params['continue'] !== null ) {
115 $cont = explode( '|', $params['continue'] );
116 $this->dieContinueUsageIf( count( $cont ) != 2 );
117 $continueTimestamp = $cont[0];
118 $continueId = (int)$cont[1];
119 $this->dieContinueUsageIf( $continueId != $cont[1] );
120 $startFrom = [ $continueTimestamp, $continueId ];
121 }
122
123 if ( $wlowner !== $user ) {
124 $options['watchlistOwner'] = $wlowner;
125 $options['watchlistOwnerToken'] = $params['token'];
126 }
127
128 if ( $params['namespace'] !== null ) {
129 $options['namespaceIds'] = $params['namespace'];
130 }
131
132 if ( $params['allrev'] ) {
133 $options['allRevisions'] = true;
134 }
135
136 if ( $params['show'] !== null ) {
137 $show = array_flip( $params['show'] );
138
139 /* Check for conflicting parameters. */
140 if ( $this->showParamsConflicting( $show ) ) {
141 $this->dieWithError( 'apierror-show' );
142 }
143
144 // Check permissions.
145 if ( isset( $show[WatchedItemQueryService::FILTER_PATROLLED] )
146 || isset( $show[WatchedItemQueryService::FILTER_NOT_PATROLLED] )
147 ) {
148 if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) {
149 $this->dieWithError( 'apierror-permissiondenied-patrolflag', 'permissiondenied' );
150 }
151 }
152
153 $options['filters'] = array_keys( $show );
154 }
155
156 if ( $params['type'] !== null ) {
157 try {
158 $rcTypes = RecentChange::parseToRCType( $params['type'] );
159 if ( $rcTypes ) {
160 $options['rcTypes'] = $rcTypes;
161 }
162 } catch ( Exception $e ) {
163 ApiBase::dieDebug( __METHOD__, $e->getMessage() );
164 }
165 }
166
167 $this->requireMaxOneParameter( $params, 'user', 'excludeuser' );
168 if ( $params['user'] !== null ) {
169 $options['onlyByUser'] = $params['user'];
170 }
171 if ( $params['excludeuser'] !== null ) {
172 $options['notByUser'] = $params['excludeuser'];
173 }
174
175 $options['limit'] = $params['limit'];
176
177 $this->getHookRunner()->onApiQueryWatchlistPrepareWatchedItemQueryServiceOptions(
178 $this, $params, $options );
179
180 $ids = [];
181 $services = MediaWikiServices::getInstance();
182 $watchedItemQuery = $services->getWatchedItemQueryService();
183 $items = $watchedItemQuery->getWatchedItemsWithRecentChangeInfo( $wlowner, $options, $startFrom );
184
185 // Get gender information
186 if ( $items !== [] && $resultPageSet === null && $this->fld_title &&
187 $services->getContentLanguage()->needsGenderDistinction()
188 ) {
189 $nsInfo = $services->getNamespaceInfo();
190 $usernames = [];
191 foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
193 $linkTarget = $watchedItem->getLinkTarget();
194 if ( $nsInfo->hasGenderDistinction( $linkTarget->getNamespace() ) ) {
195 $usernames[] = $linkTarget->getText();
196 }
197 }
198 if ( $usernames !== [] ) {
199 $services->getGenderCache()->doQuery( $usernames, __METHOD__ );
200 }
201 }
202
203 foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
205 if ( $resultPageSet === null ) {
206 $vals = $this->extractOutputData( $watchedItem, $recentChangeInfo );
207 $fit = $this->getResult()->addValue( [ 'query', $this->getModuleName() ], null, $vals );
208 if ( !$fit ) {
209 $startFrom = [ $recentChangeInfo['rc_timestamp'], $recentChangeInfo['rc_id'] ];
210 break;
211 }
212 } elseif ( $params['allrev'] ) {
213 $ids[] = (int)$recentChangeInfo['rc_this_oldid'];
214 } else {
215 $ids[] = (int)$recentChangeInfo['rc_cur_id'];
216 }
217 }
218
219 if ( $startFrom !== null ) {
220 $this->setContinueEnumParameter( 'continue', implode( '|', $startFrom ) );
221 }
222
223 if ( $resultPageSet === null ) {
224 $this->getResult()->addIndexedTagName(
225 [ 'query', $this->getModuleName() ],
226 'item'
227 );
228 } elseif ( $params['allrev'] ) {
229 $resultPageSet->populateFromRevisionIDs( $ids );
230 } else {
231 $resultPageSet->populateFromPageIDs( $ids );
232 }
233 }
234
235 private function getFieldsToInclude() {
236 $includeFields = [];
237 if ( $this->fld_flags ) {
238 $includeFields[] = WatchedItemQueryService::INCLUDE_FLAGS;
239 }
240 if ( $this->fld_user || $this->fld_userid ) {
241 $includeFields[] = WatchedItemQueryService::INCLUDE_USER_ID;
242 }
243 if ( $this->fld_user ) {
244 $includeFields[] = WatchedItemQueryService::INCLUDE_USER;
245 }
246 if ( $this->fld_comment || $this->fld_parsedcomment ) {
247 $includeFields[] = WatchedItemQueryService::INCLUDE_COMMENT;
248 }
249 if ( $this->fld_patrol ) {
250 $includeFields[] = WatchedItemQueryService::INCLUDE_PATROL_INFO;
251 $includeFields[] = WatchedItemQueryService::INCLUDE_AUTOPATROL_INFO;
252 }
253 if ( $this->fld_sizes ) {
254 $includeFields[] = WatchedItemQueryService::INCLUDE_SIZES;
255 }
256 if ( $this->fld_loginfo ) {
257 $includeFields[] = WatchedItemQueryService::INCLUDE_LOG_INFO;
258 }
259 if ( $this->fld_tags ) {
260 $includeFields[] = WatchedItemQueryService::INCLUDE_TAGS;
261 }
262 return $includeFields;
263 }
264
265 private function showParamsConflicting( array $show ) {
266 return ( isset( $show[WatchedItemQueryService::FILTER_MINOR] )
267 && isset( $show[WatchedItemQueryService::FILTER_NOT_MINOR] ) )
268 || ( isset( $show[WatchedItemQueryService::FILTER_BOT] )
269 && isset( $show[WatchedItemQueryService::FILTER_NOT_BOT] ) )
270 || ( isset( $show[WatchedItemQueryService::FILTER_ANON] )
271 && isset( $show[WatchedItemQueryService::FILTER_NOT_ANON] ) )
272 || ( isset( $show[WatchedItemQueryService::FILTER_PATROLLED] )
273 && isset( $show[WatchedItemQueryService::FILTER_NOT_PATROLLED] ) )
274 || ( isset( $show[WatchedItemQueryService::FILTER_AUTOPATROLLED] )
275 && isset( $show[WatchedItemQueryService::FILTER_NOT_AUTOPATROLLED] ) )
276 || ( isset( $show[WatchedItemQueryService::FILTER_AUTOPATROLLED] )
277 && isset( $show[WatchedItemQueryService::FILTER_NOT_PATROLLED] ) )
278 || ( isset( $show[WatchedItemQueryService::FILTER_UNREAD] )
279 && isset( $show[WatchedItemQueryService::FILTER_NOT_UNREAD] ) );
280 }
281
282 private function extractOutputData( WatchedItem $watchedItem, array $recentChangeInfo ) {
283 /* Determine the title of the page that has been changed. */
284 $title = Title::newFromLinkTarget( $watchedItem->getLinkTarget() );
285 $user = $this->getUser();
286
287 /* Our output data. */
288 $vals = [];
289 $type = (int)$recentChangeInfo['rc_type'];
290 $vals['type'] = RecentChange::parseFromRCType( $type );
291 $anyHidden = false;
292
293 /* Create a new entry in the result for the title. */
294 if ( $this->fld_title || $this->fld_ids ) {
295 // These should already have been filtered out of the query, but just in case.
296 if ( $type === RC_LOG && ( $recentChangeInfo['rc_deleted'] & LogPage::DELETED_ACTION ) ) {
297 $vals['actionhidden'] = true;
298 $anyHidden = true;
299 }
300 if ( $type !== RC_LOG ||
301 LogEventsList::userCanBitfield(
302 $recentChangeInfo['rc_deleted'],
304 $user
305 )
306 ) {
307 if ( $this->fld_title ) {
309 }
310 if ( $this->fld_ids ) {
311 $vals['pageid'] = (int)$recentChangeInfo['rc_cur_id'];
312 $vals['revid'] = (int)$recentChangeInfo['rc_this_oldid'];
313 $vals['old_revid'] = (int)$recentChangeInfo['rc_last_oldid'];
314 }
315 }
316 }
317
318 /* Add user data and 'anon' flag, if user is anonymous. */
319 if ( $this->fld_user || $this->fld_userid ) {
320 if ( $recentChangeInfo['rc_deleted'] & RevisionRecord::DELETED_USER ) {
321 $vals['userhidden'] = true;
322 $anyHidden = true;
323 }
324 if ( RevisionRecord::userCanBitfield(
325 $recentChangeInfo['rc_deleted'],
326 RevisionRecord::DELETED_USER,
327 $user
328 ) ) {
329 if ( $this->fld_userid ) {
330 $vals['userid'] = (int)$recentChangeInfo['rc_user'];
331 // for backwards compatibility
332 $vals['user'] = (int)$recentChangeInfo['rc_user'];
333 }
334
335 if ( $this->fld_user ) {
336 $vals['user'] = $recentChangeInfo['rc_user_text'];
337 }
338
339 if ( !$recentChangeInfo['rc_user'] ) {
340 $vals['anon'] = true;
341 }
342 }
343 }
344
345 /* Add flags, such as new, minor, bot. */
346 if ( $this->fld_flags ) {
347 $vals['bot'] = (bool)$recentChangeInfo['rc_bot'];
348 $vals['new'] = $recentChangeInfo['rc_type'] == RC_NEW;
349 $vals['minor'] = (bool)$recentChangeInfo['rc_minor'];
350 }
351
352 /* Add sizes of each revision. (Only available on 1.10+) */
353 if ( $this->fld_sizes ) {
354 $vals['oldlen'] = (int)$recentChangeInfo['rc_old_len'];
355 $vals['newlen'] = (int)$recentChangeInfo['rc_new_len'];
356 }
357
358 /* Add the timestamp. */
359 if ( $this->fld_timestamp ) {
360 $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $recentChangeInfo['rc_timestamp'] );
361 }
362
363 if ( $this->fld_notificationtimestamp ) {
364 $vals['notificationtimestamp'] = ( $watchedItem->getNotificationTimestamp() == null )
365 ? ''
366 : wfTimestamp( TS_ISO_8601, $watchedItem->getNotificationTimestamp() );
367 }
368
369 /* Add edit summary / log summary. */
370 if ( $this->fld_comment || $this->fld_parsedcomment ) {
371 if ( $recentChangeInfo['rc_deleted'] & RevisionRecord::DELETED_COMMENT ) {
372 $vals['commenthidden'] = true;
373 $anyHidden = true;
374 }
375 if ( RevisionRecord::userCanBitfield(
376 $recentChangeInfo['rc_deleted'],
377 RevisionRecord::DELETED_COMMENT,
378 $user
379 ) ) {
380 $comment = $this->commentStore->getComment( 'rc_comment', $recentChangeInfo )->text;
381 if ( $this->fld_comment ) {
382 $vals['comment'] = $comment;
383 }
384
385 if ( $this->fld_parsedcomment ) {
386 $vals['parsedcomment'] = Linker::formatComment( $comment, $title );
387 }
388 }
389 }
390
391 /* Add the patrolled flag */
392 if ( $this->fld_patrol ) {
393 $vals['patrolled'] = $recentChangeInfo['rc_patrolled'] != RecentChange::PRC_UNPATROLLED;
394 $vals['unpatrolled'] = ChangesList::isUnpatrolled( (object)$recentChangeInfo, $user );
395 $vals['autopatrolled'] = $recentChangeInfo['rc_patrolled'] == RecentChange::PRC_AUTOPATROLLED;
396 }
397
398 if ( $this->fld_loginfo && $recentChangeInfo['rc_type'] == RC_LOG ) {
399 if ( $recentChangeInfo['rc_deleted'] & LogPage::DELETED_ACTION ) {
400 $vals['actionhidden'] = true;
401 $anyHidden = true;
402 }
403 if ( LogEventsList::userCanBitfield(
404 $recentChangeInfo['rc_deleted'],
406 $user
407 ) ) {
408 $vals['logid'] = (int)$recentChangeInfo['rc_logid'];
409 $vals['logtype'] = $recentChangeInfo['rc_log_type'];
410 $vals['logaction'] = $recentChangeInfo['rc_log_action'];
411 $vals['logparams'] = LogFormatter::newFromRow( $recentChangeInfo )->formatParametersForApi();
412 }
413 }
414
415 if ( $this->fld_tags ) {
416 if ( $recentChangeInfo['rc_tags'] ) {
417 $tags = explode( ',', $recentChangeInfo['rc_tags'] );
418 ApiResult::setIndexedTagName( $tags, 'tag' );
419 $vals['tags'] = $tags;
420 } else {
421 $vals['tags'] = [];
422 }
423 }
424
425 if ( $anyHidden && ( $recentChangeInfo['rc_deleted'] & RevisionRecord::DELETED_RESTRICTED ) ) {
426 $vals['suppressed'] = true;
427 }
428
429 $this->getHookRunner()->onApiQueryWatchlistExtractOutputData(
430 $this, $watchedItem, $recentChangeInfo, $vals );
431
432 return $vals;
433 }
434
435 public function getAllowedParams() {
436 return [
437 'allrev' => false,
438 'start' => [
439 ApiBase::PARAM_TYPE => 'timestamp'
440 ],
441 'end' => [
442 ApiBase::PARAM_TYPE => 'timestamp'
443 ],
444 'namespace' => [
446 ApiBase::PARAM_TYPE => 'namespace'
447 ],
448 'user' => [
449 ApiBase::PARAM_TYPE => 'user',
450 UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
451 ],
452 'excludeuser' => [
453 ApiBase::PARAM_TYPE => 'user',
454 UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
455 ],
456 'dir' => [
457 ApiBase::PARAM_DFLT => 'older',
459 'newer',
460 'older'
461 ],
462 ApiHelp::PARAM_HELP_MSG => 'api-help-param-direction',
463 ],
464 'limit' => [
466 ApiBase::PARAM_TYPE => 'limit',
470 ],
471 'prop' => [
473 ApiBase::PARAM_DFLT => 'ids|title|flags',
476 'ids',
477 'title',
478 'flags',
479 'user',
480 'userid',
481 'comment',
482 'parsedcomment',
483 'timestamp',
484 'patrol',
485 'sizes',
486 'notificationtimestamp',
487 'loginfo',
488 'tags',
489 ]
490 ],
491 'show' => [
494 WatchedItemQueryService::FILTER_MINOR,
495 WatchedItemQueryService::FILTER_NOT_MINOR,
496 WatchedItemQueryService::FILTER_BOT,
497 WatchedItemQueryService::FILTER_NOT_BOT,
498 WatchedItemQueryService::FILTER_ANON,
499 WatchedItemQueryService::FILTER_NOT_ANON,
500 WatchedItemQueryService::FILTER_PATROLLED,
501 WatchedItemQueryService::FILTER_NOT_PATROLLED,
502 WatchedItemQueryService::FILTER_AUTOPATROLLED,
503 WatchedItemQueryService::FILTER_NOT_AUTOPATROLLED,
504 WatchedItemQueryService::FILTER_UNREAD,
505 WatchedItemQueryService::FILTER_NOT_UNREAD,
506 ]
507 ],
508 'type' => [
509 ApiBase::PARAM_DFLT => 'edit|new|log|categorize',
512 ApiBase::PARAM_TYPE => RecentChange::getChangeTypes()
513 ],
514 'owner' => [
515 ApiBase::PARAM_TYPE => 'user',
516 UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name' ],
517 ],
518 'token' => [
519 ApiBase::PARAM_TYPE => 'string',
521 ],
522 'continue' => [
523 ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
524 ],
525 ];
526 }
527
528 protected function getExamplesMessages() {
529 return [
530 'action=query&list=watchlist'
531 => 'apihelp-query+watchlist-example-simple',
532 'action=query&list=watchlist&wlprop=ids|title|timestamp|user|comment'
533 => 'apihelp-query+watchlist-example-props',
534 'action=query&list=watchlist&wlallrev=&wlprop=ids|title|timestamp|user|comment'
535 => 'apihelp-query+watchlist-example-allrev',
536 'action=query&generator=watchlist&prop=info'
537 => 'apihelp-query+watchlist-example-generator',
538 'action=query&generator=watchlist&gwlallrev=&prop=revisions&rvprop=timestamp|user'
539 => 'apihelp-query+watchlist-example-generator-rev',
540 'action=query&list=watchlist&wlowner=Example&wltoken=123ABC'
541 => 'apihelp-query+watchlist-example-wlowner',
542 ];
543 }
544
545 public function getHelpUrls() {
546 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Watchlist';
547 }
548}
getUser()
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition ApiBase.php:1437
const PARAM_MAX2
Definition ApiBase.php:86
const PARAM_MAX
Definition ApiBase.php:82
getWatchlistUser( $params)
Gets the user for whom to get the watchlist.
Definition ApiBase.php:1188
dieContinueUsageIf( $condition)
Die with the 'badcontinue' error.
Definition ApiBase.php:1617
static dieDebug( $method, $message)
Internal code errors should be reported with this method.
Definition ApiBase.php:1629
const PARAM_TYPE
Definition ApiBase.php:78
const PARAM_SENSITIVE
Definition ApiBase.php:122
const PARAM_DFLT
Definition ApiBase.php:70
const PARAM_HELP_MSG_PER_VALUE
((string|array|Message)[]) When PARAM_TYPE is an array, this is an array mapping those values to $msg...
Definition ApiBase.php:195
const PARAM_MIN
Definition ApiBase.php:90
const LIMIT_BIG1
Fast query, standard limit.
Definition ApiBase.php:220
requireMaxOneParameter( $params,... $required)
Die if more than one of a certain set of parameters is set and not false.
Definition ApiBase.php:944
getResult()
Get the result object.
Definition ApiBase.php:620
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:772
const PARAM_HELP_MSG
(string|array|Message) Specify an alternative i18n documentation message for this parameter.
Definition ApiBase.php:162
const LIMIT_BIG2
Fast query, apihighlimits limit.
Definition ApiBase.php:222
getModuleName()
Get the name of the module being executed by this instance.
Definition ApiBase.php:499
getHookRunner()
Get an ApiHookRunner for running core API hooks.
Definition ApiBase.php:717
const PARAM_ISMULTI
Definition ApiBase.php:74
selectNamedDB( $name, $db, $groups)
Selects the query database connection with the given name.
static addTitleInfo(&$arr, $title, $prefix='')
Add information (title and namespace) about a Title object to a result array.
setContinueEnumParameter( $paramName, $paramValue)
Overridden to set the generator param if in generator mode.
This query action allows clients to retrieve a list of recently modified pages that are part of the l...
run( $resultPageSet=null)
execute()
Evaluates the parameters, performs the requested query, and sets up the result.
extractOutputData(WatchedItem $watchedItem, array $recentChangeInfo)
showParamsConflicting(array $show)
CommentStore $commentStore
getAllowedParams()
Returns an array of allowed parameters (parameter name) => (default value) or (parameter name) => (ar...
getExamplesMessages()
Returns usage examples for this module.
getHelpUrls()
Return links to more detailed help pages about the module.
executeGenerator( $resultPageSet)
Execute this module as a generator.
__construct(ApiQuery $query, $moduleName)
This is the main query class.
Definition ApiQuery.php:37
CommentStore handles storage of comments (edit summaries, log reasons, etc) in the database.
static formatComment( $comment, $title=null, $local=false, $wikiId=null)
This function is called by all recent changes variants, by the page history, and by the user contribu...
Definition Linker.php:1199
static newFromRow( $row)
Handy shortcut for constructing a formatter directly from database row.
const DELETED_ACTION
Definition LogPage.php:38
MediaWikiServices is the service locator for the application scope of MediaWiki.
Type definition for user types.
Definition UserDef.php:23
Page revision base class.
Representation of a pair of user and title for watchlist entries.
getNotificationTimestamp()
Get the notification timestamp of this entry.
const RC_NEW
Definition Defines.php:133
const RC_LOG
Definition Defines.php:134
const DB_REPLICA
Definition defines.php:25