Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
82.48% covered (warning)
82.48%
353 / 428
50.00% covered (danger)
50.00%
5 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiQueryWatchlist
82.67% covered (warning)
82.67%
353 / 427
50.00% covered (danger)
50.00%
5 / 10
197.20
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.33% covered (success)
91.33%
137 / 150
0.00% covered (danger)
0.00%
0 / 1
44.20
 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%
98 / 98
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
100        $query = $this->changesListQueryFactory->newQuery()
101            ->caller( __METHOD__ )
102            ->watchlistUser( $wlowner );
103
104        if ( $params['prop'] !== null && $resultPageSet === null ) {
105            $prop = array_fill_keys( $params['prop'], true );
106
107            $this->fld_ids = isset( $prop['ids'] );
108            $this->fld_title = isset( $prop['title'] );
109            $this->fld_flags = isset( $prop['flags'] );
110            $this->fld_user = isset( $prop['user'] );
111            $this->fld_userid = isset( $prop['userid'] );
112            $this->fld_comment = isset( $prop['comment'] );
113            $this->fld_parsedcomment = isset( $prop['parsedcomment'] );
114            $this->fld_timestamp = isset( $prop['timestamp'] );
115            $this->fld_sizes = isset( $prop['sizes'] );
116            $this->fld_patrol = isset( $prop['patrol'] );
117            $this->fld_notificationtimestamp = isset( $prop['notificationtimestamp'] );
118            $this->fld_loginfo = isset( $prop['loginfo'] );
119            $this->fld_tags = isset( $prop['tags'] );
120            $this->fld_expiry = isset( $prop['expiry'] );
121            $this->fld_labels = isset( $prop['labels'] );
122
123            // Check if watchlist labels are enabled
124            $this->watchlistLabelsEnabled = $this->getConfig()->get( MainConfigNames::EnableWatchlistLabels );
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['allrev'] ) {
185            $query->excludeOldRevisions();
186        }
187
188        $watchTypes = [ 'watchedold', 'watchednew' ];
189        if ( $params['show'] !== null ) {
190            $show = array_fill_keys( $params['show'], true );
191
192            /* Check for conflicting parameters. */
193            if ( $this->showParamsConflicting( $show ) ) {
194                $this->dieWithError( 'apierror-show' );
195            }
196
197            // Check permissions.
198            if ( isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) ) {
199                if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) {
200                    $this->dieWithError( 'apierror-permissiondenied-patrolflag', 'permissiondenied' );
201                }
202            }
203
204            $showActions = [
205                'minor' => [ 'require', 'minor', true ],
206                '!minor' => [ 'exclude', 'minor', true ],
207                'bot' => [ 'require', 'bot', true ],
208                '!bot' => [ 'exclude', 'bot', true ],
209                'anon' => [ 'exclude', 'named' ],
210                '!anon' => [ 'require', 'named' ],
211                'patrolled' => [ 'exclude', 'patrolled', RecentChange::PRC_UNPATROLLED ],
212                '!patrolled' => [ 'require', 'patrolled', RecentChange::PRC_UNPATROLLED ],
213                'autopatrolled' => [ 'require', 'patrolled', RecentChange::PRC_AUTOPATROLLED ],
214                '!autopatrolled' => [ 'exclude', 'patrolled', RecentChange::PRC_AUTOPATROLLED ],
215            ];
216            foreach ( $show as $name => $unused ) {
217                if ( isset( $showActions[$name] ) ) {
218                    $query->applyAction( ...$showActions[$name] );
219                }
220            }
221
222            if ( isset( $show['unread'] ) ) {
223                $watchTypes = [ 'watchednew' ];
224            }
225            if ( isset( $show['!unread'] ) ) {
226                $watchTypes = [ 'watchedold' ];
227            }
228        }
229
230        $query->requireWatched( $watchTypes );
231
232        if ( $params['type'] !== null ) {
233            $sources = $this->recentChangeLookup->convertTypeToSources( $params['type'] );
234            if ( $sources ) {
235                $query->requireSources( $sources );
236            }
237        }
238
239        $this->requireMaxOneParameter( $params, 'user', 'excludeuser' );
240        if ( $params['user'] !== null ) {
241            $query->requireUser( $params['user'] );
242        }
243        if ( $params['excludeuser'] !== null ) {
244            $query->excludeUser( $params['excludeuser'] );
245        }
246
247        // Paranoia: avoid brute force searches (T19342)
248        if ( $params['user'] !== null || $params['excludeuser'] !== null ) {
249            $query->excludeDeletedUser();
250        }
251        $query->excludeDeletedLogAction();
252
253        $query->limit( $params['limit'] + 1 );
254
255        $hookData = [];
256        if ( $this->getHookContainer()->isRegistered( 'ApiQueryBaseBeforeQuery' ) ) {
257            $query->legacyMutator(
258                function ( &$tables, &$fields, &$conds, &$options, &$join_conds ) use ( &$hookData ) {
259                    $this->getHookRunner()->onApiQueryBaseBeforeQuery(
260                        $this, $tables, $fields, $conds,
261                        $options, $join_conds, $hookData );
262                }
263            );
264        }
265
266        $ids = [];
267        $res = $query->fetchResult()->getResultWrapper();
268
269        $this->getHookRunner()->onApiQueryBaseAfterQuery( $this, $res, $hookData );
270
271        // Do batch queries
272        if ( $this->fld_title && $resultPageSet === null ) {
273            $this->executeGenderCacheFromResultWrapper( $res, __METHOD__, 'rc' );
274        }
275        if ( $this->fld_parsedcomment ) {
276            $this->formattedComments = $this->commentFormatter->formatItems(
277                $this->commentFormatter->rows( $res )
278                    ->indexField( 'rc_id' )
279                    ->commentKey( 'rc_comment' )
280                    ->namespaceField( 'rc_namespace' )
281                    ->titleField( 'rc_title' )
282            );
283        }
284
285        $count = 0;
286        foreach ( $res as $row ) {
287            if ( ++$count > $params['limit'] ) {
288                // We've reached the one extra which shows that there are
289                // additional pages to be had. Stop here...
290                $this->setContinueEnumParameter( 'continue', "$row->rc_timestamp|$row->rc_id" );
291                break;
292            }
293
294            if ( $resultPageSet === null ) {
295                $vals = $this->extractOutputData( $row, $wlowner );
296                $this->processRow( $row, $vals, $hookData );
297                $fit = $this->getResult()->addValue( [ 'query', $this->getModuleName() ], null, $vals );
298                if ( !$fit ) {
299                    $this->setContinueEnumParameter( 'continue', "$row->rc_timestamp|$row->rc_id" );
300                    break;
301                }
302            } elseif ( $params['allrev'] ) {
303                $ids[] = (int)$row->rc_this_oldid;
304            } else {
305                $ids[] = (int)$row->rc_cur_id;
306            }
307        }
308
309        if ( $resultPageSet === null ) {
310            $this->getResult()->addIndexedTagName(
311                [ 'query', $this->getModuleName() ],
312                'item'
313            );
314        } elseif ( $params['allrev'] ) {
315            $resultPageSet->populateFromRevisionIDs( $ids );
316        } else {
317            $resultPageSet->populateFromPageIDs( $ids );
318        }
319    }
320
321    private function addFieldsToQuery( ChangesListQuery $query ) {
322        $fields = [];
323        if ( $this->fld_labels && $this->watchlistLabelsEnabled ) {
324            $query->addWatchlistLabelSummaryField();
325        }
326        if ( $this->fld_expiry ) {
327            $query->maybeAddWatchlistExpiryField();
328        }
329        if ( $this->fld_flags ) {
330            $fields[] = 'rc_minor';
331            $fields[] = 'rc_bot';
332        }
333        if ( $this->fld_user || $this->fld_userid || $this->fld_loginfo ) {
334            $query->rcUserFields();
335        }
336        if ( $this->fld_comment || $this->fld_parsedcomment ) {
337            $query->commentFields();
338        }
339        if ( $this->fld_patrol ) {
340            $fields[] = 'rc_patrolled';
341            $fields[] = 'rc_log_type';
342        }
343        if ( $this->fld_sizes ) {
344            $fields[] = 'rc_old_len';
345            $fields[] = 'rc_new_len';
346        }
347        if ( $this->fld_loginfo ) {
348            $fields[] = 'rc_logid';
349            $fields[] = 'rc_log_type';
350            $fields[] = 'rc_log_action';
351            $fields[] = 'rc_params';
352        }
353        if ( $this->fld_tags ) {
354            $query->addChangeTagSummaryField();
355        }
356        $query->fields( $fields );
357    }
358
359    private function showParamsConflicting( array $show ): bool {
360        return ( isset( $show['minor'] ) && isset( $show['!minor'] ) )
361        || ( isset( $show['bot'] ) && isset( $show['!bot'] ) )
362        || ( isset( $show['anon'] ) && isset( $show['!anon'] ) )
363        || ( isset( $show['patrolled'] ) && isset( $show['!patrolled'] ) )
364        || ( isset( $show['autopatrolled'] ) && isset( $show['!autopatrolled'] ) )
365        || ( isset( $show['autopatrolled'] ) && isset( $show['!patrolled'] ) )
366        || ( isset( $show['unread'] ) && isset( $show['!unread'] ) );
367    }
368
369    private function extractOutputData( stdClass $row, User $wlowner ): array {
370        $user = $this->getUser();
371        $title = PageReferenceValue::localReference( (int)$row->rc_namespace, $row->rc_title );
372
373        /* Our output data. */
374        $vals = [];
375        $vals['type'] = $this->recentChangeLookup->convertSourceToType( $row->rc_source );
376        $isLog = $row->rc_source === RecentChange::SRC_LOG;
377        $anyHidden = false;
378
379        /* Create a new entry in the result for the title. */
380        if ( $this->fld_title || $this->fld_ids ) {
381            // These should already have been filtered out of the query, but just in case.
382            if ( $isLog && ( $row->rc_deleted & LogPage::DELETED_ACTION ) ) {
383                $vals['actionhidden'] = true;
384                $anyHidden = true;
385            }
386            if ( !$isLog ||
387                LogEventsList::userCanBitfield(
388                    $row->rc_deleted,
389                    LogPage::DELETED_ACTION,
390                    $user
391                )
392            ) {
393                if ( $this->fld_title ) {
394                    $vals['ns'] = $title->getNamespace();
395                    $vals['title'] = $this->titleFormatter->getPrefixedText( $title );
396                }
397                if ( $this->fld_ids ) {
398                    $vals['pageid'] = (int)$row->rc_cur_id;
399                    $vals['revid'] = (int)$row->rc_this_oldid;
400                    $vals['old_revid'] = (int)$row->rc_last_oldid;
401                }
402            }
403        }
404
405        if ( $this->fld_user || $this->fld_userid ) {
406            if ( $row->rc_deleted & RevisionRecord::DELETED_USER ) {
407                $vals['userhidden'] = true;
408                $anyHidden = true;
409            }
410            if ( RevisionRecord::userCanBitfield(
411                $row->rc_deleted,
412                RevisionRecord::DELETED_USER,
413                $user
414            ) ) {
415                if ( $this->fld_userid ) {
416                    $vals['userid'] = (int)$row->rc_user;
417                    // for backwards compatibility
418                    $vals['user'] = (int)$row->rc_user;
419                }
420
421                if ( $this->fld_user ) {
422                    $vals['user'] = $row->rc_user_text;
423                    $vals['temp'] = $this->tempUserConfig->isTempName(
424                        $row->rc_user_text
425                    );
426                }
427
428                // Whether the user is a logged-out user (IP user). This does
429                // not include temporary users, though they are grouped with IP
430                // users for FILTER_NOT_ANON and FILTER_ANON, to match the
431                // recent changes filters (T343322).
432                $vals['anon'] = !$row->rc_user;
433            }
434        }
435
436        /* Add flags, such as new, minor, bot. */
437        if ( $this->fld_flags ) {
438            $vals['bot'] = (bool)$row->rc_bot;
439            $vals['new'] = $row->rc_source == RecentChange::SRC_NEW;
440            $vals['minor'] = (bool)$row->rc_minor;
441        }
442
443        /* Add sizes of each revision. */
444        if ( $this->fld_sizes ) {
445            $vals['oldlen'] = (int)$row->rc_old_len;
446            $vals['newlen'] = (int)$row->rc_new_len;
447        }
448
449        /* Add the timestamp. */
450        if ( $this->fld_timestamp ) {
451            $vals['timestamp'] = wfTimestamp( TS::ISO_8601, $row->rc_timestamp );
452        }
453
454        if ( $this->fld_notificationtimestamp ) {
455            $vals['notificationtimestamp'] = ( $row->wl_notificationtimestamp == null )
456                ? ''
457                : wfTimestamp( TS::ISO_8601, $row->wl_notificationtimestamp );
458        }
459
460        /* Add edit summary / log summary. */
461        if ( $this->fld_comment || $this->fld_parsedcomment ) {
462            if ( $row->rc_deleted & RevisionRecord::DELETED_COMMENT ) {
463                $vals['commenthidden'] = true;
464                $anyHidden = true;
465            }
466            if ( RevisionRecord::userCanBitfield(
467                $row->rc_deleted,
468                RevisionRecord::DELETED_COMMENT,
469                $user
470            ) ) {
471                $comment = $this->commentStore->getComment( 'rc_comment', $row )->text;
472                if ( $this->fld_comment ) {
473                    $vals['comment'] = $comment;
474                }
475
476                if ( $this->fld_parsedcomment ) {
477                    $vals['parsedcomment'] = $this->formattedComments[$row->rc_id];
478                }
479            }
480        }
481
482        /* Add the patrolled flag */
483        if ( $this->fld_patrol ) {
484            $vals['patrolled'] = $row->rc_patrolled != RecentChange::PRC_UNPATROLLED;
485            $vals['unpatrolled'] = ChangesList::isUnpatrolled( $row, $user );
486            $vals['autopatrolled'] = $row->rc_patrolled == RecentChange::PRC_AUTOPATROLLED;
487        }
488
489        if ( $this->fld_loginfo && $row->rc_source == RecentChange::SRC_LOG ) {
490            if ( $row->rc_deleted & LogPage::DELETED_ACTION ) {
491                $vals['actionhidden'] = true;
492                $anyHidden = true;
493            }
494            if ( LogEventsList::userCanBitfield(
495                $row->rc_deleted,
496                LogPage::DELETED_ACTION,
497                $user
498            ) ) {
499                $vals['logid'] = (int)$row->rc_logid;
500                $vals['logtype'] = $row->rc_log_type;
501                $vals['logaction'] = $row->rc_log_action;
502
503                $logFormatter = $this->logFormatterFactory->newFromRow( $row );
504                $vals['logparams'] = $logFormatter->formatParametersForApi();
505                $vals['logdisplay'] = $logFormatter->getActionText();
506            }
507        }
508
509        if ( $this->fld_tags ) {
510            if ( $row->ts_tags ) {
511                $tags = explode( ',', $row->ts_tags );
512                ApiResult::setIndexedTagName( $tags, 'tag' );
513                $vals['tags'] = $tags;
514            } else {
515                $vals['tags'] = [];
516            }
517        }
518
519        // T416154
520        if ( $this->fld_labels && $this->watchlistLabelsEnabled ) {
521            if ( isset( $row->wlm_label_summary ) && $row->wlm_label_summary ) {
522                $labelIds = array_map( 'intval', explode( ',', $row->wlm_label_summary ) );
523                $labelArray = [];
524                foreach ( $labelIds as $labelId ) {
525                    if ( isset( $this->userWatchlistLabels[$labelId] ) ) {
526                        $label = $this->userWatchlistLabels[$labelId];
527                        $labelArray[] = [
528                            'id' => $label->getId(),
529                            'name' => $label->getName(),
530                        ];
531                    }
532                }
533                ApiResult::setArrayType( $labelArray, 'array' );
534                ApiResult::setIndexedTagName( $labelArray, 'label' );
535                $vals['labels'] = $labelArray;
536            } else {
537                $vals['labels'] = [];
538            }
539        }
540
541        if ( $this->fld_expiry ) {
542            // Add expiration, T263796
543            $expiryString = $row->we_expiry ?? null;
544            if ( $expiryString ) {
545                $vals['expiry'] = wfTimestamp( TS::ISO_8601, $expiryString );
546            } else {
547                $vals['expiry'] = false;
548            }
549        }
550
551        if ( $anyHidden && ( $row->rc_deleted & RevisionRecord::DELETED_RESTRICTED ) ) {
552            $vals['suppressed'] = true;
553        }
554
555        if ( $this->getHookContainer()->isRegistered( 'ApiQueryWatchlistExtractOutputData' ) ) {
556            $watchedItem = new WatchedItem(
557                $wlowner,
558                $title,
559                $row->wl_notificationtimestamp,
560                $row->we_expiry ?? null
561            );
562            $recentChangeInfo = (array)$row;
563            $this->getHookRunner()->onApiQueryWatchlistExtractOutputData(
564                $this, $watchedItem, $recentChangeInfo, $vals );
565        }
566
567        return $vals;
568    }
569
570    /** @inheritDoc */
571    public function getAllowedParams() {
572        return [
573            'allrev' => false,
574            'start' => [
575                ParamValidator::PARAM_TYPE => 'timestamp'
576            ],
577            'end' => [
578                ParamValidator::PARAM_TYPE => 'timestamp'
579            ],
580            'namespace' => [
581                ParamValidator::PARAM_ISMULTI => true,
582                ParamValidator::PARAM_TYPE => 'namespace'
583            ],
584            'user' => [
585                ParamValidator::PARAM_TYPE => 'user',
586                UserDef::PARAM_RETURN_OBJECT => true,
587                UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'id', 'interwiki' ],
588            ],
589            'excludeuser' => [
590                ParamValidator::PARAM_TYPE => 'user',
591                UserDef::PARAM_RETURN_OBJECT => true,
592                UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'id', 'interwiki' ],
593            ],
594            'dir' => [
595                ParamValidator::PARAM_DEFAULT => 'older',
596                ParamValidator::PARAM_TYPE => [
597                    'newer',
598                    'older'
599                ],
600                ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
601                ApiBase::PARAM_HELP_MSG_PER_VALUE => [
602                    'newer' => 'api-help-paramvalue-direction-newer',
603                    'older' => 'api-help-paramvalue-direction-older',
604                ],
605            ],
606            'limit' => [
607                ParamValidator::PARAM_DEFAULT => 10,
608                ParamValidator::PARAM_TYPE => 'limit',
609                IntegerDef::PARAM_MIN => 1,
610                IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1,
611                IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2
612            ],
613            'prop' => [
614                ParamValidator::PARAM_ISMULTI => true,
615                ParamValidator::PARAM_DEFAULT => 'ids|title|flags',
616                ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
617                ParamValidator::PARAM_TYPE => [
618                    'ids',
619                    'title',
620                    'flags',
621                    'user',
622                    'userid',
623                    'comment',
624                    'parsedcomment',
625                    'timestamp',
626                    'patrol',
627                    'sizes',
628                    'notificationtimestamp',
629                    'loginfo',
630                    'tags',
631                    'expiry',
632                    'labels',
633                ]
634            ],
635            'show' => [
636                ParamValidator::PARAM_ISMULTI => true,
637                ParamValidator::PARAM_TYPE => [
638                    'minor',
639                    '!minor',
640                    'bot',
641                    '!bot',
642                    'anon',
643                    '!anon',
644                    'patrolled',
645                    '!patrolled',
646                    'autopatrolled',
647                    '!autopatrolled',
648                    'unread',
649                    '!unread',
650                ]
651            ],
652            'type' => [
653                ParamValidator::PARAM_DEFAULT => 'edit|new|log|categorize',
654                ParamValidator::PARAM_ISMULTI => true,
655                ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
656                ParamValidator::PARAM_TYPE => RecentChange::getChangeTypes()
657            ],
658            'owner' => [
659                ParamValidator::PARAM_TYPE => 'user',
660                UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name' ],
661            ],
662            'token' => [
663                ParamValidator::PARAM_TYPE => 'string',
664                ParamValidator::PARAM_SENSITIVE => true,
665            ],
666            'continue' => [
667                ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
668            ],
669        ];
670    }
671
672    /** @inheritDoc */
673    protected function getExamplesMessages() {
674        return [
675            'action=query&list=watchlist'
676                => 'apihelp-query+watchlist-example-simple',
677            'action=query&list=watchlist&wlprop=ids|title|timestamp|user|comment'
678                => 'apihelp-query+watchlist-example-props',
679            'action=query&list=watchlist&wlprop=ids|title|timestamp|user|comment|expiry'
680                => 'apihelp-query+watchlist-example-expiry',
681            'action=query&list=watchlist&wlallrev=&wlprop=ids|title|timestamp|user|comment'
682                => 'apihelp-query+watchlist-example-allrev',
683            'action=query&generator=watchlist&prop=info'
684                => 'apihelp-query+watchlist-example-generator',
685            'action=query&generator=watchlist&gwlallrev=&prop=revisions&rvprop=timestamp|user'
686                => 'apihelp-query+watchlist-example-generator-rev',
687            'action=query&list=watchlist&wlowner=Example&wltoken=123ABC'
688                => 'apihelp-query+watchlist-example-wlowner',
689        ];
690    }
691
692    /** @inheritDoc */
693    public function getHelpUrls() {
694        return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Watchlist';
695    }
696}
697
698/** @deprecated class alias since 1.43 */
699class_alias( ApiQueryWatchlist::class, 'ApiQueryWatchlist' );