Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
82.76% covered (warning)
82.76%
360 / 435
50.00% covered (danger)
50.00%
5 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiQueryWatchlist
82.95% covered (warning)
82.95%
360 / 434
50.00% covered (danger)
50.00%
5 / 10
198.00
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 execute
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 executeGenerator
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 run
91.50% covered (success)
91.50%
140 / 153
0.00% covered (danger)
0.00%
0 / 1
46.24
 addFieldsToQuery
88.46% covered (warning)
88.46%
23 / 26
0.00% covered (danger)
0.00%
0 / 1
14.30
 showParamsConflicting
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
14
 extractOutputData
67.46% covered (warning)
67.46%
85 / 126
0.00% covered (danger)
0.00%
0 / 1
110.70
 getAllowedParams
100.00% covered (success)
100.00%
102 / 102
100.00% covered (success)
100.00%
1 / 1
1
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
2
 getHelpUrls
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
4 *
5 * @license GPL-2.0-or-later
6 * @file
7 */
8
9namespace MediaWiki\Api;
10
11use MediaWiki\CommentFormatter\RowCommentFormatter;
12use MediaWiki\CommentStore\CommentStore;
13use MediaWiki\Logging\LogEventsList;
14use MediaWiki\Logging\LogFormatterFactory;
15use MediaWiki\Logging\LogPage;
16use Mediawiki\MainConfigNames;
17use MediaWiki\Page\PageReferenceValue;
18use MediaWiki\ParamValidator\TypeDef\UserDef;
19use MediaWiki\RecentChanges\ChangesList;
20use MediaWiki\RecentChanges\ChangesListQuery\ChangesListQuery;
21use MediaWiki\RecentChanges\ChangesListQuery\ChangesListQueryFactory;
22use MediaWiki\RecentChanges\RecentChange;
23use MediaWiki\RecentChanges\RecentChangeLookup;
24use MediaWiki\Revision\RevisionRecord;
25use MediaWiki\Title\TitleFormatter;
26use MediaWiki\User\TempUser\TempUserConfig;
27use MediaWiki\User\User;
28use MediaWiki\Watchlist\WatchedItem;
29use MediaWiki\Watchlist\WatchlistLabelStore;
30use stdClass;
31use Wikimedia\ParamValidator\ParamValidator;
32use Wikimedia\ParamValidator\TypeDef\IntegerDef;
33use Wikimedia\Timestamp\TimestampFormat as TS;
34
35/**
36 * This query action allows clients to retrieve a list of recently modified pages
37 * that are part of the logged-in user's watchlist.
38 *
39 * TODO: Factor out a common base class with ApiQueryRecentChanges
40 *
41 * @ingroup API
42 */
43class ApiQueryWatchlist extends ApiQueryGeneratorBase {
44
45    /** @var string[] */
46    private $formattedComments = [];
47
48    public function __construct(
49        ApiQuery $query,
50        string $moduleName,
51        private readonly CommentStore $commentStore,
52        private readonly ChangesListQueryFactory $changesListQueryFactory,
53        private readonly RowCommentFormatter $commentFormatter,
54        private readonly TempUserConfig $tempUserConfig,
55        private readonly LogFormatterFactory $logFormatterFactory,
56        private readonly RecentChangeLookup $recentChangeLookup,
57        private readonly TitleFormatter $titleFormatter,
58        private readonly WatchlistLabelStore $watchlistLabelStore,
59    ) {
60        parent::__construct( $query, $moduleName, 'wl' );
61    }
62
63    public function execute() {
64        $this->run();
65    }
66
67    /** @inheritDoc */
68    public function executeGenerator( $resultPageSet ) {
69        $this->run( $resultPageSet );
70    }
71
72    private bool $fld_ids = false;
73    private bool $fld_title = false;
74    private bool $fld_patrol = false;
75    private bool $fld_flags = false;
76    private bool $fld_timestamp = false;
77    private bool $fld_user = false;
78    private bool $fld_comment = false;
79    private bool $fld_parsedcomment = false;
80    private bool $fld_sizes = false;
81    private bool $fld_notificationtimestamp = false;
82    private bool $fld_userid = false;
83    private bool $fld_loginfo = false;
84    private bool $fld_tags = false;
85    private bool $fld_expiry = false;
86    private bool $fld_labels = false;
87    private bool $watchlistLabelsEnabled = false;
88    private array $userWatchlistLabels = [];
89
90    /**
91     * @param ApiPageSet|null $resultPageSet
92     * @return void
93     */
94    private function run( $resultPageSet = null ) {
95        $params = $this->extractRequestParams();
96
97        $user = $this->getUser();
98        $wlowner = $this->getWatchlistUser( $params );
99        // Check if watchlist labels are enabled
100        $this->watchlistLabelsEnabled = $this->getConfig()->get( MainConfigNames::EnableWatchlistLabels );
101
102        $query = $this->changesListQueryFactory->newQuery()
103            ->caller( __METHOD__ )
104            ->watchlistUser( $wlowner )
105            ->audience( $this->getAuthority() );
106
107        if ( $params['prop'] !== null && $resultPageSet === null ) {
108            $prop = array_fill_keys( $params['prop'], true );
109
110            $this->fld_ids = isset( $prop['ids'] );
111            $this->fld_title = isset( $prop['title'] );
112            $this->fld_flags = isset( $prop['flags'] );
113            $this->fld_user = isset( $prop['user'] );
114            $this->fld_userid = isset( $prop['userid'] );
115            $this->fld_comment = isset( $prop['comment'] );
116            $this->fld_parsedcomment = isset( $prop['parsedcomment'] );
117            $this->fld_timestamp = isset( $prop['timestamp'] );
118            $this->fld_sizes = isset( $prop['sizes'] );
119            $this->fld_patrol = isset( $prop['patrol'] );
120            $this->fld_notificationtimestamp = isset( $prop['notificationtimestamp'] );
121            $this->fld_loginfo = isset( $prop['loginfo'] );
122            $this->fld_tags = isset( $prop['tags'] );
123            $this->fld_expiry = isset( $prop['expiry'] );
124            $this->fld_labels = isset( $prop['labels'] );
125
126            if ( $this->fld_patrol && !$user->useRCPatrol() && !$user->useNPPatrol() ) {
127                $this->dieWithError( 'apierror-permissiondenied-patrolflag', 'patrol' );
128            }
129        }
130
131        // Get user's watchlist labels in advance
132        if ( $this->fld_labels && $this->watchlistLabelsEnabled ) {
133            $this->userWatchlistLabels = $this->watchlistLabelStore->loadAllForUser( $wlowner );
134        }
135
136        $query->orderBy(
137            $params['dir'] === 'older'
138            ? ChangesListQuery::SORT_TIMESTAMP_DESC
139            : ChangesListQuery::SORT_TIMESTAMP_ASC
140        );
141
142        $query->fields( [
143            'rc_id',
144            'rc_namespace',
145            'rc_title',
146            'rc_timestamp',
147            'rc_source',
148            'rc_deleted',
149            'wl_notificationtimestamp'
150        ] );
151
152        $rcIdFields = [
153            'rc_cur_id',
154            'rc_this_oldid',
155            'rc_last_oldid',
156        ];
157        if ( $resultPageSet !== null ) {
158            if ( $params['allrev'] ) {
159                $rcIdFields = [ 'rc_this_oldid' ];
160            } else {
161                $rcIdFields = [ 'rc_cur_id' ];
162            }
163        } else {
164            $this->addFieldsToQuery( $query );
165        }
166        $query->fields( $rcIdFields );
167
168        if ( $params['start'] ) {
169            $query->startAt( $params['start'] );
170        }
171        if ( $params['end'] ) {
172            $query->endAt( $params['end'] );
173        }
174
175        if ( $params['continue'] !== null ) {
176            $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'string', 'int' ] );
177            $query->startAt( $cont[0], $cont[1] );
178        }
179
180        if ( $params['namespace'] !== null ) {
181            $query->requireNamespaces( $params['namespace'] );
182        }
183
184        if ( $params['labels'] !== null && $this->watchlistLabelsEnabled ) {
185            $query->requireWatchlistLabelIds( $params['labels'] );
186        }
187
188        if ( !$params['allrev'] ) {
189            $query->excludeOldRevisions();
190        }
191
192        $watchTypes = [ 'watchedold', 'watchednew' ];
193        if ( $params['show'] !== null ) {
194            $show = array_fill_keys( $params['show'], true );
195
196            /* Check for conflicting parameters. */
197            if ( $this->showParamsConflicting( $show ) ) {
198                $this->dieWithError( 'apierror-show' );
199            }
200
201            // Check permissions.
202            if ( isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) ) {
203                if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) {
204                    $this->dieWithError( 'apierror-permissiondenied-patrolflag', 'permissiondenied' );
205                }
206            }
207
208            $showActions = [
209                'minor' => [ 'require', 'minor', true ],
210                '!minor' => [ 'exclude', 'minor', true ],
211                'bot' => [ 'require', 'bot', true ],
212                '!bot' => [ 'exclude', 'bot', true ],
213                'anon' => [ 'exclude', 'named' ],
214                '!anon' => [ 'require', 'named' ],
215                'patrolled' => [ 'exclude', 'patrolled', RecentChange::PRC_UNPATROLLED ],
216                '!patrolled' => [ 'require', 'patrolled', RecentChange::PRC_UNPATROLLED ],
217                'autopatrolled' => [ 'require', 'patrolled', RecentChange::PRC_AUTOPATROLLED ],
218                '!autopatrolled' => [ 'exclude', 'patrolled', RecentChange::PRC_AUTOPATROLLED ],
219            ];
220            foreach ( $show as $name => $unused ) {
221                if ( isset( $showActions[$name] ) ) {
222                    $query->applyAction( ...$showActions[$name] );
223                }
224            }
225
226            if ( isset( $show['unread'] ) ) {
227                $watchTypes = [ 'watchednew' ];
228            }
229            if ( isset( $show['!unread'] ) ) {
230                $watchTypes = [ 'watchedold' ];
231            }
232        }
233
234        $query->requireWatched( $watchTypes );
235
236        if ( $params['type'] !== null ) {
237            $sources = $this->recentChangeLookup->convertTypeToSources( $params['type'] );
238            if ( $sources ) {
239                $query->requireSources( $sources );
240            }
241        }
242
243        $this->requireMaxOneParameter( $params, 'user', 'excludeuser' );
244        if ( $params['user'] !== null ) {
245            $query->requireUser( $params['user'] );
246        }
247        if ( $params['excludeuser'] !== null ) {
248            $query->excludeUser( $params['excludeuser'] );
249        }
250
251        // Paranoia: avoid brute force searches (T19342)
252        if ( $params['user'] !== null || $params['excludeuser'] !== null ) {
253            $query->excludeDeletedUser();
254        }
255        $query->excludeDeletedLogAction();
256
257        $query->limit( $params['limit'] + 1 );
258
259        $hookData = [];
260        if ( $this->getHookContainer()->isRegistered( 'ApiQueryBaseBeforeQuery' ) ) {
261            $query->legacyMutator(
262                function ( &$tables, &$fields, &$conds, &$options, &$join_conds ) use ( &$hookData ) {
263                    $this->getHookRunner()->onApiQueryBaseBeforeQuery(
264                        $this, $tables, $fields, $conds,
265                        $options, $join_conds, $hookData );
266                }
267            );
268        }
269
270        $ids = [];
271        $res = $query->fetchResult()->getResultWrapper();
272
273        $this->getHookRunner()->onApiQueryBaseAfterQuery( $this, $res, $hookData );
274
275        // Do batch queries
276        if ( $this->fld_title && $resultPageSet === null ) {
277            $this->executeGenderCacheFromResultWrapper( $res, __METHOD__, 'rc' );
278        }
279        if ( $this->fld_parsedcomment ) {
280            $this->formattedComments = $this->commentFormatter->formatItems(
281                $this->commentFormatter->rows( $res )
282                    ->indexField( 'rc_id' )
283                    ->commentKey( 'rc_comment' )
284                    ->namespaceField( 'rc_namespace' )
285                    ->titleField( 'rc_title' )
286            );
287        }
288
289        $count = 0;
290        foreach ( $res as $row ) {
291            if ( ++$count > $params['limit'] ) {
292                // We've reached the one extra which shows that there are
293                // additional pages to be had. Stop here...
294                $this->setContinueEnumParameter( 'continue', "$row->rc_timestamp|$row->rc_id" );
295                break;
296            }
297
298            if ( $resultPageSet === null ) {
299                $vals = $this->extractOutputData( $row, $wlowner );
300                $this->processRow( $row, $vals, $hookData );
301                $fit = $this->getResult()->addValue( [ 'query', $this->getModuleName() ], null, $vals );
302                if ( !$fit ) {
303                    $this->setContinueEnumParameter( 'continue', "$row->rc_timestamp|$row->rc_id" );
304                    break;
305                }
306            } elseif ( $params['allrev'] ) {
307                $ids[] = (int)$row->rc_this_oldid;
308            } else {
309                $ids[] = (int)$row->rc_cur_id;
310            }
311        }
312
313        if ( $resultPageSet === null ) {
314            $this->getResult()->addIndexedTagName(
315                [ 'query', $this->getModuleName() ],
316                'item'
317            );
318        } elseif ( $params['allrev'] ) {
319            $resultPageSet->populateFromRevisionIDs( $ids );
320        } else {
321            $resultPageSet->populateFromPageIDs( $ids );
322        }
323    }
324
325    private function addFieldsToQuery( ChangesListQuery $query ) {
326        $fields = [];
327        if ( $this->fld_labels && $this->watchlistLabelsEnabled ) {
328            $query->addWatchlistLabelSummaryField();
329        }
330        if ( $this->fld_expiry ) {
331            $query->maybeAddWatchlistExpiryField();
332        }
333        if ( $this->fld_flags ) {
334            $fields[] = 'rc_minor';
335            $fields[] = 'rc_bot';
336        }
337        if ( $this->fld_user || $this->fld_userid || $this->fld_loginfo ) {
338            $query->rcUserFields();
339        }
340        if ( $this->fld_comment || $this->fld_parsedcomment ) {
341            $query->commentFields();
342        }
343        if ( $this->fld_patrol ) {
344            $fields[] = 'rc_patrolled';
345            $fields[] = 'rc_log_type';
346        }
347        if ( $this->fld_sizes ) {
348            $fields[] = 'rc_old_len';
349            $fields[] = 'rc_new_len';
350        }
351        if ( $this->fld_loginfo ) {
352            $fields[] = 'rc_logid';
353            $fields[] = 'rc_log_type';
354            $fields[] = 'rc_log_action';
355            $fields[] = 'rc_params';
356        }
357        if ( $this->fld_tags ) {
358            $query->addChangeTagSummaryField();
359        }
360        $query->fields( $fields );
361    }
362
363    private function showParamsConflicting( array $show ): bool {
364        return ( isset( $show['minor'] ) && isset( $show['!minor'] ) )
365        || ( isset( $show['bot'] ) && isset( $show['!bot'] ) )
366        || ( isset( $show['anon'] ) && isset( $show['!anon'] ) )
367        || ( isset( $show['patrolled'] ) && isset( $show['!patrolled'] ) )
368        || ( isset( $show['autopatrolled'] ) && isset( $show['!autopatrolled'] ) )
369        || ( isset( $show['autopatrolled'] ) && isset( $show['!patrolled'] ) )
370        || ( isset( $show['unread'] ) && isset( $show['!unread'] ) );
371    }
372
373    private function extractOutputData( stdClass $row, User $wlowner ): array {
374        $user = $this->getUser();
375        $title = PageReferenceValue::localReference( (int)$row->rc_namespace, $row->rc_title );
376
377        /* Our output data. */
378        $vals = [];
379        $vals['type'] = $this->recentChangeLookup->convertSourceToType( $row->rc_source );
380        $isLog = $row->rc_source === RecentChange::SRC_LOG;
381        $anyHidden = false;
382
383        /* Create a new entry in the result for the title. */
384        if ( $this->fld_title || $this->fld_ids ) {
385            // These should already have been filtered out of the query, but just in case.
386            if ( $isLog && ( $row->rc_deleted & LogPage::DELETED_ACTION ) ) {
387                $vals['actionhidden'] = true;
388                $anyHidden = true;
389            }
390            if ( !$isLog ||
391                LogEventsList::userCanBitfield(
392                    $row->rc_deleted,
393                    LogPage::DELETED_ACTION,
394                    $user
395                )
396            ) {
397                if ( $this->fld_title ) {
398                    $vals['ns'] = $title->getNamespace();
399                    $vals['title'] = $this->titleFormatter->getPrefixedText( $title );
400                }
401                if ( $this->fld_ids ) {
402                    $vals['pageid'] = (int)$row->rc_cur_id;
403                    $vals['revid'] = (int)$row->rc_this_oldid;
404                    $vals['old_revid'] = (int)$row->rc_last_oldid;
405                }
406            }
407        }
408
409        if ( $this->fld_user || $this->fld_userid ) {
410            if ( $row->rc_deleted & RevisionRecord::DELETED_USER ) {
411                $vals['userhidden'] = true;
412                $anyHidden = true;
413            }
414            if ( RevisionRecord::userCanBitfield(
415                $row->rc_deleted,
416                RevisionRecord::DELETED_USER,
417                $user
418            ) ) {
419                if ( $this->fld_userid ) {
420                    $vals['userid'] = (int)$row->rc_user;
421                    // for backwards compatibility
422                    $vals['user'] = (int)$row->rc_user;
423                }
424
425                if ( $this->fld_user ) {
426                    $vals['user'] = $row->rc_user_text;
427                    $vals['temp'] = $this->tempUserConfig->isTempName(
428                        $row->rc_user_text
429                    );
430                }
431
432                // Whether the user is a logged-out user (IP user). This does
433                // not include temporary users, though they are grouped with IP
434                // users for FILTER_NOT_ANON and FILTER_ANON, to match the
435                // recent changes filters (T343322).
436                $vals['anon'] = !$row->rc_user;
437            }
438        }
439
440        /* Add flags, such as new, minor, bot. */
441        if ( $this->fld_flags ) {
442            $vals['bot'] = (bool)$row->rc_bot;
443            $vals['new'] = $row->rc_source == RecentChange::SRC_NEW;
444            $vals['minor'] = (bool)$row->rc_minor;
445        }
446
447        /* Add sizes of each revision. */
448        if ( $this->fld_sizes ) {
449            $vals['oldlen'] = (int)$row->rc_old_len;
450            $vals['newlen'] = (int)$row->rc_new_len;
451        }
452
453        /* Add the timestamp. */
454        if ( $this->fld_timestamp ) {
455            $vals['timestamp'] = wfTimestamp( TS::ISO_8601, $row->rc_timestamp );
456        }
457
458        if ( $this->fld_notificationtimestamp ) {
459            $vals['notificationtimestamp'] = ( $row->wl_notificationtimestamp == null )
460                ? ''
461                : wfTimestamp( TS::ISO_8601, $row->wl_notificationtimestamp );
462        }
463
464        /* Add edit summary / log summary. */
465        if ( $this->fld_comment || $this->fld_parsedcomment ) {
466            if ( $row->rc_deleted & RevisionRecord::DELETED_COMMENT ) {
467                $vals['commenthidden'] = true;
468                $anyHidden = true;
469            }
470            if ( RevisionRecord::userCanBitfield(
471                $row->rc_deleted,
472                RevisionRecord::DELETED_COMMENT,
473                $user
474            ) ) {
475                $comment = $this->commentStore->getComment( 'rc_comment', $row )->text;
476                if ( $this->fld_comment ) {
477                    $vals['comment'] = $comment;
478                }
479
480                if ( $this->fld_parsedcomment ) {
481                    $vals['parsedcomment'] = $this->formattedComments[$row->rc_id];
482                }
483            }
484        }
485
486        /* Add the patrolled flag */
487        if ( $this->fld_patrol ) {
488            $vals['patrolled'] = $row->rc_patrolled != RecentChange::PRC_UNPATROLLED;
489            $vals['unpatrolled'] = ChangesList::isUnpatrolled( $row, $user );
490            $vals['autopatrolled'] = $row->rc_patrolled == RecentChange::PRC_AUTOPATROLLED;
491        }
492
493        if ( $this->fld_loginfo && $row->rc_source == RecentChange::SRC_LOG ) {
494            if ( $row->rc_deleted & LogPage::DELETED_ACTION ) {
495                $vals['actionhidden'] = true;
496                $anyHidden = true;
497            }
498            if ( LogEventsList::userCanBitfield(
499                $row->rc_deleted,
500                LogPage::DELETED_ACTION,
501                $user
502            ) ) {
503                $vals['logid'] = (int)$row->rc_logid;
504                $vals['logtype'] = $row->rc_log_type;
505                $vals['logaction'] = $row->rc_log_action;
506
507                $logFormatter = $this->logFormatterFactory->newFromRow( $row );
508                $vals['logparams'] = $logFormatter->formatParametersForApi();
509                $vals['logdisplay'] = $logFormatter->getActionText();
510            }
511        }
512
513        if ( $this->fld_tags ) {
514            if ( $row->ts_tags ) {
515                $tags = explode( ',', $row->ts_tags );
516                ApiResult::setIndexedTagName( $tags, 'tag' );
517                $vals['tags'] = $tags;
518            } else {
519                $vals['tags'] = [];
520            }
521        }
522
523        // T416154
524        if ( $this->fld_labels && $this->watchlistLabelsEnabled ) {
525            if ( isset( $row->wlm_label_summary ) && $row->wlm_label_summary ) {
526                $labelIds = array_map( 'intval', explode( ',', $row->wlm_label_summary ) );
527                $labelArray = [];
528                foreach ( $labelIds as $labelId ) {
529                    if ( isset( $this->userWatchlistLabels[$labelId] ) ) {
530                        $label = $this->userWatchlistLabels[$labelId];
531                        $labelArray[] = [
532                            'id' => $label->getId(),
533                            'name' => $label->getName(),
534                        ];
535                    }
536                }
537                ApiResult::setArrayType( $labelArray, 'array' );
538                ApiResult::setIndexedTagName( $labelArray, 'label' );
539                $vals['labels'] = $labelArray;
540            } else {
541                $vals['labels'] = [];
542            }
543        }
544
545        if ( $this->fld_expiry ) {
546            // Add expiration, T263796
547            $expiryString = $row->we_expiry ?? null;
548            if ( $expiryString ) {
549                $vals['expiry'] = wfTimestamp( TS::ISO_8601, $expiryString );
550            } else {
551                $vals['expiry'] = false;
552            }
553        }
554
555        if ( $anyHidden && ( $row->rc_deleted & RevisionRecord::DELETED_RESTRICTED ) ) {
556            $vals['suppressed'] = true;
557        }
558
559        if ( $this->getHookContainer()->isRegistered( 'ApiQueryWatchlistExtractOutputData' ) ) {
560            $watchedItem = new WatchedItem(
561                $wlowner,
562                $title,
563                $row->wl_notificationtimestamp,
564                $row->we_expiry ?? null
565            );
566            $recentChangeInfo = (array)$row;
567            $this->getHookRunner()->onApiQueryWatchlistExtractOutputData(
568                $this, $watchedItem, $recentChangeInfo, $vals );
569        }
570
571        return $vals;
572    }
573
574    /** @inheritDoc */
575    public function getAllowedParams() {
576        return [
577            'allrev' => false,
578            'start' => [
579                ParamValidator::PARAM_TYPE => 'timestamp'
580            ],
581            'end' => [
582                ParamValidator::PARAM_TYPE => 'timestamp'
583            ],
584            'namespace' => [
585                ParamValidator::PARAM_ISMULTI => true,
586                ParamValidator::PARAM_TYPE => 'namespace'
587            ],
588            'user' => [
589                ParamValidator::PARAM_TYPE => 'user',
590                UserDef::PARAM_RETURN_OBJECT => true,
591                UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'id', 'interwiki' ],
592            ],
593            'excludeuser' => [
594                ParamValidator::PARAM_TYPE => 'user',
595                UserDef::PARAM_RETURN_OBJECT => true,
596                UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'id', 'interwiki' ],
597            ],
598            'dir' => [
599                ParamValidator::PARAM_DEFAULT => 'older',
600                ParamValidator::PARAM_TYPE => [
601                    'newer',
602                    'older'
603                ],
604                ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
605                ApiBase::PARAM_HELP_MSG_PER_VALUE => [
606                    'newer' => 'api-help-paramvalue-direction-newer',
607                    'older' => 'api-help-paramvalue-direction-older',
608                ],
609            ],
610            'limit' => [
611                ParamValidator::PARAM_DEFAULT => 10,
612                ParamValidator::PARAM_TYPE => 'limit',
613                IntegerDef::PARAM_MIN => 1,
614                IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1,
615                IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2
616            ],
617            'prop' => [
618                ParamValidator::PARAM_ISMULTI => true,
619                ParamValidator::PARAM_DEFAULT => 'ids|title|flags',
620                ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
621                ParamValidator::PARAM_TYPE => [
622                    'ids',
623                    'title',
624                    'flags',
625                    'user',
626                    'userid',
627                    'comment',
628                    'parsedcomment',
629                    'timestamp',
630                    'patrol',
631                    'sizes',
632                    'notificationtimestamp',
633                    'loginfo',
634                    'tags',
635                    'expiry',
636                    'labels',
637                ]
638            ],
639            'show' => [
640                ParamValidator::PARAM_ISMULTI => true,
641                ParamValidator::PARAM_TYPE => [
642                    'minor',
643                    '!minor',
644                    'bot',
645                    '!bot',
646                    'anon',
647                    '!anon',
648                    'patrolled',
649                    '!patrolled',
650                    'autopatrolled',
651                    '!autopatrolled',
652                    'unread',
653                    '!unread',
654                ]
655            ],
656            'type' => [
657                ParamValidator::PARAM_DEFAULT => 'edit|new|log|categorize',
658                ParamValidator::PARAM_ISMULTI => true,
659                ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
660                ParamValidator::PARAM_TYPE => RecentChange::getChangeTypes()
661            ],
662            'labels' => [
663                ParamValidator::PARAM_ISMULTI => true,
664                ParamValidator::PARAM_TYPE => 'integer',
665            ],
666            'owner' => [
667                ParamValidator::PARAM_TYPE => 'user',
668                UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name' ],
669            ],
670            'token' => [
671                ParamValidator::PARAM_TYPE => 'string',
672                ParamValidator::PARAM_SENSITIVE => true,
673            ],
674            'continue' => [
675                ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
676            ],
677        ];
678    }
679
680    /** @inheritDoc */
681    protected function getExamplesMessages() {
682        return [
683            'action=query&list=watchlist'
684                => 'apihelp-query+watchlist-example-simple',
685            'action=query&list=watchlist&wlprop=ids|title|timestamp|user|comment'
686                => 'apihelp-query+watchlist-example-props',
687            'action=query&list=watchlist&wlprop=ids|title|timestamp|user|comment|expiry'
688                => 'apihelp-query+watchlist-example-expiry',
689            'action=query&list=watchlist&wlallrev=&wlprop=ids|title|timestamp|user|comment'
690                => 'apihelp-query+watchlist-example-allrev',
691            'action=query&generator=watchlist&prop=info'
692                => 'apihelp-query+watchlist-example-generator',
693            'action=query&generator=watchlist&gwlallrev=&prop=revisions&rvprop=timestamp|user'
694                => 'apihelp-query+watchlist-example-generator-rev',
695            'action=query&list=watchlist&wlowner=Example&wltoken=123ABC'
696                => 'apihelp-query+watchlist-example-wlowner',
697        ];
698    }
699
700    /** @inheritDoc */
701    public function getHelpUrls() {
702        return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Watchlist';
703    }
704}
705
706/** @deprecated class alias since 1.43 */
707class_alias( ApiQueryWatchlist::class, 'ApiQueryWatchlist' );