Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
71.83% |
334 / 465 |
|
33.33% |
3 / 9 |
CRAP | |
0.00% |
0 / 1 |
ApiQueryUserContribs | |
71.83% |
334 / 465 |
|
33.33% |
3 / 9 |
423.07 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
execute | |
82.81% |
159 / 192 |
|
0.00% |
0 / 1 |
53.83 | |||
prepareQuery | |
46.73% |
50 / 107 |
|
0.00% |
0 / 1 |
268.93 | |||
extractRowInfo | |
42.11% |
24 / 57 |
|
0.00% |
0 / 1 |
146.28 | |||
continueStr | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
4.05 | |||
getCacheMode | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getAllowedParams | |
100.00% |
86 / 86 |
|
100.00% |
1 / 1 |
1 | |||
getExamplesMessages | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
getHelpUrls | |
0.00% |
0 / 1 |
|
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 | |
23 | use MediaWiki\CommentFormatter\CommentFormatter; |
24 | use MediaWiki\CommentStore\CommentStore; |
25 | use MediaWiki\MainConfigNames; |
26 | use MediaWiki\ParamValidator\TypeDef\UserDef; |
27 | use MediaWiki\Revision\RevisionRecord; |
28 | use MediaWiki\Revision\RevisionStore; |
29 | use MediaWiki\Storage\NameTableAccessException; |
30 | use MediaWiki\Storage\NameTableStore; |
31 | use MediaWiki\Title\Title; |
32 | use MediaWiki\User\ActorMigration; |
33 | use MediaWiki\User\ExternalUserNames; |
34 | use MediaWiki\User\UserIdentity; |
35 | use MediaWiki\User\UserIdentityLookup; |
36 | use MediaWiki\User\UserIdentityValue; |
37 | use MediaWiki\User\UserNameUtils; |
38 | use Wikimedia\IPUtils; |
39 | use Wikimedia\ParamValidator\ParamValidator; |
40 | use Wikimedia\ParamValidator\TypeDef\IntegerDef; |
41 | use 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 | */ |
48 | class 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 | } |