Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
85.71% covered (warning)
85.71%
354 / 413
50.00% covered (danger)
50.00%
5 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiQueryWatchlist
85.92% covered (warning)
85.92%
354 / 412
50.00% covered (danger)
50.00%
5 / 10
145.37
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
8 / 8
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.78% covered (success)
91.78%
134 / 146
0.00% covered (danger)
0.00%
0 / 1
41.93
 addFieldsToQuery
91.67% covered (success)
91.67%
22 / 24
0.00% covered (danger)
0.00%
0 / 1
12.08
 showParamsConflicting
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
14
 extractOutputData
75.68% covered (warning)
75.68%
84 / 111
0.00% covered (danger)
0.00%
0 / 1
58.78
 getAllowedParams
100.00% covered (success)
100.00%
97 / 97
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\Page\PageReferenceValue;
17use MediaWiki\ParamValidator\TypeDef\UserDef;
18use MediaWiki\RecentChanges\ChangesList;
19use MediaWiki\RecentChanges\ChangesListQuery\ChangesListQuery;
20use MediaWiki\RecentChanges\ChangesListQuery\ChangesListQueryFactory;
21use MediaWiki\RecentChanges\RecentChange;
22use MediaWiki\RecentChanges\RecentChangeLookup;
23use MediaWiki\Revision\RevisionRecord;
24use MediaWiki\Title\TitleFormatter;
25use MediaWiki\User\TempUser\TempUserConfig;
26use MediaWiki\User\User;
27use MediaWiki\Watchlist\WatchedItem;
28use stdClass;
29use Wikimedia\ParamValidator\ParamValidator;
30use Wikimedia\ParamValidator\TypeDef\IntegerDef;
31use Wikimedia\Timestamp\TimestampFormat as TS;
32
33/**
34 * This query action allows clients to retrieve a list of recently modified pages
35 * that are part of the logged-in user's watchlist.
36 *
37 * TODO: Factor out a common base class with ApiQueryRecentChanges
38 *
39 * @ingroup API
40 */
41class ApiQueryWatchlist extends ApiQueryGeneratorBase {
42
43    private CommentStore $commentStore;
44    private RowCommentFormatter $commentFormatter;
45    private TempUserConfig $tempUserConfig;
46    private LogFormatterFactory $logFormatterFactory;
47    private ChangesListQueryFactory $changesListQueryFactory;
48    private RecentChangeLookup $recentChangeLookup;
49    private TitleFormatter $titleFormatter;
50
51    /** @var string[] */
52    private $formattedComments = [];
53
54    public function __construct(
55        ApiQuery $query,
56        string $moduleName,
57        CommentStore $commentStore,
58        ChangesListQueryFactory $changesListQueryFactory,
59        RowCommentFormatter $commentFormatter,
60        TempUserConfig $tempUserConfig,
61        LogFormatterFactory $logFormatterFactory,
62        RecentChangeLookup $recentChangeLookup,
63        TitleFormatter $titleFormatter,
64    ) {
65        parent::__construct( $query, $moduleName, 'wl' );
66        $this->commentStore = $commentStore;
67        $this->changesListQueryFactory = $changesListQueryFactory;
68        $this->commentFormatter = $commentFormatter;
69        $this->tempUserConfig = $tempUserConfig;
70        $this->logFormatterFactory = $logFormatterFactory;
71        $this->recentChangeLookup = $recentChangeLookup;
72        $this->titleFormatter = $titleFormatter;
73    }
74
75    public function execute() {
76        $this->run();
77    }
78
79    /** @inheritDoc */
80    public function executeGenerator( $resultPageSet ) {
81        $this->run( $resultPageSet );
82    }
83
84    private bool $fld_ids = false;
85    private bool $fld_title = false;
86    private bool $fld_patrol = false;
87    private bool $fld_flags = false;
88    private bool $fld_timestamp = false;
89    private bool $fld_user = false;
90    private bool $fld_comment = false;
91    private bool $fld_parsedcomment = false;
92    private bool $fld_sizes = false;
93    private bool $fld_notificationtimestamp = false;
94    private bool $fld_userid = false;
95    private bool $fld_loginfo = false;
96    private bool $fld_tags = false;
97    private bool $fld_expiry = false;
98
99    /**
100     * @param ApiPageSet|null $resultPageSet
101     * @return void
102     */
103    private function run( $resultPageSet = null ) {
104        $params = $this->extractRequestParams();
105
106        $user = $this->getUser();
107        $wlowner = $this->getWatchlistUser( $params );
108
109        $query = $this->changesListQueryFactory->newQuery()
110            ->caller( __METHOD__ )
111            ->watchlistUser( $wlowner );
112
113        if ( $params['prop'] !== null && $resultPageSet === null ) {
114            $prop = array_fill_keys( $params['prop'], true );
115
116            $this->fld_ids = isset( $prop['ids'] );
117            $this->fld_title = isset( $prop['title'] );
118            $this->fld_flags = isset( $prop['flags'] );
119            $this->fld_user = isset( $prop['user'] );
120            $this->fld_userid = isset( $prop['userid'] );
121            $this->fld_comment = isset( $prop['comment'] );
122            $this->fld_parsedcomment = isset( $prop['parsedcomment'] );
123            $this->fld_timestamp = isset( $prop['timestamp'] );
124            $this->fld_sizes = isset( $prop['sizes'] );
125            $this->fld_patrol = isset( $prop['patrol'] );
126            $this->fld_notificationtimestamp = isset( $prop['notificationtimestamp'] );
127            $this->fld_loginfo = isset( $prop['loginfo'] );
128            $this->fld_tags = isset( $prop['tags'] );
129            $this->fld_expiry = isset( $prop['expiry'] );
130
131            if ( $this->fld_patrol && !$user->useRCPatrol() && !$user->useNPPatrol() ) {
132                $this->dieWithError( 'apierror-permissiondenied-patrolflag', 'patrol' );
133            }
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_expiry ) {
324            $query->maybeAddWatchlistExpiryField();
325        }
326        if ( $this->fld_flags ) {
327            $fields[] = 'rc_minor';
328            $fields[] = 'rc_bot';
329        }
330        if ( $this->fld_user || $this->fld_userid || $this->fld_loginfo ) {
331            $query->rcUserFields();
332        }
333        if ( $this->fld_comment || $this->fld_parsedcomment ) {
334            $query->commentFields();
335        }
336        if ( $this->fld_patrol ) {
337            $fields[] = 'rc_patrolled';
338            $fields[] = 'rc_log_type';
339        }
340        if ( $this->fld_sizes ) {
341            $fields[] = 'rc_old_len';
342            $fields[] = 'rc_new_len';
343        }
344        if ( $this->fld_loginfo ) {
345            $fields[] = 'rc_logid';
346            $fields[] = 'rc_log_type';
347            $fields[] = 'rc_log_action';
348            $fields[] = 'rc_params';
349        }
350        if ( $this->fld_tags ) {
351            $query->addChangeTagSummaryField();
352        }
353        $query->fields( $fields );
354    }
355
356    private function showParamsConflicting( array $show ): bool {
357        return ( isset( $show['minor'] ) && isset( $show['!minor'] ) )
358        || ( isset( $show['bot'] ) && isset( $show['!bot'] ) )
359        || ( isset( $show['anon'] ) && isset( $show['!anon'] ) )
360        || ( isset( $show['patrolled'] ) && isset( $show['!patrolled'] ) )
361        || ( isset( $show['autopatrolled'] ) && isset( $show['!autopatrolled'] ) )
362        || ( isset( $show['autopatrolled'] ) && isset( $show['!patrolled'] ) )
363        || ( isset( $show['unread'] ) && isset( $show['!unread'] ) );
364    }
365
366    private function extractOutputData( stdClass $row, User $wlowner ): array {
367        $user = $this->getUser();
368        $title = PageReferenceValue::localReference( (int)$row->rc_namespace, $row->rc_title );
369
370        /* Our output data. */
371        $vals = [];
372        $vals['type'] = $this->recentChangeLookup->convertSourceToType( $row->rc_source );
373        $isLog = $row->rc_source === RecentChange::SRC_LOG;
374        $anyHidden = false;
375
376        /* Create a new entry in the result for the title. */
377        if ( $this->fld_title || $this->fld_ids ) {
378            // These should already have been filtered out of the query, but just in case.
379            if ( $isLog && ( $row->rc_deleted & LogPage::DELETED_ACTION ) ) {
380                $vals['actionhidden'] = true;
381                $anyHidden = true;
382            }
383            if ( !$isLog ||
384                LogEventsList::userCanBitfield(
385                    $row->rc_deleted,
386                    LogPage::DELETED_ACTION,
387                    $user
388                )
389            ) {
390                if ( $this->fld_title ) {
391                    $vals['ns'] = $title->getNamespace();
392                    $vals['title'] = $this->titleFormatter->getPrefixedText( $title );
393                }
394                if ( $this->fld_ids ) {
395                    $vals['pageid'] = (int)$row->rc_cur_id;
396                    $vals['revid'] = (int)$row->rc_this_oldid;
397                    $vals['old_revid'] = (int)$row->rc_last_oldid;
398                }
399            }
400        }
401
402        if ( $this->fld_user || $this->fld_userid ) {
403            if ( $row->rc_deleted & RevisionRecord::DELETED_USER ) {
404                $vals['userhidden'] = true;
405                $anyHidden = true;
406            }
407            if ( RevisionRecord::userCanBitfield(
408                $row->rc_deleted,
409                RevisionRecord::DELETED_USER,
410                $user
411            ) ) {
412                if ( $this->fld_userid ) {
413                    $vals['userid'] = (int)$row->rc_user;
414                    // for backwards compatibility
415                    $vals['user'] = (int)$row->rc_user;
416                }
417
418                if ( $this->fld_user ) {
419                    $vals['user'] = $row->rc_user_text;
420                    $vals['temp'] = $this->tempUserConfig->isTempName(
421                        $row->rc_user_text
422                    );
423                }
424
425                // Whether the user is a logged-out user (IP user). This does
426                // not include temporary users, though they are grouped with IP
427                // users for FILTER_NOT_ANON and FILTER_ANON, to match the
428                // recent changes filters (T343322).
429                $vals['anon'] = !$row->rc_user;
430            }
431        }
432
433        /* Add flags, such as new, minor, bot. */
434        if ( $this->fld_flags ) {
435            $vals['bot'] = (bool)$row->rc_bot;
436            $vals['new'] = $row->rc_source == RecentChange::SRC_NEW;
437            $vals['minor'] = (bool)$row->rc_minor;
438        }
439
440        /* Add sizes of each revision. */
441        if ( $this->fld_sizes ) {
442            $vals['oldlen'] = (int)$row->rc_old_len;
443            $vals['newlen'] = (int)$row->rc_new_len;
444        }
445
446        /* Add the timestamp. */
447        if ( $this->fld_timestamp ) {
448            $vals['timestamp'] = wfTimestamp( TS::ISO_8601, $row->rc_timestamp );
449        }
450
451        if ( $this->fld_notificationtimestamp ) {
452            $vals['notificationtimestamp'] = ( $row->wl_notificationtimestamp == null )
453                ? ''
454                : wfTimestamp( TS::ISO_8601, $row->wl_notificationtimestamp );
455        }
456
457        /* Add edit summary / log summary. */
458        if ( $this->fld_comment || $this->fld_parsedcomment ) {
459            if ( $row->rc_deleted & RevisionRecord::DELETED_COMMENT ) {
460                $vals['commenthidden'] = true;
461                $anyHidden = true;
462            }
463            if ( RevisionRecord::userCanBitfield(
464                $row->rc_deleted,
465                RevisionRecord::DELETED_COMMENT,
466                $user
467            ) ) {
468                $comment = $this->commentStore->getComment( 'rc_comment', $row )->text;
469                if ( $this->fld_comment ) {
470                    $vals['comment'] = $comment;
471                }
472
473                if ( $this->fld_parsedcomment ) {
474                    $vals['parsedcomment'] = $this->formattedComments[$row->rc_id];
475                }
476            }
477        }
478
479        /* Add the patrolled flag */
480        if ( $this->fld_patrol ) {
481            $vals['patrolled'] = $row->rc_patrolled != RecentChange::PRC_UNPATROLLED;
482            $vals['unpatrolled'] = ChangesList::isUnpatrolled( $row, $user );
483            $vals['autopatrolled'] = $row->rc_patrolled == RecentChange::PRC_AUTOPATROLLED;
484        }
485
486        if ( $this->fld_loginfo && $row->rc_source == RecentChange::SRC_LOG ) {
487            if ( $row->rc_deleted & LogPage::DELETED_ACTION ) {
488                $vals['actionhidden'] = true;
489                $anyHidden = true;
490            }
491            if ( LogEventsList::userCanBitfield(
492                $row->rc_deleted,
493                LogPage::DELETED_ACTION,
494                $user
495            ) ) {
496                $vals['logid'] = (int)$row->rc_logid;
497                $vals['logtype'] = $row->rc_log_type;
498                $vals['logaction'] = $row->rc_log_action;
499
500                $logFormatter = $this->logFormatterFactory->newFromRow( $row );
501                $vals['logparams'] = $logFormatter->formatParametersForApi();
502                $vals['logdisplay'] = $logFormatter->getActionText();
503            }
504        }
505
506        if ( $this->fld_tags ) {
507            if ( $row->ts_tags ) {
508                $tags = explode( ',', $row->ts_tags );
509                ApiResult::setIndexedTagName( $tags, 'tag' );
510                $vals['tags'] = $tags;
511            } else {
512                $vals['tags'] = [];
513            }
514        }
515
516        if ( $this->fld_expiry ) {
517            // Add expiration, T263796
518            $expiryString = $row->we_expiry ?? null;
519            if ( $expiryString ) {
520                $vals['expiry'] = wfTimestamp( TS::ISO_8601, $expiryString );
521            } else {
522                $vals['expiry'] = false;
523            }
524        }
525
526        if ( $anyHidden && ( $row->rc_deleted & RevisionRecord::DELETED_RESTRICTED ) ) {
527            $vals['suppressed'] = true;
528        }
529
530        if ( $this->getHookContainer()->isRegistered( 'ApiQueryWatchlistExtractOutputData' ) ) {
531            $watchedItem = new WatchedItem(
532                $wlowner,
533                $title,
534                $row->wl_notificationtimestamp,
535                $row->we_expiry ?? null
536            );
537            $recentChangeInfo = (array)$row;
538            $this->getHookRunner()->onApiQueryWatchlistExtractOutputData(
539                $this, $watchedItem, $recentChangeInfo, $vals );
540        }
541
542        return $vals;
543    }
544
545    /** @inheritDoc */
546    public function getAllowedParams() {
547        return [
548            'allrev' => false,
549            'start' => [
550                ParamValidator::PARAM_TYPE => 'timestamp'
551            ],
552            'end' => [
553                ParamValidator::PARAM_TYPE => 'timestamp'
554            ],
555            'namespace' => [
556                ParamValidator::PARAM_ISMULTI => true,
557                ParamValidator::PARAM_TYPE => 'namespace'
558            ],
559            'user' => [
560                ParamValidator::PARAM_TYPE => 'user',
561                UserDef::PARAM_RETURN_OBJECT => true,
562                UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'id', 'interwiki' ],
563            ],
564            'excludeuser' => [
565                ParamValidator::PARAM_TYPE => 'user',
566                UserDef::PARAM_RETURN_OBJECT => true,
567                UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'id', 'interwiki' ],
568            ],
569            'dir' => [
570                ParamValidator::PARAM_DEFAULT => 'older',
571                ParamValidator::PARAM_TYPE => [
572                    'newer',
573                    'older'
574                ],
575                ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
576                ApiBase::PARAM_HELP_MSG_PER_VALUE => [
577                    'newer' => 'api-help-paramvalue-direction-newer',
578                    'older' => 'api-help-paramvalue-direction-older',
579                ],
580            ],
581            'limit' => [
582                ParamValidator::PARAM_DEFAULT => 10,
583                ParamValidator::PARAM_TYPE => 'limit',
584                IntegerDef::PARAM_MIN => 1,
585                IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1,
586                IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2
587            ],
588            'prop' => [
589                ParamValidator::PARAM_ISMULTI => true,
590                ParamValidator::PARAM_DEFAULT => 'ids|title|flags',
591                ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
592                ParamValidator::PARAM_TYPE => [
593                    'ids',
594                    'title',
595                    'flags',
596                    'user',
597                    'userid',
598                    'comment',
599                    'parsedcomment',
600                    'timestamp',
601                    'patrol',
602                    'sizes',
603                    'notificationtimestamp',
604                    'loginfo',
605                    'tags',
606                    'expiry',
607                ]
608            ],
609            'show' => [
610                ParamValidator::PARAM_ISMULTI => true,
611                ParamValidator::PARAM_TYPE => [
612                    'minor',
613                    '!minor',
614                    'bot',
615                    '!bot',
616                    'anon',
617                    '!anon',
618                    'patrolled',
619                    '!patrolled',
620                    'autopatrolled',
621                    '!autopatrolled',
622                    'unread',
623                    '!unread',
624                ]
625            ],
626            'type' => [
627                ParamValidator::PARAM_DEFAULT => 'edit|new|log|categorize',
628                ParamValidator::PARAM_ISMULTI => true,
629                ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
630                ParamValidator::PARAM_TYPE => RecentChange::getChangeTypes()
631            ],
632            'owner' => [
633                ParamValidator::PARAM_TYPE => 'user',
634                UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name' ],
635            ],
636            'token' => [
637                ParamValidator::PARAM_TYPE => 'string',
638                ParamValidator::PARAM_SENSITIVE => true,
639            ],
640            'continue' => [
641                ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
642            ],
643        ];
644    }
645
646    /** @inheritDoc */
647    protected function getExamplesMessages() {
648        return [
649            'action=query&list=watchlist'
650                => 'apihelp-query+watchlist-example-simple',
651            'action=query&list=watchlist&wlprop=ids|title|timestamp|user|comment'
652                => 'apihelp-query+watchlist-example-props',
653            'action=query&list=watchlist&wlprop=ids|title|timestamp|user|comment|expiry'
654                => 'apihelp-query+watchlist-example-expiry',
655            'action=query&list=watchlist&wlallrev=&wlprop=ids|title|timestamp|user|comment'
656                => 'apihelp-query+watchlist-example-allrev',
657            'action=query&generator=watchlist&prop=info'
658                => 'apihelp-query+watchlist-example-generator',
659            'action=query&generator=watchlist&gwlallrev=&prop=revisions&rvprop=timestamp|user'
660                => 'apihelp-query+watchlist-example-generator-rev',
661            'action=query&list=watchlist&wlowner=Example&wltoken=123ABC'
662                => 'apihelp-query+watchlist-example-wlowner',
663        ];
664    }
665
666    /** @inheritDoc */
667    public function getHelpUrls() {
668        return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Watchlist';
669    }
670}
671
672/** @deprecated class alias since 1.43 */
673class_alias( ApiQueryWatchlist::class, 'ApiQueryWatchlist' );