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