Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
76.62% |
213 / 278 |
|
42.86% |
3 / 7 |
CRAP | |
0.00% |
0 / 1 |
| ApiQueryAllUsers | |
76.90% |
213 / 277 |
|
42.86% |
3 / 7 |
104.40 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getCanonicalUserName | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| execute | |
71.29% |
144 / 202 |
|
0.00% |
0 / 1 |
119.49 | |||
| getCacheMode | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getAllowedParams | |
98.48% |
65 / 66 |
|
0.00% |
0 / 1 |
2 | |||
| getExamplesMessages | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
| getHelpUrls | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * Copyright © 2007 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" |
| 4 | * |
| 5 | * @license GPL-2.0-or-later |
| 6 | * @file |
| 7 | */ |
| 8 | |
| 9 | namespace MediaWiki\Api; |
| 10 | |
| 11 | use MediaWiki\Language\Language; |
| 12 | use MediaWiki\MainConfigNames; |
| 13 | use MediaWiki\Permissions\GroupPermissionsLookup; |
| 14 | use MediaWiki\RecentChanges\RecentChangeLookup; |
| 15 | use MediaWiki\User\TempUser\TempUserConfig; |
| 16 | use MediaWiki\User\TempUser\TempUserDetailsLookup; |
| 17 | use MediaWiki\User\UserFactory; |
| 18 | use MediaWiki\User\UserGroupManager; |
| 19 | use MediaWiki\User\UserIdentityValue; |
| 20 | use Wikimedia\ParamValidator\ParamValidator; |
| 21 | use Wikimedia\ParamValidator\TypeDef\IntegerDef; |
| 22 | use Wikimedia\Rdbms\IExpression; |
| 23 | use Wikimedia\Rdbms\LikeValue; |
| 24 | |
| 25 | /** |
| 26 | * Query module to enumerate all registered users. |
| 27 | * |
| 28 | * @ingroup API |
| 29 | */ |
| 30 | class ApiQueryAllUsers extends ApiQueryBase { |
| 31 | use ApiQueryBlockInfoTrait; |
| 32 | |
| 33 | public function __construct( |
| 34 | ApiQuery $query, |
| 35 | string $moduleName, |
| 36 | private readonly UserFactory $userFactory, |
| 37 | private readonly UserGroupManager $userGroupManager, |
| 38 | private readonly GroupPermissionsLookup $groupPermissionsLookup, |
| 39 | private readonly Language $contentLanguage, |
| 40 | private readonly TempUserConfig $tempUserConfig, |
| 41 | private readonly RecentChangeLookup $recentChangeLookup, |
| 42 | private readonly TempUserDetailsLookup $tempUserDetailsLookup |
| 43 | ) { |
| 44 | parent::__construct( $query, $moduleName, 'au' ); |
| 45 | } |
| 46 | |
| 47 | /** |
| 48 | * This function converts the user name to a canonical form |
| 49 | * which is stored in the database. |
| 50 | * @param string $name |
| 51 | * @return string |
| 52 | */ |
| 53 | private function getCanonicalUserName( $name ) { |
| 54 | $name = $this->contentLanguage->ucfirst( $name ); |
| 55 | return strtr( $name, '_', ' ' ); |
| 56 | } |
| 57 | |
| 58 | public function execute() { |
| 59 | $params = $this->extractRequestParams(); |
| 60 | $activeUserDays = $this->getConfig()->get( MainConfigNames::ActiveUserDays ); |
| 61 | |
| 62 | $db = $this->getDB(); |
| 63 | |
| 64 | $prop = $params['prop']; |
| 65 | if ( $prop !== null ) { |
| 66 | $prop = array_fill_keys( $prop, true ); |
| 67 | $fld_blockinfo = isset( $prop['blockinfo'] ); |
| 68 | $fld_editcount = isset( $prop['editcount'] ); |
| 69 | $fld_groups = isset( $prop['groups'] ); |
| 70 | $fld_rights = isset( $prop['rights'] ); |
| 71 | $fld_registration = isset( $prop['registration'] ); |
| 72 | $fld_implicitgroups = isset( $prop['implicitgroups'] ); |
| 73 | $fld_centralids = isset( $prop['centralids'] ); |
| 74 | $fld_tempexpired = isset( $prop['tempexpired'] ); |
| 75 | } else { |
| 76 | $fld_blockinfo = $fld_editcount = $fld_groups = $fld_registration = |
| 77 | $fld_rights = $fld_implicitgroups = $fld_centralids = $fld_tempexpired = false; |
| 78 | } |
| 79 | |
| 80 | $limit = $params['limit']; |
| 81 | |
| 82 | $this->addTables( 'user' ); |
| 83 | |
| 84 | $dir = ( $params['dir'] == 'descending' ? 'older' : 'newer' ); |
| 85 | $from = $params['from'] === null ? null : $this->getCanonicalUserName( $params['from'] ); |
| 86 | $to = $params['to'] === null ? null : $this->getCanonicalUserName( $params['to'] ); |
| 87 | |
| 88 | # MySQL can't figure out that 'user_name' and 'qcc_title' are the same |
| 89 | # despite the JOIN condition, so manually sort on the correct one. |
| 90 | $userFieldToSort = $params['activeusers'] ? 'qcc_title' : 'user_name'; |
| 91 | |
| 92 | # Some of these subtable joins are going to give us duplicate rows, so |
| 93 | # calculate the maximum number of duplicates we might see. |
| 94 | $maxDuplicateRows = 1; |
| 95 | |
| 96 | $this->addWhereRange( $userFieldToSort, $dir, $from, $to ); |
| 97 | |
| 98 | if ( $params['prefix'] !== null ) { |
| 99 | $this->addWhere( |
| 100 | $db->expr( |
| 101 | $userFieldToSort, |
| 102 | IExpression::LIKE, |
| 103 | new LikeValue( $this->getCanonicalUserName( $params['prefix'] ), $db->anyString() ) |
| 104 | ) |
| 105 | ); |
| 106 | } |
| 107 | |
| 108 | $excludeNamed = $params['excludenamed']; |
| 109 | $excludeTemp = $params['excludetemp']; |
| 110 | |
| 111 | if ( $this->tempUserConfig->isKnown() ) { |
| 112 | if ( $excludeTemp ) { |
| 113 | $this->addWhere( |
| 114 | $this->tempUserConfig->getMatchCondition( $db, 'user_name', IExpression::NOT_LIKE ) |
| 115 | ); |
| 116 | } |
| 117 | if ( $excludeNamed ) { |
| 118 | $this->addWhere( |
| 119 | $this->tempUserConfig->getMatchCondition( $db, 'user_name', IExpression::LIKE ) |
| 120 | ); |
| 121 | } |
| 122 | } |
| 123 | |
| 124 | if ( $params['rights'] !== null && count( $params['rights'] ) ) { |
| 125 | $groups = []; |
| 126 | // TODO: this does not properly account for $wgRevokePermissions |
| 127 | foreach ( $params['rights'] as $r ) { |
| 128 | if ( in_array( $r, $this->getPermissionManager()->getImplicitRights(), true ) ) { |
| 129 | $groups[] = '*'; |
| 130 | } else { |
| 131 | $groups = array_merge( |
| 132 | $groups, |
| 133 | $this->groupPermissionsLookup->getGroupsWithPermission( $r ) |
| 134 | ); |
| 135 | } |
| 136 | } |
| 137 | |
| 138 | if ( $groups === [] ) { |
| 139 | // No group with the given right(s) exists, no need for a query |
| 140 | $this->getResult()->addIndexedTagName( [ 'query', $this->getModuleName() ], '' ); |
| 141 | |
| 142 | return; |
| 143 | } |
| 144 | |
| 145 | $groups = array_unique( $groups ); |
| 146 | if ( in_array( '*', $groups, true ) || in_array( 'user', $groups, true ) ) { |
| 147 | // All user rows logically match but there are no "*"/"user" user_groups rows |
| 148 | $groups = []; |
| 149 | } |
| 150 | |
| 151 | if ( $params['group'] === null ) { |
| 152 | $params['group'] = $groups; |
| 153 | } else { |
| 154 | $params['group'] = array_unique( array_merge( $params['group'], $groups ) ); |
| 155 | } |
| 156 | } |
| 157 | |
| 158 | $this->requireMaxOneParameter( $params, 'group', 'excludegroup' ); |
| 159 | |
| 160 | if ( $params['group'] !== null && count( $params['group'] ) ) { |
| 161 | // Filter only users that belong to a given group. This might |
| 162 | // produce as many rows-per-user as there are groups being checked. |
| 163 | $this->addTables( 'user_groups', 'ug1' ); |
| 164 | $this->addJoinConds( [ |
| 165 | 'ug1' => [ |
| 166 | 'JOIN', |
| 167 | [ |
| 168 | 'ug1.ug_user=user_id', |
| 169 | 'ug1.ug_group' => $params['group'], |
| 170 | $db->expr( 'ug1.ug_expiry', '=', null )->or( 'ug1.ug_expiry', '>=', $db->timestamp() ), |
| 171 | ] |
| 172 | ] |
| 173 | ] ); |
| 174 | $maxDuplicateRows *= count( $params['group'] ); |
| 175 | } |
| 176 | |
| 177 | if ( $params['excludegroup'] !== null && count( $params['excludegroup'] ) ) { |
| 178 | // Filter only users don't belong to a given group. This can only |
| 179 | // produce one row-per-user, because we only keep on "no match". |
| 180 | $this->addTables( 'user_groups', 'ug1' ); |
| 181 | |
| 182 | $this->addJoinConds( [ 'ug1' => [ 'LEFT JOIN', |
| 183 | [ |
| 184 | 'ug1.ug_user=user_id', |
| 185 | $db->expr( 'ug1.ug_expiry', '=', null )->or( 'ug1.ug_expiry', '>=', $db->timestamp() ), |
| 186 | 'ug1.ug_group' => $params['excludegroup'], |
| 187 | ] |
| 188 | ] ] ); |
| 189 | $this->addWhere( [ 'ug1.ug_user' => null ] ); |
| 190 | } |
| 191 | |
| 192 | if ( $params['witheditsonly'] ) { |
| 193 | $this->addWhere( $db->expr( 'user_editcount', '>', 0 ) ); |
| 194 | } |
| 195 | |
| 196 | $this->addDeletedUserFilter(); |
| 197 | |
| 198 | if ( $fld_groups || $fld_rights ) { |
| 199 | $this->addFields( [ 'groups' => |
| 200 | $db->newSelectQueryBuilder() |
| 201 | ->table( 'user_groups' ) |
| 202 | ->field( 'ug_group' ) |
| 203 | ->where( [ |
| 204 | 'ug_user=user_id', |
| 205 | $db->expr( 'ug_expiry', '=', null )->or( 'ug_expiry', '>=', $db->timestamp() ) |
| 206 | ] ) |
| 207 | ->buildGroupConcatField( '|' ) |
| 208 | ] ); |
| 209 | } |
| 210 | |
| 211 | if ( $params['activeusers'] ) { |
| 212 | $activeUserSeconds = $activeUserDays * 86400; |
| 213 | |
| 214 | // Filter query to only include users in the active users cache. |
| 215 | // There shouldn't be any duplicate rows in querycachetwo here. |
| 216 | $this->addTables( 'querycachetwo' ); |
| 217 | $this->addJoinConds( [ 'querycachetwo' => [ |
| 218 | 'JOIN', [ |
| 219 | 'qcc_type' => 'activeusers', |
| 220 | 'qcc_namespace' => NS_USER, |
| 221 | 'qcc_title=user_name', |
| 222 | ], |
| 223 | ] ] ); |
| 224 | |
| 225 | // Actually count the actions using a subquery (T66505 and T66507) |
| 226 | $timestamp = $db->timestamp( (int)wfTimestamp( TS_UNIX ) - $activeUserSeconds ); |
| 227 | $subqueryBuilder = $db->newSelectQueryBuilder() |
| 228 | ->select( 'COUNT(*)' ) |
| 229 | ->from( 'recentchanges' ) |
| 230 | ->join( 'actor', null, 'rc_actor = actor_id' ) |
| 231 | ->where( [ |
| 232 | 'actor_user = user_id', |
| 233 | $db->expr( 'rc_source', '=', $this->recentChangeLookup->getPrimarySources() ), |
| 234 | $db->expr( 'rc_log_type', '=', null ) |
| 235 | ->or( 'rc_log_type', '!=', 'newusers' ), |
| 236 | $db->expr( 'rc_timestamp', '>=', $timestamp ), |
| 237 | ] ); |
| 238 | $this->addFields( [ |
| 239 | 'recentactions' => '(' . $subqueryBuilder->caller( __METHOD__ )->getSQL() . ')' |
| 240 | ] ); |
| 241 | } |
| 242 | |
| 243 | $sqlLimit = $limit + $maxDuplicateRows; |
| 244 | $this->addOption( 'LIMIT', $sqlLimit ); |
| 245 | |
| 246 | $this->addFields( [ |
| 247 | 'user_name', |
| 248 | 'user_id' |
| 249 | ] ); |
| 250 | $this->addFieldsIf( 'user_editcount', $fld_editcount ); |
| 251 | $this->addFieldsIf( 'user_registration', $fld_registration ); |
| 252 | |
| 253 | $res = $this->select( __METHOD__ ); |
| 254 | $count = 0; |
| 255 | $countDuplicates = 0; |
| 256 | $lastUser = false; |
| 257 | $result = $this->getResult(); |
| 258 | $blockInfos = $fld_blockinfo ? $this->getBlockDetailsForRows( $res ) : null; |
| 259 | foreach ( $res as $row ) { |
| 260 | $count++; |
| 261 | |
| 262 | if ( $lastUser === $row->user_name ) { |
| 263 | // Duplicate row due to one of the needed subtable joins. |
| 264 | // Ignore it, but count the number of them to sensibly handle |
| 265 | // miscalculation of $maxDuplicateRows. |
| 266 | $countDuplicates++; |
| 267 | if ( $countDuplicates == $maxDuplicateRows ) { |
| 268 | ApiBase::dieDebug( __METHOD__, 'Saw more duplicate rows than expected' ); |
| 269 | } |
| 270 | continue; |
| 271 | } |
| 272 | |
| 273 | $countDuplicates = 0; |
| 274 | $lastUser = $row->user_name; |
| 275 | |
| 276 | if ( $count > $limit ) { |
| 277 | // We've reached the one extra which shows that there are |
| 278 | // additional pages to be had. Stop here... |
| 279 | $this->setContinueEnumParameter( 'from', $row->user_name ); |
| 280 | break; |
| 281 | } |
| 282 | |
| 283 | if ( $count == $sqlLimit ) { |
| 284 | // Should never hit this (either the $countDuplicates check or |
| 285 | // the $count > $limit check should hit first), but check it |
| 286 | // anyway just in case. |
| 287 | ApiBase::dieDebug( __METHOD__, 'Saw more duplicate rows than expected' ); |
| 288 | } |
| 289 | |
| 290 | if ( $params['activeusers'] && (int)$row->recentactions === 0 ) { |
| 291 | // activeusers cache was out of date |
| 292 | continue; |
| 293 | } |
| 294 | |
| 295 | $data = [ |
| 296 | 'userid' => (int)$row->user_id, |
| 297 | 'name' => $row->user_name, |
| 298 | ]; |
| 299 | |
| 300 | if ( $fld_centralids ) { |
| 301 | $data += ApiQueryUserInfo::getCentralUserInfo( |
| 302 | $this->getConfig(), $this->userFactory->newFromId( (int)$row->user_id ), $params['attachedwiki'] |
| 303 | ); |
| 304 | } |
| 305 | |
| 306 | if ( $fld_blockinfo && isset( $blockInfos[$row->user_id] ) ) { |
| 307 | $data += $blockInfos[$row->user_id]; |
| 308 | } |
| 309 | if ( $row->hu_deleted ) { |
| 310 | $data['hidden'] = true; |
| 311 | } |
| 312 | if ( $fld_editcount ) { |
| 313 | $data['editcount'] = (int)$row->user_editcount; |
| 314 | } |
| 315 | if ( $params['activeusers'] ) { |
| 316 | $data['recentactions'] = (int)$row->recentactions; |
| 317 | } |
| 318 | if ( $fld_registration ) { |
| 319 | $data['registration'] = $row->user_registration ? |
| 320 | wfTimestamp( TS_ISO_8601, $row->user_registration ) : ''; |
| 321 | } |
| 322 | |
| 323 | if ( $fld_implicitgroups || $fld_groups || $fld_rights ) { |
| 324 | $implicitGroups = $this->userGroupManager |
| 325 | ->getUserImplicitGroups( $this->userFactory->newFromId( (int)$row->user_id ) ); |
| 326 | if ( isset( $row->groups ) && $row->groups !== '' ) { |
| 327 | $groups = array_merge( $implicitGroups, explode( '|', $row->groups ) ); |
| 328 | } else { |
| 329 | $groups = $implicitGroups; |
| 330 | } |
| 331 | |
| 332 | if ( $fld_groups ) { |
| 333 | $data['groups'] = $groups; |
| 334 | ApiResult::setIndexedTagName( $data['groups'], 'g' ); |
| 335 | ApiResult::setArrayType( $data['groups'], 'array' ); |
| 336 | } |
| 337 | |
| 338 | if ( $fld_implicitgroups ) { |
| 339 | $data['implicitgroups'] = $implicitGroups; |
| 340 | ApiResult::setIndexedTagName( $data['implicitgroups'], 'g' ); |
| 341 | ApiResult::setArrayType( $data['implicitgroups'], 'array' ); |
| 342 | } |
| 343 | |
| 344 | if ( $fld_rights ) { |
| 345 | $user = $this->userFactory->newFromId( (int)$row->user_id ); |
| 346 | $data['rights'] = $this->getPermissionManager()->getUserPermissions( $user ); |
| 347 | ApiResult::setIndexedTagName( $data['rights'], 'r' ); |
| 348 | ApiResult::setArrayType( $data['rights'], 'array' ); |
| 349 | } |
| 350 | } |
| 351 | |
| 352 | if ( $fld_tempexpired ) { |
| 353 | if ( $this->tempUserConfig->isTempName( $row->user_name ) ) { |
| 354 | $userIdentity = UserIdentityValue::newRegistered( $row->user_id, $row->user_name ); |
| 355 | $data['tempexpired'] = $this->tempUserDetailsLookup->isExpired( $userIdentity ); |
| 356 | } else { |
| 357 | $data['tempexpired'] = null; |
| 358 | } |
| 359 | } |
| 360 | |
| 361 | $fit = $result->addValue( [ 'query', $this->getModuleName() ], null, $data ); |
| 362 | if ( !$fit ) { |
| 363 | $this->setContinueEnumParameter( 'from', $data['name'] ); |
| 364 | break; |
| 365 | } |
| 366 | } |
| 367 | |
| 368 | $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'u' ); |
| 369 | } |
| 370 | |
| 371 | /** @inheritDoc */ |
| 372 | public function getCacheMode( $params ) { |
| 373 | return 'anon-public-user-private'; |
| 374 | } |
| 375 | |
| 376 | /** @inheritDoc */ |
| 377 | public function getAllowedParams( $flags = 0 ) { |
| 378 | $userGroups = $this->userGroupManager->listAllGroups(); |
| 379 | |
| 380 | if ( $flags & ApiBase::GET_VALUES_FOR_HELP ) { |
| 381 | sort( $userGroups ); |
| 382 | } |
| 383 | |
| 384 | return [ |
| 385 | 'from' => null, |
| 386 | 'to' => null, |
| 387 | 'prefix' => null, |
| 388 | 'dir' => [ |
| 389 | ParamValidator::PARAM_DEFAULT => 'ascending', |
| 390 | ParamValidator::PARAM_TYPE => [ |
| 391 | 'ascending', |
| 392 | 'descending' |
| 393 | ], |
| 394 | ], |
| 395 | 'group' => [ |
| 396 | ParamValidator::PARAM_TYPE => $userGroups, |
| 397 | ParamValidator::PARAM_ISMULTI => true, |
| 398 | ], |
| 399 | 'excludegroup' => [ |
| 400 | ParamValidator::PARAM_TYPE => $userGroups, |
| 401 | ParamValidator::PARAM_ISMULTI => true, |
| 402 | ], |
| 403 | 'rights' => [ |
| 404 | ParamValidator::PARAM_TYPE => array_unique( array_merge( |
| 405 | $this->getPermissionManager()->getAllPermissions(), |
| 406 | $this->getPermissionManager()->getImplicitRights() |
| 407 | ) ), |
| 408 | ParamValidator::PARAM_ISMULTI => true, |
| 409 | ], |
| 410 | 'prop' => [ |
| 411 | ParamValidator::PARAM_ISMULTI => true, |
| 412 | ParamValidator::PARAM_TYPE => [ |
| 413 | 'blockinfo', |
| 414 | 'groups', |
| 415 | 'implicitgroups', |
| 416 | 'rights', |
| 417 | 'editcount', |
| 418 | 'registration', |
| 419 | 'centralids', |
| 420 | 'tempexpired', |
| 421 | ], |
| 422 | ApiBase::PARAM_HELP_MSG_PER_VALUE => [], |
| 423 | ], |
| 424 | 'limit' => [ |
| 425 | ParamValidator::PARAM_DEFAULT => 10, |
| 426 | ParamValidator::PARAM_TYPE => 'limit', |
| 427 | IntegerDef::PARAM_MIN => 1, |
| 428 | IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1, |
| 429 | IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2 |
| 430 | ], |
| 431 | 'witheditsonly' => false, |
| 432 | 'activeusers' => [ |
| 433 | ParamValidator::PARAM_DEFAULT => false, |
| 434 | ApiBase::PARAM_HELP_MSG => [ |
| 435 | 'apihelp-query+allusers-param-activeusers', |
| 436 | $this->getConfig()->get( MainConfigNames::ActiveUserDays ) |
| 437 | ], |
| 438 | ], |
| 439 | 'attachedwiki' => null, |
| 440 | 'excludenamed' => [ |
| 441 | ParamValidator::PARAM_TYPE => 'boolean', |
| 442 | ], |
| 443 | 'excludetemp' => [ |
| 444 | ParamValidator::PARAM_TYPE => 'boolean', |
| 445 | ], |
| 446 | ]; |
| 447 | } |
| 448 | |
| 449 | /** @inheritDoc */ |
| 450 | protected function getExamplesMessages() { |
| 451 | return [ |
| 452 | 'action=query&list=allusers&aufrom=Y' |
| 453 | => 'apihelp-query+allusers-example-y', |
| 454 | ]; |
| 455 | } |
| 456 | |
| 457 | /** @inheritDoc */ |
| 458 | public function getHelpUrls() { |
| 459 | return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Allusers'; |
| 460 | } |
| 461 | } |
| 462 | |
| 463 | /** @deprecated class alias since 1.43 */ |
| 464 | class_alias( ApiQueryAllUsers::class, 'ApiQueryAllUsers' ); |