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