Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
71.43% |
335 / 469 |
|
33.33% |
3 / 9 |
CRAP | |
0.00% |
0 / 1 |
ApiQueryUserContribs | |
71.58% |
335 / 468 |
|
33.33% |
3 / 9 |
431.19 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
execute | |
82.81% |
159 / 192 |
|
0.00% |
0 / 1 |
53.83 | |||
prepareQuery | |
45.87% |
50 / 109 |
|
0.00% |
0 / 1 |
280.22 | |||
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 | namespace MediaWiki\Api; |
24 | |
25 | use MediaWiki\ChangeTags\ChangeTagsStore; |
26 | use MediaWiki\CommentFormatter\CommentFormatter; |
27 | use MediaWiki\CommentStore\CommentStore; |
28 | use MediaWiki\MainConfigNames; |
29 | use MediaWiki\ParamValidator\TypeDef\UserDef; |
30 | use MediaWiki\Revision\RevisionRecord; |
31 | use MediaWiki\Revision\RevisionStore; |
32 | use MediaWiki\Storage\NameTableAccessException; |
33 | use MediaWiki\Storage\NameTableStore; |
34 | use MediaWiki\Title\Title; |
35 | use MediaWiki\User\ActorMigration; |
36 | use MediaWiki\User\ExternalUserNames; |
37 | use MediaWiki\User\UserIdentity; |
38 | use MediaWiki\User\UserIdentityLookup; |
39 | use MediaWiki\User\UserIdentityValue; |
40 | use MediaWiki\User\UserNameUtils; |
41 | use RecentChange; |
42 | use stdClass; |
43 | use Wikimedia\IPUtils; |
44 | use Wikimedia\ParamValidator\ParamValidator; |
45 | use Wikimedia\ParamValidator\TypeDef\IntegerDef; |
46 | use 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 | */ |
53 | class 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 */ |
767 | class_alias( ApiQueryUserContribs::class, 'ApiQueryUserContribs' ); |