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