Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
84.16% covered (warning)
84.16%
356 / 423
54.55% covered (warning)
54.55%
6 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiQueryRecentChanges
84.36% covered (warning)
84.36%
356 / 422
54.55% covered (warning)
54.55%
6 / 11
231.79
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
 initProperties
100.00% covered (success)
100.00%
14 / 14
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
79.89% covered (warning)
79.89%
143 / 179
0.00% covered (danger)
0.00%
0 / 1
139.04
 extractRowInfo
79.21% covered (warning)
79.21%
80 / 101
0.00% covered (danger)
0.00%
0 / 1
66.86
 includesPatrollingFlags
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
5
 getCacheMode
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
6.56
 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 / 6
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\ParamValidator\TypeDef\NamespaceDef;
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\Revision\SlotRecord;
26use MediaWiki\Revision\SlotRoleRegistry;
27use MediaWiki\Title\Title;
28use MediaWiki\User\UserNameUtils;
29use stdClass;
30use Wikimedia\ParamValidator\ParamValidator;
31use Wikimedia\ParamValidator\TypeDef\IntegerDef;
32use Wikimedia\Timestamp\TimestampFormat as TS;
33
34/**
35 * A query action to enumerate the recent changes that were done to the wiki.
36 * Various filters are supported.
37 *
38 * TODO: Factor out a common base class with ApiQueryWatchlist
39 *
40 * @ingroup RecentChanges
41 * @ingroup API
42 */
43class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
44
45    private CommentStore $commentStore;
46    private RowCommentFormatter $commentFormatter;
47    private SlotRoleRegistry $slotRoleRegistry;
48    private UserNameUtils $userNameUtils;
49    private LogFormatterFactory $logFormatterFactory;
50    private ChangesListQueryFactory $changesListQueryFactory;
51    private RecentChangeLookup $recentChangeLookup;
52
53    /** @var string[] */
54    private $formattedComments = [];
55
56    public function __construct(
57        ApiQuery $query,
58        string $moduleName,
59        CommentStore $commentStore,
60        RowCommentFormatter $commentFormatter,
61        SlotRoleRegistry $slotRoleRegistry,
62        UserNameUtils $userNameUtils,
63        LogFormatterFactory $logFormatterFactory,
64        ChangesListQueryFactory $changesListQueryFactory,
65        RecentChangeLookup $recentChangeLookup,
66    ) {
67        parent::__construct( $query, $moduleName, 'rc' );
68        $this->commentStore = $commentStore;
69        $this->commentFormatter = $commentFormatter;
70        $this->slotRoleRegistry = $slotRoleRegistry;
71        $this->userNameUtils = $userNameUtils;
72        $this->logFormatterFactory = $logFormatterFactory;
73        $this->changesListQueryFactory = $changesListQueryFactory;
74        $this->recentChangeLookup = $recentChangeLookup;
75    }
76
77    private bool $fld_comment = false;
78    private bool $fld_parsedcomment = false;
79    private bool $fld_user = false;
80    private bool $fld_userid = false;
81    private bool $fld_flags = false;
82    private bool $fld_timestamp = false;
83    private bool $fld_title = false;
84    private bool $fld_ids = false;
85    private bool $fld_sizes = false;
86    private bool $fld_redirect = false;
87    private bool $fld_patrolled = false;
88    private bool $fld_loginfo = false;
89    private bool $fld_tags = false;
90    private bool $fld_sha1 = false;
91
92    /**
93     * Sets internal state to include the desired properties in the output.
94     * @param array $prop Associative array of properties, only keys are used here
95     */
96    public function initProperties( $prop ) {
97        $this->fld_comment = isset( $prop['comment'] );
98        $this->fld_parsedcomment = isset( $prop['parsedcomment'] );
99        $this->fld_user = isset( $prop['user'] );
100        $this->fld_userid = isset( $prop['userid'] );
101        $this->fld_flags = isset( $prop['flags'] );
102        $this->fld_timestamp = isset( $prop['timestamp'] );
103        $this->fld_title = isset( $prop['title'] );
104        $this->fld_ids = isset( $prop['ids'] );
105        $this->fld_sizes = isset( $prop['sizes'] );
106        $this->fld_redirect = isset( $prop['redirect'] );
107        $this->fld_patrolled = isset( $prop['patrolled'] );
108        $this->fld_loginfo = isset( $prop['loginfo'] );
109        $this->fld_tags = isset( $prop['tags'] );
110        $this->fld_sha1 = isset( $prop['sha1'] );
111    }
112
113    public function execute() {
114        $this->run();
115    }
116
117    /** @inheritDoc */
118    public function executeGenerator( $resultPageSet ) {
119        $this->run( $resultPageSet );
120    }
121
122    /**
123     * Generates and outputs the result of this query based upon the provided parameters.
124     *
125     * @param ApiPageSet|null $resultPageSet
126     */
127    public function run( $resultPageSet = null ) {
128        $user = $this->getUser();
129        /* Get the parameters of the request. */
130        $params = $this->extractRequestParams();
131
132        $query = $this->changesListQueryFactory->newQuery();
133        $query->watchlistUser( $user )
134            ->audience( $this->getAuthority() );
135
136        $sources = $this->recentChangeLookup->getAllSources();
137
138        if ( $params['dir'] === 'newer' ) {
139            $query->orderBy( ChangesListQuery::SORT_TIMESTAMP_ASC );
140        }
141        $startTimestamp = $params['start'];
142        $end = $params['end'];
143        if ( $params['continue'] !== null ) {
144            $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'timestamp', 'int' ] );
145            $startTimestamp = $cont[0];
146            $startId = $cont[1];
147        } else {
148            $startId = null;
149        }
150        if ( $startTimestamp !== null ) {
151            $query->startAt( $startTimestamp, $startId );
152        }
153        if ( $end !== null ) {
154            $query->endAt( $end );
155        }
156
157        if ( $params['type'] !== null ) {
158            $sources = array_intersect( $sources,
159                $this->recentChangeLookup->convertTypeToSources( $params['type'] ) );
160        }
161
162        $title = $params['title'];
163        if ( $title !== null ) {
164            $titleObj = Title::newFromText( $title );
165            if ( $titleObj === null || $titleObj->isExternal() ) {
166                $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $title ) ] );
167            } else {
168                if ( $params['namespace'] && !in_array( $titleObj->getNamespace(), $params['namespace'] ) ) {
169                    $this->requireMaxOneParameter( $params, 'title', 'namespace' );
170                }
171                $query->requireTitle( $titleObj );
172            }
173        } elseif ( $params['namespace'] !== null ) {
174            $query->requireNamespaces( $params['namespace'] );
175        }
176
177        if ( $params['show'] !== null ) {
178            $show = array_fill_keys( $params['show'], true );
179
180            /* Check for conflicting parameters. */
181            if ( ( isset( $show['minor'] ) && isset( $show['!minor'] ) )
182                || ( isset( $show['bot'] ) && isset( $show['!bot'] ) )
183                || ( isset( $show['anon'] ) && isset( $show['!anon'] ) )
184                || ( isset( $show['redirect'] ) && isset( $show['!redirect'] ) )
185                || ( isset( $show['patrolled'] ) && isset( $show['!patrolled'] ) )
186                || ( isset( $show['patrolled'] ) && isset( $show['unpatrolled'] ) )
187                || ( isset( $show['!patrolled'] ) && isset( $show['unpatrolled'] ) )
188                || ( isset( $show['autopatrolled'] ) && isset( $show['!autopatrolled'] ) )
189                || ( isset( $show['autopatrolled'] ) && isset( $show['unpatrolled'] ) )
190                || ( isset( $show['autopatrolled'] ) && isset( $show['!patrolled'] ) )
191            ) {
192                $this->dieWithError( 'apierror-show' );
193            }
194
195            // Check permissions
196            if ( $this->includesPatrollingFlags( $show ) && !$user->useRCPatrol() && !$user->useNPPatrol() ) {
197                $this->dieWithError( 'apierror-permissiondenied-patrolflag', 'permissiondenied' );
198            }
199
200            /* Add additional conditions to query depending upon parameters. */
201            $showActions = [
202                'minor' => [ 'require', 'minor', true ],
203                '!minor' => [ 'exclude', 'minor', true ],
204                'bot' => [ 'require', 'bot', true ],
205                '!bot' => [ 'exclude', 'bot', true ],
206                'anon' => [ 'exclude', 'named' ],
207                '!anon' => [ 'require', 'named' ],
208                'patrolled' => [ 'exclude', 'patrolled', RecentChange::PRC_UNPATROLLED ],
209                '!patrolled' => [ 'require', 'patrolled', RecentChange::PRC_UNPATROLLED ],
210                'redirect' => [ 'require', 'redirect', true ],
211                '!redirect' => [ 'exclude', 'redirect', true ],
212                'autopatrolled' => [ 'require', 'patrolled', RecentChange::PRC_AUTOPATROLLED ],
213                '!autopatrolled' => [ 'exclude', 'patrolled', RecentChange::PRC_AUTOPATROLLED ],
214            ];
215            foreach ( $show as $name => $unused ) {
216                if ( isset( $showActions[$name] ) ) {
217                    $query->applyAction( ...$showActions[$name] );
218                }
219            }
220
221            if ( isset( $show['unpatrolled'] ) ) {
222                // See ChangesList::isUnpatrolled
223                if ( $user->useRCPatrol() ) {
224                    $query->requirePatrolled( RecentChange::PRC_UNPATROLLED );
225                } elseif ( $user->useNPPatrol() ) {
226                    $query->requirePatrolled( RecentChange::PRC_UNPATROLLED );
227                    $sources = array_intersect( $sources, [ RecentChange::SRC_NEW ] );
228                }
229            }
230        }
231
232        $this->requireMaxOneParameter( $params, 'user', 'excludeuser' );
233
234        if ( $params['prop'] !== null ) {
235            $prop = array_fill_keys( $params['prop'], true );
236
237            /* Set up internal members based upon params. */
238            $this->initProperties( $prop );
239        }
240
241        if ( $this->fld_user || $this->fld_userid ) {
242            $query->rcUserFields();
243        }
244
245        if ( $params['user'] !== null ) {
246            $query->requireUser( $params['user'] );
247        }
248
249        if ( $params['excludeuser'] !== null ) {
250            $query->excludeUser( $params['excludeuser'] );
251        }
252
253        /* Add the fields we're concerned with to our query. */
254        $query->fields( [
255            'rc_id',
256            'rc_timestamp',
257            'rc_namespace',
258            'rc_title',
259            'rc_cur_id',
260            'rc_source',
261            'rc_deleted'
262        ] );
263
264        /* Determine what properties we need to display. */
265        if ( $params['prop'] !== null ) {
266            if ( $this->fld_patrolled && !$user->useRCPatrol() && !$user->useNPPatrol() ) {
267                $this->dieWithError( 'apierror-permissiondenied-patrolflag', 'permissiondenied' );
268            }
269
270            /* Add fields to our query if they are specified as a needed parameter. */
271            if ( $this->fld_ids ) {
272                $query->fields( [ 'rc_this_oldid', 'rc_last_oldid' ] );
273            }
274            if ( $this->fld_flags ) {
275                $query->fields( [ 'rc_minor', 'rc_bot' ] );
276            }
277            if ( $this->fld_sizes ) {
278                $query->fields( [ 'rc_old_len', 'rc_new_len' ] );
279            }
280            if ( $this->fld_patrolled ) {
281                $query->fields( [ 'rc_patrolled', 'rc_log_type' ] );
282            }
283            if ( $this->fld_loginfo ) {
284                $query->fields( [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ] );
285            }
286            if ( $this->fld_redirect ) {
287                $query->addRedirectField();
288            }
289        }
290        if ( $resultPageSet && $params['generaterevisions'] ) {
291            $query->fields( [ 'rc_this_oldid' ] );
292        }
293        if ( $this->fld_tags ) {
294            $query->addChangeTagSummaryField();
295        }
296        if ( $this->fld_sha1 ) {
297            $query->sha1Fields();
298        }
299        if ( $params['toponly'] ) {
300            $query->requireLatest();
301        }
302        if ( $params['tag'] !== null ) {
303            $query->requireChangeTags( [ $params['tag'] ] );
304        }
305
306        // Paranoia: avoid brute force searches (T19342)
307        if ( $params['user'] !== null || $params['excludeuser'] !== null ) {
308            $query->excludeDeletedUser();
309        }
310        if ( $this->getRequest()->getCheck( 'namespace' ) ) {
311            $query->excludeDeletedLogAction();
312        }
313
314        if ( $this->fld_comment || $this->fld_parsedcomment ) {
315            $query->commentFields();
316        }
317
318        if ( $params['slot'] !== null ) {
319            // Only include changes that touch page content (i.e. RC_NEW, RC_EDIT)
320            $sources = array_intersect( $sources,
321                [ RecentChange::SRC_NEW, RecentChange::SRC_EDIT ] );
322            $query->requireSlotChanged( $params['slot'] );
323        }
324
325        $query->requireSources( $sources )
326            ->limit( $params['limit'] + 1 )
327            ->maxExecutionTime( $this->getConfig()->get(
328                MainConfigNames::MaxExecutionTimeForExpensiveQueries ) )
329            ->caller( __METHOD__ );
330
331        $hookData = [];
332        if ( $this->getHookContainer()->isRegistered( 'ApiQueryBaseBeforeQuery' ) ) {
333            $query->legacyMutator(
334                function ( &$tables, &$fields, &$conds, &$options, &$join_conds ) use ( &$hookData ) {
335                    $this->getHookRunner()->onApiQueryBaseBeforeQuery(
336                        $this, $tables, $fields, $conds,
337                        $options, $join_conds, $hookData );
338                }
339            );
340        }
341
342        $count = 0;
343        /* Perform the actual query. */
344        $res = $query->fetchResult()->getResultWrapper();
345
346        $this->getHookRunner()->onApiQueryBaseAfterQuery( $this, $res, $hookData );
347
348        // Do batch queries
349        if ( $this->fld_title && $resultPageSet === null ) {
350            $this->executeGenderCacheFromResultWrapper( $res, __METHOD__, 'rc' );
351        }
352        if ( $this->fld_parsedcomment ) {
353            $this->formattedComments = $this->commentFormatter->formatItems(
354                $this->commentFormatter->rows( $res )
355                    ->indexField( 'rc_id' )
356                    ->commentKey( 'rc_comment' )
357                    ->namespaceField( 'rc_namespace' )
358                    ->titleField( 'rc_title' )
359            );
360        }
361
362        $revids = [];
363        $titles = [];
364
365        $result = $this->getResult();
366
367        /* Iterate through the rows, adding data extracted from them to our query result. */
368        foreach ( $res as $row ) {
369            if ( $count === 0 && $resultPageSet !== null ) {
370                // Set the non-continue since the list of recentchanges is
371                // prone to having entries added at the start frequently.
372                $this->getContinuationManager()->addGeneratorNonContinueParam(
373                    $this, 'continue', "$row->rc_timestamp|$row->rc_id"
374                );
375            }
376            if ( ++$count > $params['limit'] ) {
377                // We've reached the one extra which shows that there are
378                // additional pages to be had. Stop here...
379                $this->setContinueEnumParameter( 'continue', "$row->rc_timestamp|$row->rc_id" );
380                break;
381            }
382
383            if ( $resultPageSet === null ) {
384                /* Extract the data from a single row. */
385                $vals = $this->extractRowInfo( $row );
386
387                /* Add that row's data to our final output. */
388                $fit = $this->processRow( $row, $vals, $hookData ) &&
389                    $result->addValue( [ 'query', $this->getModuleName() ], null, $vals );
390                if ( !$fit ) {
391                    $this->setContinueEnumParameter( 'continue', "$row->rc_timestamp|$row->rc_id" );
392                    break;
393                }
394            } elseif ( $params['generaterevisions'] ) {
395                $revid = (int)$row->rc_this_oldid;
396                if ( $revid > 0 ) {
397                    $revids[] = $revid;
398                }
399            } else {
400                $titles[] = Title::makeTitle( $row->rc_namespace, $row->rc_title );
401            }
402        }
403
404        if ( $resultPageSet === null ) {
405            /* Format the result */
406            $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'rc' );
407        } elseif ( $params['generaterevisions'] ) {
408            $resultPageSet->populateFromRevisionIDs( $revids );
409        } else {
410            $resultPageSet->populateFromTitles( $titles );
411        }
412    }
413
414    /**
415     * Extracts from a single sql row the data needed to describe one recent change.
416     *
417     * @param stdClass $row The row from which to extract the data.
418     * @return array An array mapping strings (descriptors) to their respective string values.
419     */
420    public function extractRowInfo( $row ) {
421        /* Determine the title of the page that has been changed. */
422        $title = Title::makeTitle( $row->rc_namespace, $row->rc_title );
423        $user = $this->getUser();
424
425        /* Our output data. */
426        $vals = [];
427
428        $vals['type'] = $this->recentChangeLookup->convertSourceToType( $row->rc_source );
429        $isLog = $row->rc_source === RecentChange::SRC_LOG;
430
431        $anyHidden = false;
432
433        /* Create a new entry in the result for the title. */
434        if ( $this->fld_title || $this->fld_ids ) {
435            if ( $isLog && ( $row->rc_deleted & LogPage::DELETED_ACTION ) ) {
436                $vals['actionhidden'] = true;
437                $anyHidden = true;
438            }
439            if ( !$isLog ||
440                LogEventsList::userCanBitfield( $row->rc_deleted, LogPage::DELETED_ACTION, $user )
441            ) {
442                if ( $this->fld_title ) {
443                    ApiQueryBase::addTitleInfo( $vals, $title );
444                }
445                if ( $this->fld_ids ) {
446                    $vals['pageid'] = (int)$row->rc_cur_id;
447                    $vals['revid'] = (int)$row->rc_this_oldid;
448                    $vals['old_revid'] = (int)$row->rc_last_oldid;
449                }
450            }
451        }
452
453        if ( $this->fld_ids ) {
454            $vals['rcid'] = (int)$row->rc_id;
455        }
456
457        /* Add user data and 'anon' flag, if user is anonymous. */
458        if ( $this->fld_user || $this->fld_userid ) {
459            if ( $row->rc_deleted & RevisionRecord::DELETED_USER ) {
460                $vals['userhidden'] = true;
461                $anyHidden = true;
462            }
463            if ( RevisionRecord::userCanBitfield( $row->rc_deleted, RevisionRecord::DELETED_USER, $user ) ) {
464                if ( $this->fld_user ) {
465                    $vals['user'] = $row->rc_user_text;
466                }
467
468                if ( $this->fld_userid ) {
469                    $vals['userid'] = (int)$row->rc_user;
470                }
471
472                if ( isset( $row->rc_user_text ) && $this->userNameUtils->isTemp( $row->rc_user_text ) ) {
473                    $vals['temp'] = true;
474                }
475
476                if ( !$row->rc_user ) {
477                    $vals['anon'] = true;
478                }
479            }
480        }
481
482        /* Add flags, such as new, minor, bot. */
483        if ( $this->fld_flags ) {
484            $vals['bot'] = (bool)$row->rc_bot;
485            $vals['new'] = $row->rc_source == RecentChange::SRC_NEW;
486            $vals['minor'] = (bool)$row->rc_minor;
487        }
488
489        /* Add sizes of each revision. (Only available on 1.10+) */
490        if ( $this->fld_sizes ) {
491            $vals['oldlen'] = (int)$row->rc_old_len;
492            $vals['newlen'] = (int)$row->rc_new_len;
493        }
494
495        /* Add the timestamp. */
496        if ( $this->fld_timestamp ) {
497            $vals['timestamp'] = wfTimestamp( TS::ISO_8601, $row->rc_timestamp );
498        }
499
500        /* Add edit summary / log summary. */
501        if ( $this->fld_comment || $this->fld_parsedcomment ) {
502            if ( $row->rc_deleted & RevisionRecord::DELETED_COMMENT ) {
503                $vals['commenthidden'] = true;
504                $anyHidden = true;
505            }
506            if ( RevisionRecord::userCanBitfield(
507                $row->rc_deleted, RevisionRecord::DELETED_COMMENT, $user
508            ) ) {
509                if ( $this->fld_comment ) {
510                    $vals['comment'] = $this->commentStore->getComment( 'rc_comment', $row )->text;
511                }
512
513                if ( $this->fld_parsedcomment ) {
514                    $vals['parsedcomment'] = $this->formattedComments[$row->rc_id];
515                }
516            }
517        }
518
519        if ( $this->fld_redirect ) {
520            $vals['redirect'] = (bool)$row->page_is_redirect;
521        }
522
523        /* Add the patrolled flag */
524        if ( $this->fld_patrolled ) {
525            $vals['patrolled'] = $row->rc_patrolled != RecentChange::PRC_UNPATROLLED;
526            $vals['unpatrolled'] = ChangesList::isUnpatrolled( $row, $user );
527            $vals['autopatrolled'] = $row->rc_patrolled == RecentChange::PRC_AUTOPATROLLED;
528        }
529
530        if ( $this->fld_loginfo && $row->rc_source == RecentChange::SRC_LOG ) {
531            if ( $row->rc_deleted & LogPage::DELETED_ACTION ) {
532                $vals['actionhidden'] = true;
533                $anyHidden = true;
534            }
535            if ( LogEventsList::userCanBitfield( $row->rc_deleted, LogPage::DELETED_ACTION, $user ) ) {
536                $vals['logid'] = (int)$row->rc_logid;
537                $vals['logtype'] = $row->rc_log_type;
538                $vals['logaction'] = $row->rc_log_action;
539                $vals['logparams'] = $this->logFormatterFactory->newFromRow( $row )->formatParametersForApi();
540            }
541        }
542
543        if ( $this->fld_tags ) {
544            if ( $row->ts_tags ) {
545                $tags = explode( ',', $row->ts_tags );
546                ApiResult::setIndexedTagName( $tags, 'tag' );
547                $vals['tags'] = $tags;
548            } else {
549                $vals['tags'] = [];
550            }
551        }
552
553        if ( $this->fld_sha1 && $row->rev_slot_pairs !== null ) {
554            if ( $row->rev_deleted & RevisionRecord::DELETED_TEXT ) {
555                $vals['sha1hidden'] = true;
556                $anyHidden = true;
557            }
558            if ( RevisionRecord::userCanBitfield(
559                $row->rev_deleted, RevisionRecord::DELETED_TEXT, $user
560            ) ) {
561                $combinedBase36 = '';
562                if ( $row->rev_slot_pairs !== '' ) {
563                    $items = explode( ',', $row->rev_slot_pairs );
564                    $slotHashes = [];
565                    foreach ( $items as $item ) {
566                        $parts = explode( ':', $item );
567                        $slotHashes[$parts[0]] = $parts[1];
568                    }
569                    ksort( $slotHashes );
570
571                    $accu = null;
572                    foreach ( $slotHashes as $slotHash ) {
573                        $accu = $accu === null
574                            ? $slotHash
575                            : SlotRecord::base36Sha1( $accu . $slotHash );
576                    }
577                    $combinedBase36 = $accu ?? SlotRecord::base36Sha1( '' );
578                }
579
580                $vals['sha1'] = $combinedBase36 !== ''
581                    ? \Wikimedia\base_convert( $combinedBase36, 36, 16, 40 )
582                    : '';
583            }
584        }
585
586        if ( $anyHidden && ( $row->rc_deleted & RevisionRecord::DELETED_RESTRICTED ) ) {
587            $vals['suppressed'] = true;
588        }
589
590        return $vals;
591    }
592
593    /**
594     * @param array $flagsArray flipped array (string flags are keys)
595     * @return bool
596     */
597    private function includesPatrollingFlags( array $flagsArray ) {
598        return isset( $flagsArray['patrolled'] ) ||
599            isset( $flagsArray['!patrolled'] ) ||
600            isset( $flagsArray['unpatrolled'] ) ||
601            isset( $flagsArray['autopatrolled'] ) ||
602            isset( $flagsArray['!autopatrolled'] );
603    }
604
605    /** @inheritDoc */
606    public function getCacheMode( $params ) {
607        if ( isset( $params['show'] ) &&
608            $this->includesPatrollingFlags( array_fill_keys( $params['show'], true ) )
609        ) {
610            return 'private';
611        }
612        if ( $this->userCanSeeRevDel() ) {
613            return 'private';
614        }
615        if ( $params['prop'] !== null && in_array( 'parsedcomment', $params['prop'] ) ) {
616            // MediaWiki\CommentFormatter\CommentFormatter::formatItems() calls wfMessage() among other things
617            return 'anon-public-user-private';
618        }
619
620        return 'public';
621    }
622
623    /** @inheritDoc */
624    public function getAllowedParams() {
625        $slotRoles = $this->slotRoleRegistry->getKnownRoles();
626        sort( $slotRoles, SORT_STRING );
627
628        return [
629            'start' => [
630                ParamValidator::PARAM_TYPE => 'timestamp'
631            ],
632            'end' => [
633                ParamValidator::PARAM_TYPE => 'timestamp'
634            ],
635            'dir' => [
636                ParamValidator::PARAM_DEFAULT => 'older',
637                ParamValidator::PARAM_TYPE => [
638                    'newer',
639                    'older'
640                ],
641                ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
642                ApiBase::PARAM_HELP_MSG_PER_VALUE => [
643                    'newer' => 'api-help-paramvalue-direction-newer',
644                    'older' => 'api-help-paramvalue-direction-older',
645                ],
646            ],
647            'namespace' => [
648                ParamValidator::PARAM_ISMULTI => true,
649                ParamValidator::PARAM_TYPE => 'namespace',
650                NamespaceDef::PARAM_EXTRA_NAMESPACES => [ NS_MEDIA, NS_SPECIAL ],
651            ],
652            'user' => [
653                ParamValidator::PARAM_TYPE => 'user',
654                UserDef::PARAM_RETURN_OBJECT => true,
655                UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'id', 'interwiki' ],
656            ],
657            'excludeuser' => [
658                ParamValidator::PARAM_TYPE => 'user',
659                UserDef::PARAM_RETURN_OBJECT => true,
660                UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'id', 'interwiki' ],
661            ],
662            'tag' => null,
663            'prop' => [
664                ParamValidator::PARAM_ISMULTI => true,
665                ParamValidator::PARAM_DEFAULT => 'title|timestamp|ids',
666                ParamValidator::PARAM_TYPE => [
667                    'user',
668                    'userid',
669                    'comment',
670                    'parsedcomment',
671                    'flags',
672                    'timestamp',
673                    'title',
674                    'ids',
675                    'sizes',
676                    'redirect',
677                    'patrolled',
678                    'loginfo',
679                    'tags',
680                    'sha1',
681                ],
682                ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
683            ],
684            'show' => [
685                ParamValidator::PARAM_ISMULTI => true,
686                ParamValidator::PARAM_TYPE => [
687                    'minor',
688                    '!minor',
689                    'bot',
690                    '!bot',
691                    'anon',
692                    '!anon',
693                    'redirect',
694                    '!redirect',
695                    'patrolled',
696                    '!patrolled',
697                    'unpatrolled',
698                    'autopatrolled',
699                    '!autopatrolled',
700                ]
701            ],
702            'limit' => [
703                ParamValidator::PARAM_DEFAULT => 10,
704                ParamValidator::PARAM_TYPE => 'limit',
705                IntegerDef::PARAM_MIN => 1,
706                IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1,
707                IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2
708            ],
709            // TODO: deprecate and use rc_source directly, here and in the result
710            'type' => [
711                ParamValidator::PARAM_DEFAULT => 'edit|new|log|categorize',
712                ParamValidator::PARAM_ISMULTI => true,
713                ParamValidator::PARAM_TYPE => RecentChange::getChangeTypes()
714            ],
715            'toponly' => false,
716            'title' => null,
717            'continue' => [
718                ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
719            ],
720            'generaterevisions' => false,
721            'slot' => [
722                ParamValidator::PARAM_TYPE => $slotRoles
723            ],
724        ];
725    }
726
727    /** @inheritDoc */
728    protected function getExamplesMessages() {
729        return [
730            'action=query&list=recentchanges'
731                => 'apihelp-query+recentchanges-example-simple',
732            'action=query&generator=recentchanges&grcshow=!patrolled&prop=info'
733                => 'apihelp-query+recentchanges-example-generator',
734        ];
735    }
736
737    /** @inheritDoc */
738    public function getHelpUrls() {
739        return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Recentchanges';
740    }
741}
742
743/** @deprecated class alias since 1.43 */
744class_alias( ApiQueryRecentChanges::class, 'ApiQueryRecentChanges' );