X-Git-Url: https://scripts.mit.edu/gitweb/autoinstallsdev/mediawiki.git/blobdiff_plain/19e297c21b10b1b8a3acad5e73fc71dcb35db44a..6932310fd58ebef145fa01eb76edf7150284d8ea:/includes/api/ApiQueryAllUsers.php diff --git a/includes/api/ApiQueryAllUsers.php b/includes/api/ApiQueryAllUsers.php index e31814d4..d594ad44 100644 --- a/includes/api/ApiQueryAllUsers.php +++ b/includes/api/ApiQueryAllUsers.php @@ -1,10 +1,10 @@ @gmail.com + * Copyright © 2007 Yuri Astrakhan "@gmail.com" * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -24,24 +24,32 @@ * @file */ -if ( !defined( 'MEDIAWIKI' ) ) { - // Eclipse helper - will be ignored in production - require_once( 'ApiQueryBase.php' ); -} - /** * Query module to enumerate all registered users. * * @ingroup API */ class ApiQueryAllUsers extends ApiQueryBase { - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'au' ); } + /** + * This function converts the user name to a canonical form + * which is stored in the database. + * @param string $name + * @return string + */ + private function getCanonicalUserName( $name ) { + return strtr( $name, '_', ' ' ); + } + public function execute() { - $db = $this->getDB(); $params = $this->extractRequestParams(); + $activeUserDays = $this->getConfig()->get( 'ActiveUserDays' ); + + $db = $this->getDB(); + $commentStore = new CommentStore( 'ipb_reason' ); $prop = $params['prop']; if ( !is_null( $prop ) ) { @@ -49,154 +57,267 @@ class ApiQueryAllUsers extends ApiQueryBase { $fld_blockinfo = isset( $prop['blockinfo'] ); $fld_editcount = isset( $prop['editcount'] ); $fld_groups = isset( $prop['groups'] ); + $fld_rights = isset( $prop['rights'] ); $fld_registration = isset( $prop['registration'] ); + $fld_implicitgroups = isset( $prop['implicitgroups'] ); + $fld_centralids = isset( $prop['centralids'] ); } else { - $fld_blockinfo = $fld_editcount = $fld_groups = $fld_registration = $fld_rights = false; + $fld_blockinfo = $fld_editcount = $fld_groups = $fld_registration = + $fld_rights = $fld_implicitgroups = $fld_centralids = false; } $limit = $params['limit']; - $this->addTables( 'user', 'u1' ); - $useIndex = true; - if ( !is_null( $params['from'] ) ) { - $this->addWhere( 'u1.user_name >= ' . $db->addQuotes( $this->keyToTitle( $params['from'] ) ) ); + $this->addTables( 'user' ); + + $dir = ( $params['dir'] == 'descending' ? 'older' : 'newer' ); + $from = is_null( $params['from'] ) ? null : $this->getCanonicalUserName( $params['from'] ); + $to = is_null( $params['to'] ) ? null : $this->getCanonicalUserName( $params['to'] ); + + # MySQL can't figure out that 'user_name' and 'qcc_title' are the same + # despite the JOIN condition, so manually sort on the correct one. + $userFieldToSort = $params['activeusers'] ? 'qcc_title' : 'user_name'; + + # Some of these subtable joins are going to give us duplicate rows, so + # calculate the maximum number of duplicates we might see. + $maxDuplicateRows = 1; + + $this->addWhereRange( $userFieldToSort, $dir, $from, $to ); + + if ( !is_null( $params['prefix'] ) ) { + $this->addWhere( $userFieldToSort . + $db->buildLike( $this->getCanonicalUserName( $params['prefix'] ), $db->anyString() ) ); } - if ( !is_null( $params['to'] ) ) { - $this->addWhere( 'u1.user_name <= ' . $db->addQuotes( $this->keyToTitle( $params['to'] ) ) ); + + if ( !is_null( $params['rights'] ) && count( $params['rights'] ) ) { + $groups = []; + foreach ( $params['rights'] as $r ) { + $groups = array_merge( $groups, User::getGroupsWithPermission( $r ) ); + } + + // no group with the given right(s) exists, no need for a query + if ( !count( $groups ) ) { + $this->getResult()->addIndexedTagName( [ 'query', $this->getModuleName() ], '' ); + + return; + } + + $groups = array_unique( $groups ); + + if ( is_null( $params['group'] ) ) { + $params['group'] = $groups; + } else { + $params['group'] = array_unique( array_merge( $params['group'], $groups ) ); + } } - if ( !is_null( $params['prefix'] ) ) { - $this->addWhere( 'u1.user_name' . $db->buildLike( $this->keyToTitle( $params['prefix'] ), $db->anyString() ) ); + $this->requireMaxOneParameter( $params, 'group', 'excludegroup' ); + + if ( !is_null( $params['group'] ) && count( $params['group'] ) ) { + // Filter only users that belong to a given group. This might + // produce as many rows-per-user as there are groups being checked. + $this->addTables( 'user_groups', 'ug1' ); + $this->addJoinConds( [ + 'ug1' => [ + 'INNER JOIN', + [ + 'ug1.ug_user=user_id', + 'ug1.ug_group' => $params['group'], + 'ug1.ug_expiry IS NULL OR ug1.ug_expiry >= ' . $db->addQuotes( $db->timestamp() ) + ] + ] + ] ); + $maxDuplicateRows *= count( $params['group'] ); } - if ( !is_null( $params['group'] ) ) { - $useIndex = false; - // Filter only users that belong to a given group + if ( !is_null( $params['excludegroup'] ) && count( $params['excludegroup'] ) ) { + // Filter only users don't belong to a given group. This can only + // produce one row-per-user, because we only keep on "no match". $this->addTables( 'user_groups', 'ug1' ); - $ug1 = $this->getAliasedName( 'user_groups', 'ug1' ); - $this->addJoinConds( array( $ug1 => array( 'INNER JOIN', array( 'ug1.ug_user=u1.user_id', - 'ug1.ug_group' => $params['group'] ) ) ) ); + + if ( count( $params['excludegroup'] ) == 1 ) { + $exclude = [ 'ug1.ug_group' => $params['excludegroup'][0] ]; + } else { + $exclude = [ $db->makeList( + [ 'ug1.ug_group' => $params['excludegroup'] ], + LIST_OR + ) ]; + } + $this->addJoinConds( [ 'ug1' => [ 'LEFT OUTER JOIN', + array_merge( [ + 'ug1.ug_user=user_id', + 'ug1.ug_expiry IS NULL OR ug1.ug_expiry >= ' . $db->addQuotes( $db->timestamp() ) + ], $exclude ) + ] ] ); + $this->addWhere( 'ug1.ug_user IS NULL' ); } if ( $params['witheditsonly'] ) { - $this->addWhere( 'u1.user_editcount > 0' ); + $this->addWhere( 'user_editcount > 0' ); } - if ( $fld_groups ) { - // Show the groups the given users belong to - // request more than needed to avoid not getting all rows that belong to one user - $groupCount = count( User::getAllGroups() ); - $sqlLimit = $limit + $groupCount + 1; + $this->showHiddenUsersAddBlockInfo( $fld_blockinfo ); - $this->addTables( 'user_groups', 'ug2' ); - $tname = $this->getAliasedName( 'user_groups', 'ug2' ); - $this->addJoinConds( array( $tname => array( 'LEFT JOIN', 'ug2.ug_user=u1.user_id' ) ) ); - $this->addFields( 'ug2.ug_group ug_group2' ); - } else { - $sqlLimit = $limit + 1; + if ( $fld_groups || $fld_rights ) { + $this->addFields( [ 'groups' => + $db->buildGroupConcatField( '|', 'user_groups', 'ug_group', [ + 'ug_user=user_id', + 'ug_expiry IS NULL OR ug_expiry >= ' . $db->addQuotes( $db->timestamp() ) + ] ) + ] ); + } + + if ( $params['activeusers'] ) { + $activeUserSeconds = $activeUserDays * 86400; + + // Filter query to only include users in the active users cache. + // There shouldn't be any duplicate rows in querycachetwo here. + $this->addTables( 'querycachetwo' ); + $this->addJoinConds( [ 'querycachetwo' => [ + 'INNER JOIN', [ + 'qcc_type' => 'activeusers', + 'qcc_namespace' => NS_USER, + 'qcc_title=user_name', + ], + ] ] ); + + // Actually count the actions using a subquery (T66505 and T66507) + $timestamp = $db->timestamp( wfTimestamp( TS_UNIX ) - $activeUserSeconds ); + $this->addFields( [ + 'recentactions' => '(' . $db->selectSQLText( + 'recentchanges', + 'COUNT(*)', + [ + 'rc_user_text = user_name', + 'rc_type != ' . $db->addQuotes( RC_EXTERNAL ), // no wikidata + 'rc_log_type IS NULL OR rc_log_type != ' . $db->addQuotes( 'newusers' ), + 'rc_timestamp >= ' . $db->addQuotes( $timestamp ), + ] + ) . ')' + ] ); } - $this->showHiddenUsersAddBlockInfo( $fld_blockinfo ); + $sqlLimit = $limit + $maxDuplicateRows; $this->addOption( 'LIMIT', $sqlLimit ); - $this->addFields( array( - 'u1.user_name', - 'u1.user_id' - ) ); - $this->addFieldsIf( 'u1.user_editcount', $fld_editcount ); - $this->addFieldsIf( 'u1.user_registration', $fld_registration ); - - $this->addOption( 'ORDER BY', 'u1.user_name' ); - if ( $useIndex ) { - $u1 = $this->getAliasedName( 'user', 'u1' ); - $this->addOption( 'USE INDEX', array( $u1 => 'user_name' ) ); - } + $this->addFields( [ + 'user_name', + 'user_id' + ] ); + $this->addFieldsIf( 'user_editcount', $fld_editcount ); + $this->addFieldsIf( 'user_registration', $fld_registration ); $res = $this->select( __METHOD__ ); - $count = 0; - $lastUserData = false; + $countDuplicates = 0; $lastUser = false; $result = $this->getResult(); - - // - // This loop keeps track of the last entry. - // For each new row, if the new row is for different user then the last, the last entry is added to results. - // Otherwise, the group of the new row is appended to the last entry. - // The setContinue... is more complex because of this, and takes into account the higher sql limit - // to make sure all rows that belong to the same user are received. - foreach ( $res as $row ) { $count++; - if ( $lastUser !== $row->user_name ) { - // Save the last pass's user data - if ( is_array( $lastUserData ) ) { - $fit = $result->addValue( array( 'query', $this->getModuleName() ), - null, $lastUserData ); + if ( $lastUser === $row->user_name ) { + // Duplicate row due to one of the needed subtable joins. + // Ignore it, but count the number of them to sanely handle + // miscalculation of $maxDuplicateRows. + $countDuplicates++; + if ( $countDuplicates == $maxDuplicateRows ) { + ApiBase::dieDebug( __METHOD__, 'Saw more duplicate rows than expected' ); + } + continue; + } - $lastUserData = null; + $countDuplicates = 0; + $lastUser = $row->user_name; - if ( !$fit ) { - $this->setContinueEnumParameter( 'from', - $this->keyToTitle( $lastUserData['name'] ) ); - break; - } - } + if ( $count > $limit ) { + // We've reached the one extra which shows that there are + // additional pages to be had. Stop here... + $this->setContinueEnumParameter( 'from', $row->user_name ); + break; + } - if ( $count > $limit ) { - // We've reached the one extra which shows that there are additional pages to be had. Stop here... - $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->user_name ) ); - break; - } + if ( $count == $sqlLimit ) { + // Should never hit this (either the $countDuplicates check or + // the $count > $limit check should hit first), but check it + // anyway just in case. + ApiBase::dieDebug( __METHOD__, 'Saw more duplicate rows than expected' ); + } - // Record new user's data - $lastUser = $row->user_name; - $lastUserData = array( - 'name' => $lastUser, - 'userid' => $row->user_id, - ); - if ( $fld_blockinfo && !is_null( $row->blocker_name ) ) { - $lastUserData['blockedby'] = $row->blocker_name; - $lastUserData['blockreason'] = $row->ipb_reason; - } - if ( $row->ipb_deleted ) { - $lastUserData['hidden'] = ''; - } - if ( $fld_editcount ) { - $lastUserData['editcount'] = intval( $row->user_editcount ); - } - if ( $fld_registration ) { - $lastUserData['registration'] = $row->user_registration ? - wfTimestamp( TS_ISO_8601, $row->user_registration ) : ''; - } + if ( $params['activeusers'] && $row->recentactions === 0 ) { + // activeusers cache was out of date + continue; + } + + $data = [ + 'userid' => (int)$row->user_id, + 'name' => $row->user_name, + ]; + if ( $fld_centralids ) { + $data += ApiQueryUserInfo::getCentralUserInfo( + $this->getConfig(), User::newFromId( $row->user_id ), $params['attachedwiki'] + ); } - if ( $sqlLimit == $count ) { - // BUG! database contains group name that User::getAllGroups() does not return - // TODO: should handle this more gracefully - ApiBase::dieDebug( __METHOD__, - 'MediaWiki configuration error: the database contains more user groups than known to User::getAllGroups() function' ); + if ( $fld_blockinfo && !is_null( $row->ipb_by_text ) ) { + $data['blockid'] = (int)$row->ipb_id; + $data['blockedby'] = $row->ipb_by_text; + $data['blockedbyid'] = (int)$row->ipb_by; + $data['blockedtimestamp'] = wfTimestamp( TS_ISO_8601, $row->ipb_timestamp ); + $data['blockreason'] = $commentStore->getComment( $row )->text; + $data['blockexpiry'] = $row->ipb_expiry; + } + if ( $row->ipb_deleted ) { + $data['hidden'] = true; + } + if ( $fld_editcount ) { + $data['editcount'] = intval( $row->user_editcount ); + } + if ( $params['activeusers'] ) { + $data['recentactions'] = intval( $row->recentactions ); + // @todo 'recenteditcount' is set for BC, remove in 1.25 + $data['recenteditcount'] = $data['recentactions']; } + if ( $fld_registration ) { + $data['registration'] = $row->user_registration ? + wfTimestamp( TS_ISO_8601, $row->user_registration ) : ''; + } + + if ( $fld_implicitgroups || $fld_groups || $fld_rights ) { + $implicitGroups = User::newFromId( $row->user_id )->getAutomaticGroups(); + if ( isset( $row->groups ) && $row->groups !== '' ) { + $groups = array_merge( $implicitGroups, explode( '|', $row->groups ) ); + } else { + $groups = $implicitGroups; + } + + if ( $fld_groups ) { + $data['groups'] = $groups; + ApiResult::setIndexedTagName( $data['groups'], 'g' ); + ApiResult::setArrayType( $data['groups'], 'array' ); + } - // Add user's group info - if ( $fld_groups && !is_null( $row->ug_group2 ) ) { - $lastUserData['groups'][] = $row->ug_group2; - $result->setIndexedTagName( $lastUserData['groups'], 'g' ); + if ( $fld_implicitgroups ) { + $data['implicitgroups'] = $implicitGroups; + ApiResult::setIndexedTagName( $data['implicitgroups'], 'g' ); + ApiResult::setArrayType( $data['implicitgroups'], 'array' ); + } + + if ( $fld_rights ) { + $data['rights'] = User::getGroupPermissions( $groups ); + ApiResult::setIndexedTagName( $data['rights'], 'r' ); + ApiResult::setArrayType( $data['rights'], 'array' ); + } } - } - if ( is_array( $lastUserData ) ) { - $fit = $result->addValue( array( 'query', $this->getModuleName() ), - null, $lastUserData ); + $fit = $result->addValue( [ 'query', $this->getModuleName() ], null, $data ); if ( !$fit ) { - $this->setContinueEnumParameter( 'from', - $this->keyToTitle( $lastUserData['name'] ) ); + $this->setContinueEnumParameter( 'from', $data['name'] ); + break; } } - $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'u' ); + $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'u' ); } public function getCacheMode( $params ) { @@ -204,62 +325,71 @@ class ApiQueryAllUsers extends ApiQueryBase { } public function getAllowedParams() { - return array( + $userGroups = User::getAllGroups(); + + return [ 'from' => null, 'to' => null, 'prefix' => null, - 'group' => array( - ApiBase::PARAM_TYPE => User::getAllGroups() - ), - 'prop' => array( + 'dir' => [ + ApiBase::PARAM_DFLT => 'ascending', + ApiBase::PARAM_TYPE => [ + 'ascending', + 'descending' + ], + ], + 'group' => [ + ApiBase::PARAM_TYPE => $userGroups, + ApiBase::PARAM_ISMULTI => true, + ], + 'excludegroup' => [ + ApiBase::PARAM_TYPE => $userGroups, + ApiBase::PARAM_ISMULTI => true, + ], + 'rights' => [ + ApiBase::PARAM_TYPE => User::getAllRights(), + ApiBase::PARAM_ISMULTI => true, + ], + 'prop' => [ ApiBase::PARAM_ISMULTI => true, - ApiBase::PARAM_TYPE => array( + ApiBase::PARAM_TYPE => [ 'blockinfo', 'groups', + 'implicitgroups', + 'rights', 'editcount', - 'registration' - ) - ), - 'limit' => array( + 'registration', + 'centralids', + ], + ApiBase::PARAM_HELP_MSG_PER_VALUE => [], + ], + 'limit' => [ ApiBase::PARAM_DFLT => 10, ApiBase::PARAM_TYPE => 'limit', ApiBase::PARAM_MIN => 1, ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1, ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2 - ), + ], 'witheditsonly' => false, - ); - } - - public function getParamDescription() { - return array( - 'from' => 'The user name to start enumerating from', - 'to' => 'The user name to stop enumerating at', - 'prefix' => 'Search for all users that begin with this value', - 'group' => 'Limit users to a given group name', - 'prop' => array( - 'What pieces of information to include.', - ' blockinfo - Adds the information about a current block on the user', - ' groups - Lists groups that the user is in', - ' editcount - Adds the edit count of the user', - ' registration - Adds the timestamp of when the user registered', - '`groups` property uses more server resources and may return fewer results than the limit' ), - 'limit' => 'How many total user names to return', - 'witheditsonly' => 'Only list users who have made edits', - ); - } - - public function getDescription() { - return 'Enumerate all registered users'; + 'activeusers' => [ + ApiBase::PARAM_DFLT => false, + ApiBase::PARAM_HELP_MSG => [ + 'apihelp-query+allusers-param-activeusers', + $this->getConfig()->get( 'ActiveUserDays' ) + ], + ], + 'attachedwiki' => null, + ]; } - protected function getExamples() { - return array( - 'api.php?action=query&list=allusers&aufrom=Y', - ); + protected function getExamplesMessages() { + return [ + 'action=query&list=allusers&aufrom=Y' + => 'apihelp-query+allusers-example-Y', + ]; } - public function getVersion() { - return __CLASS__ . ': $Id$'; + public function getHelpUrls() { + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Allusers'; } }