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