Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
71.18% covered (warning)
71.18%
326 / 458
33.33% covered (danger)
33.33%
3 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiQueryUserContribs
71.33% covered (warning)
71.33%
326 / 457
33.33% covered (danger)
33.33%
3 / 9
395.00
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 execute
81.87% covered (warning)
81.87%
149 / 182
0.00% covered (danger)
0.00%
0 / 1
55.54
 prepareQuery
47.22% covered (danger)
47.22%
51 / 108
0.00% covered (danger)
0.00%
0 / 1
182.54
 extractRowInfo
42.11% covered (danger)
42.11%
24 / 57
0.00% covered (danger)
0.00%
0 / 1
146.28
 continueStr
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
4.05
 getCacheMode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAllowedParams
100.00% covered (success)
100.00%
86 / 86
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\ChangeTags\ChangeTagsStore;
12use MediaWiki\CommentFormatter\CommentFormatter;
13use MediaWiki\CommentStore\CommentStore;
14use MediaWiki\MainConfigNames;
15use MediaWiki\ParamValidator\TypeDef\UserDef;
16use MediaWiki\RecentChanges\RecentChange;
17use MediaWiki\Revision\RevisionRecord;
18use MediaWiki\Revision\RevisionStore;
19use MediaWiki\Storage\NameTableAccessException;
20use MediaWiki\Storage\NameTableStore;
21use MediaWiki\Title\Title;
22use MediaWiki\User\ActorMigration;
23use MediaWiki\User\ExternalUserNames;
24use MediaWiki\User\UserIdentity;
25use MediaWiki\User\UserIdentityLookup;
26use MediaWiki\User\UserIdentityValue;
27use MediaWiki\User\UserNameUtils;
28use stdClass;
29use Wikimedia\IPUtils;
30use Wikimedia\ParamValidator\ParamValidator;
31use Wikimedia\ParamValidator\TypeDef\IntegerDef;
32use Wikimedia\Rdbms\SelectQueryBuilder;
33use Wikimedia\Timestamp\TimestampFormat as TS;
34
35/**
36 * This query action adds a list of a specified user's contributions to the output.
37 *
38 * @ingroup API
39 */
40class ApiQueryUserContribs extends ApiQueryBase {
41
42    private CommentStore $commentStore;
43    private UserIdentityLookup $userIdentityLookup;
44    private UserNameUtils $userNameUtils;
45    private RevisionStore $revisionStore;
46    private NameTableStore $changeTagDefStore;
47    private ChangeTagsStore $changeTagsStore;
48    private ActorMigration $actorMigration;
49    private CommentFormatter $commentFormatter;
50
51    public function __construct(
52        ApiQuery $query,
53        string $moduleName,
54        CommentStore $commentStore,
55        UserIdentityLookup $userIdentityLookup,
56        UserNameUtils $userNameUtils,
57        RevisionStore $revisionStore,
58        NameTableStore $changeTagDefStore,
59        ChangeTagsStore $changeTagsStore,
60        ActorMigration $actorMigration,
61        CommentFormatter $commentFormatter
62    ) {
63        parent::__construct( $query, $moduleName, 'uc' );
64        $this->commentStore = $commentStore;
65        $this->userIdentityLookup = $userIdentityLookup;
66        $this->userNameUtils = $userNameUtils;
67        $this->revisionStore = $revisionStore;
68        $this->changeTagDefStore = $changeTagDefStore;
69        $this->changeTagsStore = $changeTagsStore;
70        $this->actorMigration = $actorMigration;
71        $this->commentFormatter = $commentFormatter;
72    }
73
74    private array $params;
75    private bool $multiUserMode;
76    private string $orderBy;
77    private array $parentLens;
78
79    /** @var array<string,true> */
80    private array $prop = [];
81
82    public function execute() {
83        // Parse some parameters
84        $this->params = $this->extractRequestParams();
85
86        $this->prop = array_fill_keys( $this->params['prop'], true );
87
88        $dbSecondary = $this->getDB(); // any random replica DB
89
90        $sort = ( $this->params['dir'] == 'newer' ?
91            SelectQueryBuilder::SORT_ASC : SelectQueryBuilder::SORT_DESC );
92        $op = ( $this->params['dir'] == 'older' ? '<=' : '>=' );
93
94        // Create an Iterator that produces the UserIdentity objects we need, depending
95        // on which of the 'userprefix', 'userids', 'iprange', or 'user' params
96        // was specified.
97        $this->requireOnlyOneParameter( $this->params, 'userprefix', 'userids', 'iprange', 'user' );
98        if ( isset( $this->params['userprefix'] ) ) {
99            $this->multiUserMode = true;
100            $this->orderBy = 'name';
101            $fname = __METHOD__;
102
103            // Because 'userprefix' might produce a huge number of users (e.g.
104            // a wiki with users "Test00000001" to "Test99999999"), use a
105            // generator with batched lookup and continuation.
106            $userIter = call_user_func( function () use ( $dbSecondary, $sort, $op, $fname ) {
107                $fromName = false;
108                if ( $this->params['continue'] !== null ) {
109                    $continue = $this->parseContinueParamOrDie( $this->params['continue'],
110                        [ 'string', 'string', 'string', 'int' ] );
111                    $this->dieContinueUsageIf( $continue[0] !== 'name' );
112                    $fromName = $continue[1];
113                }
114
115                $limit = 501;
116                do {
117                    $usersBatch = $this->userIdentityLookup
118                        ->newSelectQueryBuilder()
119                        ->caller( $fname )
120                        ->limit( $limit )
121                        ->whereUserNamePrefix( $this->params['userprefix'] )
122                        ->where( $fromName !== false
123                            ? $dbSecondary->buildComparison( $op, [ 'actor_name' => $fromName ] )
124                            : [] )
125                        ->orderByName( $sort )
126                        ->fetchUserIdentities();
127
128                    $count = 0;
129                    $fromName = false;
130                    foreach ( $usersBatch as $user ) {
131                        if ( ++$count >= $limit ) {
132                            $fromName = $user->getName();
133                            break;
134                        }
135                        yield $user;
136                    }
137                } while ( $fromName !== false );
138            } );
139            // Do the actual sorting client-side, because otherwise
140            // prepareQuery might try to sort by actor and confuse everything.
141            $batchSize = 1;
142        } elseif ( isset( $this->params['userids'] ) ) {
143            if ( $this->params['userids'] === [] ) {
144                $encParamName = $this->encodeParamName( 'userids' );
145                $this->dieWithError( [ 'apierror-paramempty', $encParamName ], "paramempty_$encParamName" );
146            }
147
148            $ids = [];
149            foreach ( $this->params['userids'] as $uid ) {
150                if ( $uid <= 0 ) {
151                    $this->dieWithError( [ 'apierror-invaliduserid', $uid ], 'invaliduserid' );
152                }
153                $ids[] = $uid;
154            }
155
156            $this->orderBy = 'actor';
157            $this->multiUserMode = count( $ids ) > 1;
158
159            $fromId = false;
160            if ( $this->multiUserMode && $this->params['continue'] !== null ) {
161                $continue = $this->parseContinueParamOrDie( $this->params['continue'],
162                    [ 'string', 'int', 'string', 'int' ] );
163                $this->dieContinueUsageIf( $continue[0] !== 'actor' );
164                $fromId = $continue[1];
165            }
166
167            $userIter = $this->userIdentityLookup
168                ->newSelectQueryBuilder()
169                ->caller( __METHOD__ )
170                ->whereUserIds( $ids )
171                ->orderByUserId( $sort )
172                ->where( $fromId ? $dbSecondary->buildComparison( $op, [ 'actor_id' => $fromId ] ) : [] )
173                ->fetchUserIdentities();
174            $batchSize = count( $ids );
175        } elseif ( isset( $this->params['iprange'] ) ) {
176            // Make sure it is a valid range and within the CIDR limit
177            $ipRange = $this->params['iprange'];
178            $contribsCIDRLimit = $this->getConfig()->get( MainConfigNames::RangeContributionsCIDRLimit );
179            if ( IPUtils::isIPv4( $ipRange ) ) {
180                $type = 'IPv4';
181                $cidrLimit = $contribsCIDRLimit['IPv4'];
182            } elseif ( IPUtils::isIPv6( $ipRange ) ) {
183                $type = 'IPv6';
184                $cidrLimit = $contribsCIDRLimit['IPv6'];
185            } else {
186                $this->dieWithError( [ 'apierror-invalidiprange', $ipRange ], 'invalidiprange' );
187            }
188            $range = IPUtils::parseCIDR( $ipRange )[1];
189            if ( $range === false ) {
190                $this->dieWithError( [ 'apierror-invalidiprange', $ipRange ], 'invalidiprange' );
191            } elseif ( $range < $cidrLimit ) {
192                $this->dieWithError( [ 'apierror-cidrtoobroad', $type, $cidrLimit ] );
193            }
194
195            $this->multiUserMode = true;
196            $this->orderBy = 'name';
197            $fname = __METHOD__;
198
199            // Because 'iprange' might produce a huge number of ips, use a
200            // generator with batched lookup and continuation.
201            $userIter = call_user_func( function () use ( $dbSecondary, $sort, $op, $fname, $ipRange ) {
202                [ $start, $end ] = IPUtils::parseRange( $ipRange );
203                if ( $this->params['continue'] !== null ) {
204                    $continue = $this->parseContinueParamOrDie( $this->params['continue'],
205                        [ 'string', 'string', 'string', 'int' ] );
206                    $this->dieContinueUsageIf( $continue[0] !== 'name' );
207                    $fromName = $continue[1];
208                    $fromIPHex = IPUtils::toHex( $fromName );
209                    $this->dieContinueUsageIf( $fromIPHex === false );
210                    if ( $op == '<=' ) {
211                        $end = $fromIPHex;
212                    } else {
213                        $start = $fromIPHex;
214                    }
215                }
216
217                $limit = 501;
218
219                do {
220                    $res = $dbSecondary->newSelectQueryBuilder()
221                        ->select( 'ipc_hex' )
222                        ->from( 'ip_changes' )
223                        ->where( $dbSecondary->expr( 'ipc_hex', '>=', $start )->and( 'ipc_hex', '<=', $end ) )
224                        ->groupBy( 'ipc_hex' )
225                        ->orderBy( 'ipc_hex', $sort )
226                        ->limit( $limit )
227                        ->caller( $fname )
228                        ->fetchResultSet();
229
230                    $count = 0;
231                    $fromName = false;
232                    foreach ( $res as $row ) {
233                        $ipAddr = IPUtils::formatHex( $row->ipc_hex );
234                        if ( ++$count >= $limit ) {
235                            $fromName = $ipAddr;
236                            break;
237                        }
238                        yield UserIdentityValue::newAnonymous( $ipAddr );
239                    }
240                } while ( $fromName !== false );
241            } );
242            // Do the actual sorting client-side, because otherwise
243            // prepareQuery might try to sort by actor and confuse everything.
244            $batchSize = 1;
245        } else {
246            $names = [];
247            if ( !count( $this->params['user'] ) ) {
248                $encParamName = $this->encodeParamName( 'user' );
249                $this->dieWithError(
250                    [ 'apierror-paramempty', $encParamName ], "paramempty_$encParamName"
251                );
252            }
253            foreach ( $this->params['user'] as $u ) {
254                if ( $u === '' ) {
255                    $encParamName = $this->encodeParamName( 'user' );
256                    $this->dieWithError(
257                        [ 'apierror-paramempty', $encParamName ], "paramempty_$encParamName"
258                    );
259                }
260
261                if ( $this->userNameUtils->isIP( $u ) || ExternalUserNames::isExternal( $u ) ) {
262                    $names[$u] = null;
263                } else {
264                    $name = $this->userNameUtils->getCanonical( $u );
265                    if ( $name === false ) {
266                        $encParamName = $this->encodeParamName( 'user' );
267                        $this->dieWithError(
268                            [ 'apierror-baduser', $encParamName, wfEscapeWikiText( $u ) ], "baduser_$encParamName"
269                        );
270                    }
271                    $names[$name] = null;
272                }
273            }
274
275            $this->orderBy = 'actor';
276            $this->multiUserMode = count( $names ) > 1;
277
278            $fromId = false;
279            if ( $this->multiUserMode && $this->params['continue'] !== null ) {
280                $continue = $this->parseContinueParamOrDie( $this->params['continue'],
281                    [ 'string', 'int', 'string', 'int' ] );
282                $this->dieContinueUsageIf( $continue[0] !== 'actor' );
283                $fromId = $continue[1];
284            }
285
286            $userIter = $this->userIdentityLookup
287                ->newSelectQueryBuilder()
288                ->caller( __METHOD__ )
289                ->whereUserNames( array_keys( $names ) )
290                ->orderByName( $sort )
291                ->where( $fromId ? $dbSecondary->buildComparison( $op, [ 'actor_id' => $fromId ] ) : [] )
292                ->fetchUserIdentities();
293            $batchSize = count( $names );
294        }
295
296        $count = 0;
297        $limit = $this->params['limit'];
298        $userIter->rewind();
299        while ( $userIter->valid() ) {
300            $users = [];
301            while ( count( $users ) < $batchSize && $userIter->valid() ) {
302                $users[] = $userIter->current();
303                $userIter->next();
304            }
305
306            $hookData = [];
307            $this->prepareQuery( $users, $limit - $count );
308            $res = $this->select( __METHOD__, [], $hookData );
309
310            if ( isset( $this->prop['title'] ) ) {
311                $this->executeGenderCacheFromResultWrapper( $res, __METHOD__ );
312            }
313
314            if ( isset( $this->prop['sizediff'] ) ) {
315                $revIds = [];
316                foreach ( $res as $row ) {
317                    if ( $row->rev_parent_id ) {
318                        $revIds[] = (int)$row->rev_parent_id;
319                    }
320                }
321                $this->parentLens = $this->revisionStore->getRevisionSizes( $revIds );
322            }
323
324            foreach ( $res as $row ) {
325                if ( ++$count > $limit ) {
326                    // We've reached the one extra which shows that there are
327                    // additional pages to be had. Stop here...
328                    $this->setContinueEnumParameter( 'continue', $this->continueStr( $row ) );
329                    break 2;
330                }
331
332                $vals = $this->extractRowInfo( $row );
333                $fit = $this->processRow( $row, $vals, $hookData ) &&
334                    $this->getResult()->addValue( [ 'query', $this->getModuleName() ], null, $vals );
335                if ( !$fit ) {
336                    $this->setContinueEnumParameter( 'continue', $this->continueStr( $row ) );
337                    break 2;
338                }
339            }
340        }
341
342        $this->getResult()->addIndexedTagName( [ 'query', $this->getModuleName() ], 'item' );
343    }
344
345    /**
346     * Prepares the query and returns the limit of rows requested
347     * @param UserIdentity[] $users
348     * @param int $limit
349     */
350    private function prepareQuery( array $users, $limit ) {
351        $this->resetQueryParams();
352        $db = $this->getDB();
353
354        $queryBuilder = $this->revisionStore->newSelectQueryBuilder( $db )->joinComment()->joinPage();
355        $revWhere = $this->actorMigration->getWhere( $db, 'rev_user', $users );
356
357        $orderUserField = 'rev_actor';
358        $userField = $this->orderBy === 'actor' ? 'rev_actor' : 'actor_name';
359        $tsField = 'rev_timestamp';
360        $idField = 'rev_id';
361
362        $this->getQueryBuilder()->merge( $queryBuilder );
363        $this->addWhere( $revWhere['conds'] );
364        // Force the appropriate index to avoid bad query plans (T307815 and T307295)
365        if ( isset( $revWhere['orconds']['newactor'] ) ) {
366            $this->addOption( 'USE INDEX', [ 'revision' => 'rev_actor_timestamp' ] );
367        }
368
369        // Handle continue parameter
370        if ( $this->params['continue'] !== null ) {
371            if ( $this->multiUserMode ) {
372                $continue = $this->parseContinueParamOrDie( $this->params['continue'],
373                    [ 'string', 'string', 'timestamp', 'int' ] );
374                $modeFlag = array_shift( $continue );
375                $this->dieContinueUsageIf( $modeFlag !== $this->orderBy );
376                $encUser = array_shift( $continue );
377            } else {
378                $continue = $this->parseContinueParamOrDie( $this->params['continue'],
379                    [ 'timestamp', 'int' ] );
380            }
381            $op = ( $this->params['dir'] == 'older' ? '<=' : '>=' );
382            if ( $this->multiUserMode ) {
383                $this->addWhere( $db->buildComparison( $op, [
384                    // @phan-suppress-next-line PhanPossiblyUndeclaredVariable encUser is set when used
385                    $userField => $encUser,
386                    $tsField => $db->timestamp( $continue[0] ),
387                    $idField => $continue[1],
388                ] ) );
389            } else {
390                $this->addWhere( $db->buildComparison( $op, [
391                    $tsField => $db->timestamp( $continue[0] ),
392                    $idField => $continue[1],
393                ] ) );
394            }
395        }
396
397        // Don't include any revisions where we're not supposed to be able to
398        // see the username.
399        if ( !$this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
400            $bitmask = RevisionRecord::DELETED_USER;
401        } elseif ( !$this->getAuthority()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
402            $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
403        } else {
404            $bitmask = 0;
405        }
406        if ( $bitmask ) {
407            $this->addWhere( $db->bitAnd( 'rev_deleted', $bitmask ) . " != $bitmask" );
408        }
409
410        // Add the user field to ORDER BY if there are multiple users
411        if ( count( $users ) > 1 ) {
412            $this->addWhereRange( $orderUserField, $this->params['dir'], null, null );
413        }
414
415        // Then timestamp
416        $this->addTimestampWhereRange( $tsField,
417            $this->params['dir'], $this->params['start'], $this->params['end'] );
418
419        // Then rev_id for a total ordering
420        $this->addWhereRange( $idField, $this->params['dir'], null, null );
421
422        $this->addWhereFld( 'page_namespace', $this->params['namespace'] );
423
424        $show = $this->params['show'];
425        if ( $this->params['toponly'] ) { // deprecated/old param
426            $show[] = 'top';
427        }
428        if ( $show !== null ) {
429            /** @var array<string,true> $show */
430            $show = array_fill_keys( $show, true );
431
432            foreach ( $show as $key => $_ ) {
433                // If there is a negated and non-negated option the same time
434                if ( str_starts_with( $key, '!' ) && isset( $show[substr( $key, 1 )] ) ) {
435                    $this->dieWithError( 'apierror-show' );
436                }
437            }
438            if ( isset( $show['autopatrolled'] ) && isset( $show['!patrolled'] ) ) {
439                $this->dieWithError( 'apierror-show' );
440            }
441
442            $this->addWhereIf( [ 'rev_minor_edit' => 0 ], isset( $show['!minor'] ) );
443            $this->addWhereIf( $db->expr( 'rev_minor_edit', '!=', 0 ), isset( $show['minor'] ) );
444            $this->addWhereIf(
445                [ 'rc_patrolled' => RecentChange::PRC_UNPATROLLED ],
446                isset( $show['!patrolled'] )
447            );
448            $this->addWhereIf(
449                $db->expr( 'rc_patrolled', '!=', RecentChange::PRC_UNPATROLLED ),
450                isset( $show['patrolled'] )
451            );
452            $this->addWhereIf(
453                $db->expr( 'rc_patrolled', '!=', RecentChange::PRC_AUTOPATROLLED ),
454                isset( $show['!autopatrolled'] )
455            );
456            $this->addWhereIf(
457                [ 'rc_patrolled' => RecentChange::PRC_AUTOPATROLLED ],
458                isset( $show['autopatrolled'] )
459            );
460            $this->addWhereIf( $idField . ' != page_latest', isset( $show['!top'] ) );
461            $this->addWhereIf( $idField . ' = page_latest', isset( $show['top'] ) );
462            $this->addWhereIf( $db->expr( 'rev_parent_id', '!=', 0 ), isset( $show['!new'] ) );
463            $this->addWhereIf( [ 'rev_parent_id' => 0 ], isset( $show['new'] ) );
464        }
465        $this->addOption( 'LIMIT', $limit + 1 );
466
467        if ( isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) ||
468            isset( $show['autopatrolled'] ) || isset( $show['!autopatrolled'] ) ||
469            isset( $this->prop['patrolled'] )
470        ) {
471            $user = $this->getUser();
472            if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) {
473                $this->dieWithError( 'apierror-permissiondenied-patrolflag', 'permissiondenied' );
474            }
475
476            $isFilterset = isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) ||
477                isset( $show['autopatrolled'] ) || isset( $show['!autopatrolled'] );
478            $this->addTables( 'recentchanges' );
479            $this->addJoinConds( [ 'recentchanges' => [
480                $isFilterset ? 'JOIN' : 'LEFT JOIN',
481                [ 'rc_this_oldid = ' . $idField ]
482            ] ] );
483        }
484
485        $this->addFieldsIf( 'rc_patrolled', isset( $this->prop['patrolled'] ) );
486
487        if ( isset( $this->prop['tags'] ) ) {
488            $this->addFields( [
489                'ts_tags' => $this->changeTagsStore->makeTagSummarySubquery( 'revision' )
490            ] );
491        }
492
493        if ( isset( $this->params['tag'] ) ) {
494            $this->addTables( 'change_tag' );
495            $this->addJoinConds(
496                [ 'change_tag' => [ 'JOIN', [ $idField . ' = ct_rev_id' ] ] ]
497            );
498            try {
499                $this->addWhereFld( 'ct_tag_id', $this->changeTagDefStore->getId( $this->params['tag'] ) );
500            } catch ( NameTableAccessException ) {
501                // Return nothing.
502                $this->addWhere( '1=0' );
503            }
504        }
505        $this->addOption(
506            'MAX_EXECUTION_TIME',
507            $this->getConfig()->get( MainConfigNames::MaxExecutionTimeForExpensiveQueries )
508        );
509    }
510
511    /**
512     * Extract fields from the database row and append them to a result array
513     *
514     * @param stdClass $row
515     * @return array
516     */
517    private function extractRowInfo( $row ) {
518        $vals = [];
519        $anyHidden = false;
520
521        if ( $row->rev_deleted & RevisionRecord::DELETED_TEXT ) {
522            $vals['texthidden'] = true;
523            $anyHidden = true;
524        }
525
526        // Any rows where we can't view the user were filtered out in the query.
527        $vals['userid'] = (int)$row->rev_user;
528        $vals['user'] = $row->rev_user_text;
529        if ( $row->rev_deleted & RevisionRecord::DELETED_USER ) {
530            $vals['userhidden'] = true;
531            $anyHidden = true;
532        }
533        if ( $this->prop['ids'] ?? false ) {
534            $vals['pageid'] = (int)$row->rev_page;
535            $vals['revid'] = (int)$row->rev_id;
536
537            if ( $row->rev_parent_id !== null ) {
538                $vals['parentid'] = (int)$row->rev_parent_id;
539            }
540        }
541
542        $title = Title::makeTitle( $row->page_namespace, $row->page_title );
543
544        if ( isset( $this->prop['title'] ) ) {
545            ApiQueryBase::addTitleInfo( $vals, $title );
546        }
547
548        if ( isset( $this->prop['timestamp'] ) ) {
549            $vals['timestamp'] = wfTimestamp( TS::ISO_8601, $row->rev_timestamp );
550        }
551
552        if ( isset( $this->prop['flags'] ) ) {
553            $vals['new'] = $row->rev_parent_id == 0 && $row->rev_parent_id !== null;
554            $vals['minor'] = (bool)$row->rev_minor_edit;
555            $vals['top'] = $row->page_latest == $row->rev_id;
556        }
557
558        if ( isset( $this->prop['comment'] ) || isset( $this->prop['parsedcomment'] ) ) {
559            if ( $row->rev_deleted & RevisionRecord::DELETED_COMMENT ) {
560                $vals['commenthidden'] = true;
561                $anyHidden = true;
562            }
563
564            $userCanView = RevisionRecord::userCanBitfield(
565                $row->rev_deleted,
566                RevisionRecord::DELETED_COMMENT, $this->getAuthority()
567            );
568
569            if ( $userCanView ) {
570                $comment = $this->commentStore->getComment( 'rev_comment', $row )->text;
571                if ( isset( $this->prop['comment'] ) ) {
572                    $vals['comment'] = $comment;
573                }
574
575                if ( isset( $this->prop['parsedcomment'] ) ) {
576                    $vals['parsedcomment'] = $this->commentFormatter->format( $comment, $title );
577                }
578            }
579        }
580
581        if ( isset( $this->prop['patrolled'] ) ) {
582            $vals['patrolled'] = $row->rc_patrolled != RecentChange::PRC_UNPATROLLED;
583            $vals['autopatrolled'] = $row->rc_patrolled == RecentChange::PRC_AUTOPATROLLED;
584        }
585
586        if ( isset( $this->prop['size'] ) && $row->rev_len !== null ) {
587            $vals['size'] = (int)$row->rev_len;
588        }
589
590        if ( isset( $this->prop['sizediff'] )
591            && $row->rev_len !== null
592            && $row->rev_parent_id !== null
593        ) {
594            $parentLen = $this->parentLens[$row->rev_parent_id] ?? 0;
595            $vals['sizediff'] = (int)$row->rev_len - $parentLen;
596        }
597
598        if ( isset( $this->prop['tags'] ) ) {
599            if ( $row->ts_tags ) {
600                $tags = explode( ',', $row->ts_tags );
601                ApiResult::setIndexedTagName( $tags, 'tag' );
602                $vals['tags'] = $tags;
603            } else {
604                $vals['tags'] = [];
605            }
606        }
607
608        if ( $anyHidden && ( $row->rev_deleted & RevisionRecord::DELETED_RESTRICTED ) ) {
609            $vals['suppressed'] = true;
610        }
611
612        return $vals;
613    }
614
615    private function continueStr( \stdClass $row ): string {
616        if ( $this->multiUserMode ) {
617            switch ( $this->orderBy ) {
618                case 'name':
619                    return "name|$row->rev_user_text|$row->rev_timestamp|$row->rev_id";
620                case 'actor':
621                    return "actor|$row->rev_actor|$row->rev_timestamp|$row->rev_id";
622            }
623        } else {
624            return "$row->rev_timestamp|$row->rev_id";
625        }
626    }
627
628    /** @inheritDoc */
629    public function getCacheMode( $params ) {
630        // This module provides access to deleted revisions and patrol flags if
631        // the requester is logged in
632        return 'anon-public-user-private';
633    }
634
635    /** @inheritDoc */
636    public function getAllowedParams() {
637        return [
638            'limit' => [
639                ParamValidator::PARAM_DEFAULT => 10,
640                ParamValidator::PARAM_TYPE => 'limit',
641                IntegerDef::PARAM_MIN => 1,
642                IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1,
643                IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2
644            ],
645            'start' => [
646                ParamValidator::PARAM_TYPE => 'timestamp'
647            ],
648            'end' => [
649                ParamValidator::PARAM_TYPE => 'timestamp'
650            ],
651            'continue' => [
652                ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
653            ],
654            'user' => [
655                ParamValidator::PARAM_TYPE => 'user',
656                UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'interwiki' ],
657                ParamValidator::PARAM_ISMULTI => true
658            ],
659            'userids' => [
660                ParamValidator::PARAM_TYPE => 'integer',
661                ParamValidator::PARAM_ISMULTI => true
662            ],
663            'userprefix' => null,
664            'iprange' => null,
665            'dir' => [
666                ParamValidator::PARAM_DEFAULT => 'older',
667                ParamValidator::PARAM_TYPE => [
668                    'newer',
669                    'older'
670                ],
671                ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
672                ApiBase::PARAM_HELP_MSG_PER_VALUE => [
673                    'newer' => 'api-help-paramvalue-direction-newer',
674                    'older' => 'api-help-paramvalue-direction-older',
675                ],
676            ],
677            'namespace' => [
678                ParamValidator::PARAM_ISMULTI => true,
679                ParamValidator::PARAM_TYPE => 'namespace'
680            ],
681            'prop' => [
682                ParamValidator::PARAM_ISMULTI => true,
683                ParamValidator::PARAM_DEFAULT => 'ids|title|timestamp|comment|size|flags',
684                ParamValidator::PARAM_TYPE => [
685                    'ids',
686                    'title',
687                    'timestamp',
688                    'comment',
689                    'parsedcomment',
690                    'size',
691                    'sizediff',
692                    'flags',
693                    'patrolled',
694                    'tags'
695                ],
696                ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
697            ],
698            'show' => [
699                ParamValidator::PARAM_ISMULTI => true,
700                ParamValidator::PARAM_TYPE => [
701                    'minor',
702                    '!minor',
703                    'patrolled',
704                    '!patrolled',
705                    'autopatrolled',
706                    '!autopatrolled',
707                    'top',
708                    '!top',
709                    'new',
710                    '!new',
711                ],
712                ApiBase::PARAM_HELP_MSG => [
713                    'apihelp-query+usercontribs-param-show',
714                    $this->getConfig()->get( MainConfigNames::RCMaxAge )
715                ],
716            ],
717            'tag' => null,
718            'toponly' => [
719                ParamValidator::PARAM_DEFAULT => false,
720                ParamValidator::PARAM_DEPRECATED => true,
721            ],
722        ];
723    }
724
725    /** @inheritDoc */
726    protected function getExamplesMessages() {
727        return [
728            'action=query&list=usercontribs&ucuser=Example'
729                => 'apihelp-query+usercontribs-example-user',
730            'action=query&list=usercontribs&ucuserprefix=192.0.2.'
731                => 'apihelp-query+usercontribs-example-ipprefix',
732        ];
733    }
734
735    /** @inheritDoc */
736    public function getHelpUrls() {
737        return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Usercontribs';
738    }
739}
740
741/** @deprecated class alias since 1.43 */
742class_alias( ApiQueryUserContribs::class, 'ApiQueryUserContribs' );