diff options
Diffstat (limited to 'includes/api/ApiQueryUserContributions.php')
-rw-r--r-- | includes/api/ApiQueryUserContributions.php | 262 |
1 files changed, 141 insertions, 121 deletions
diff --git a/includes/api/ApiQueryUserContributions.php b/includes/api/ApiQueryUserContributions.php index 9a9be7b2..4b167b8b 100644 --- a/includes/api/ApiQueryUserContributions.php +++ b/includes/api/ApiQueryUserContributions.php @@ -31,14 +31,14 @@ */ class ApiQueryContributions extends ApiQueryBase { - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'uc' ); } private $params, $prefixMode, $userprefix, $multiUserMode, $usernames, $parentLens; private $fld_ids = false, $fld_title = false, $fld_timestamp = false, - $fld_comment = false, $fld_parsedcomment = false, $fld_flags = false, - $fld_patrolled = false, $fld_tags = false, $fld_size = false, $fld_sizediff = false; + $fld_comment = false, $fld_parsedcomment = false, $fld_flags = false, + $fld_patrolled = false, $fld_tags = false, $fld_size = false, $fld_sizediff = false; public function execute() { // Parse some parameters @@ -56,6 +56,11 @@ class ApiQueryContributions extends ApiQueryBase { $this->fld_patrolled = isset( $prop['patrolled'] ); $this->fld_tags = isset( $prop['tags'] ); + // Most of this code will use the 'contributions' group DB, which can map to slaves + // with extra user based indexes or partioning by user. The additional metadata + // queries should use a regular slave since the lookup pattern is not all by user. + $dbSecondary = $this->getDB(); // any random slave + // TODO: if the query is going only against the revision table, should this be done? $this->selectNamedDB( 'contributions', DB_SLAVE, 'contributions' ); @@ -90,7 +95,7 @@ class ApiQueryContributions extends ApiQueryBase { $revIds[] = $row->rev_parent_id; } } - $this->parentLens = Revision::getParentLengths( $this->getDB(), $revIds ); + $this->parentLens = Revision::getParentLengths( $dbSecondary, $revIds ); $res->rewind(); // reset } @@ -100,36 +105,32 @@ class ApiQueryContributions extends ApiQueryBase { // Fetch each row foreach ( $res as $row ) { - if ( ++ $count > $limit ) { - // We've reached the one extra which shows that there are additional pages to be had. Stop here... - if ( $this->multiUserMode ) { - $this->setContinueEnumParameter( 'continue', $this->continueStr( $row ) ); - } else { - $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->rev_timestamp ) ); - } + if ( ++$count > $limit ) { + // We've reached the one extra which shows that there are + // additional pages to be had. Stop here... + $this->setContinueEnumParameter( 'continue', $this->continueStr( $row ) ); break; } $vals = $this->extractRowInfo( $row ); $fit = $this->getResult()->addValue( array( 'query', $this->getModuleName() ), null, $vals ); if ( !$fit ) { - if ( $this->multiUserMode ) { - $this->setContinueEnumParameter( 'continue', $this->continueStr( $row ) ); - } else { - $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->rev_timestamp ) ); - } + $this->setContinueEnumParameter( 'continue', $this->continueStr( $row ) ); break; } } - $this->getResult()->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'item' ); + $this->getResult()->setIndexedTagName_internal( + array( 'query', $this->getModuleName() ), + 'item' + ); } /** * Validate the 'user' parameter and set the value to compare * against `revision`.`rev_user_text` * - * @param $user string + * @param string $user */ private function prepareUsername( $user ) { if ( !is_null( $user ) && $user !== '' ) { @@ -158,26 +159,53 @@ class ApiQueryContributions extends ApiQueryBase { $this->addWhere( 'page_id=rev_page' ); // Handle continue parameter - if ( $this->multiUserMode && !is_null( $this->params['continue'] ) ) { + if ( !is_null( $this->params['continue'] ) ) { $continue = explode( '|', $this->params['continue'] ); - $this->dieContinueUsageIf( count( $continue ) != 2 ); $db = $this->getDB(); - $encUser = $db->addQuotes( $continue[0] ); - $encTS = $db->addQuotes( $db->timestamp( $continue[1] ) ); + if ( $this->multiUserMode ) { + $this->dieContinueUsageIf( count( $continue ) != 3 ); + $encUser = $db->addQuotes( array_shift( $continue ) ); + } else { + $this->dieContinueUsageIf( count( $continue ) != 2 ); + } + $encTS = $db->addQuotes( $db->timestamp( $continue[0] ) ); + $encId = (int)$continue[1]; + $this->dieContinueUsageIf( $encId != $continue[1] ); $op = ( $this->params['dir'] == 'older' ? '<' : '>' ); - $this->addWhere( - "rev_user_text $op $encUser OR " . - "(rev_user_text = $encUser AND " . - "rev_timestamp $op= $encTS)" - ); + if ( $this->multiUserMode ) { + $this->addWhere( + "rev_user_text $op $encUser OR " . + "(rev_user_text = $encUser AND " . + "(rev_timestamp $op $encTS OR " . + "(rev_timestamp = $encTS AND " . + "rev_id $op= $encId)))" + ); + } else { + $this->addWhere( + "rev_timestamp $op $encTS OR " . + "(rev_timestamp = $encTS AND " . + "rev_id $op= $encId)" + ); + } } - if ( !$user->isAllowed( 'hideuser' ) ) { - $this->addWhere( $this->getDB()->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' ); + // Don't include any revisions where we're not supposed to be able to + // see the username. + if ( !$user->isAllowed( 'deletedhistory' ) ) { + $bitmask = Revision::DELETED_USER; + } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { + $bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED; + } else { + $bitmask = 0; } + if ( $bitmask ) { + $this->addWhere( $this->getDB()->bitAnd( 'rev_deleted', $bitmask ) . " != $bitmask" ); + } + // We only want pages by the specified users. if ( $this->prefixMode ) { - $this->addWhere( 'rev_user_text' . $this->getDB()->buildLike( $this->userprefix, $this->getDB()->anyString() ) ); + $this->addWhere( 'rev_user_text' . + $this->getDB()->buildLike( $this->userprefix, $this->getDB()->anyString() ) ); } else { $this->addWhereFld( 'rev_user_text', $this->usernames ); } @@ -189,13 +217,24 @@ class ApiQueryContributions extends ApiQueryBase { } $this->addTimestampWhereRange( 'rev_timestamp', $this->params['dir'], $this->params['start'], $this->params['end'] ); + // Include in ORDER BY for uniqueness + $this->addWhereRange( 'rev_id', $this->params['dir'], null, null ); + $this->addWhereFld( 'page_namespace', $this->params['namespace'] ); $show = $this->params['show']; + if ( $this->params['toponly'] ) { // deprecated/old param + $this->logFeatureUsage( 'list=usercontribs&uctoponly' ); + $show[] = 'top'; + } if ( !is_null( $show ) ) { $show = array_flip( $show ); + if ( ( isset( $show['minor'] ) && isset( $show['!minor'] ) ) - || ( isset( $show['patrolled'] ) && isset( $show['!patrolled'] ) ) ) { + || ( isset( $show['patrolled'] ) && isset( $show['!patrolled'] ) ) + || ( isset( $show['top'] ) && isset( $show['!top'] ) ) + || ( isset( $show['new'] ) && isset( $show['!new'] ) ) + ) { $this->dieUsageMsg( 'show' ); } @@ -203,6 +242,10 @@ class ApiQueryContributions extends ApiQueryBase { $this->addWhereIf( 'rev_minor_edit != 0', isset( $show['minor'] ) ); $this->addWhereIf( 'rc_patrolled = 0', isset( $show['!patrolled'] ) ); $this->addWhereIf( 'rc_patrolled != 0', isset( $show['patrolled'] ) ); + $this->addWhereIf( 'rev_id != page_latest', isset( $show['!top'] ) ); + $this->addWhereIf( 'rev_id = page_latest', isset( $show['top'] ) ); + $this->addWhereIf( 'rev_parent_id != 0', isset( $show['!new'] ) ); + $this->addWhereIf( 'rev_parent_id = 0', isset( $show['new'] ) ); } $this->addOption( 'LIMIT', $this->params['limit'] + 1 ); $index = array( 'revision' => 'usertext_timestamp' ); @@ -211,6 +254,7 @@ class ApiQueryContributions extends ApiQueryBase { // ns+title checks if the user has access rights for this page // user_text is necessary if multiple users were specified $this->addFields( array( + 'rev_id', 'rev_timestamp', 'page_namespace', 'page_title', @@ -220,9 +264,13 @@ class ApiQueryContributions extends ApiQueryBase { ) ); if ( isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) || - $this->fld_patrolled ) { + $this->fld_patrolled + ) { if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) { - $this->dieUsage( 'You need the patrol right to request the patrolled flag', 'permissiondenied' ); + $this->dieUsage( + 'You need the patrol right to request the patrolled flag', + 'permissiondenied' + ); } // Use a redundant join condition on both @@ -249,7 +297,6 @@ class ApiQueryContributions extends ApiQueryBase { $this->addTables( $tables ); $this->addFieldsIf( 'rev_page', $this->fld_ids ); - $this->addFieldsIf( 'rev_id', $this->fld_ids || $this->fld_flags ); $this->addFieldsIf( 'page_latest', $this->fld_flags ); // $this->addFieldsIf( 'rev_text_id', $this->fld_ids ); // Should this field be exposed? $this->addFieldsIf( 'rev_comment', $this->fld_comment || $this->fld_parsedcomment ); @@ -260,19 +307,18 @@ class ApiQueryContributions extends ApiQueryBase { if ( $this->fld_tags ) { $this->addTables( 'tag_summary' ); - $this->addJoinConds( array( 'tag_summary' => array( 'LEFT JOIN', array( 'rev_id=ts_rev_id' ) ) ) ); + $this->addJoinConds( + array( 'tag_summary' => array( 'LEFT JOIN', array( 'rev_id=ts_rev_id' ) ) ) + ); $this->addFields( 'ts_tags' ); } if ( isset( $this->params['tag'] ) ) { $this->addTables( 'change_tag' ); - $this->addJoinConds( array( 'change_tag' => array( 'INNER JOIN', array( 'rev_id=ct_rev_id' ) ) ) ); + $this->addJoinConds( + array( 'change_tag' => array( 'INNER JOIN', array( 'rev_id=ct_rev_id' ) ) ) + ); $this->addWhereFld( 'ct_tag', $this->params['tag'] ); - $index['change_tag'] = 'change_tag_tag_id'; - } - - if ( $this->params['toponly'] ) { - $this->addWhere( 'rev_id = page_latest' ); } $this->addOption( 'USE INDEX', $index ); @@ -281,16 +327,24 @@ class ApiQueryContributions extends ApiQueryBase { /** * Extract fields from the database row and append them to a result array * - * @param $row + * @param stdClass $row * @return array */ private function extractRowInfo( $row ) { $vals = array(); + $anyHidden = false; + if ( $row->rev_deleted & Revision::DELETED_TEXT ) { + $vals['texthidden'] = ''; + $anyHidden = true; + } + + // Any rows where we can't view the user were filtered out in the query. $vals['userid'] = $row->rev_user; $vals['user'] = $row->rev_user_text; if ( $row->rev_deleted & Revision::DELETED_USER ) { $vals['userhidden'] = ''; + $anyHidden = true; } if ( $this->fld_ids ) { $vals['pageid'] = intval( $row->rev_page ); @@ -327,7 +381,15 @@ class ApiQueryContributions extends ApiQueryBase { if ( ( $this->fld_comment || $this->fld_parsedcomment ) && isset( $row->rev_comment ) ) { if ( $row->rev_deleted & Revision::DELETED_COMMENT ) { $vals['commenthidden'] = ''; - } else { + $anyHidden = true; + } + + $userCanView = Revision::userCanBitfield( + $row->rev_deleted, + Revision::DELETED_COMMENT, $this->getUser() + ); + + if ( $userCanView ) { if ( $this->fld_comment ) { $vals['comment'] = $row->rev_comment; } @@ -346,8 +408,13 @@ class ApiQueryContributions extends ApiQueryBase { $vals['size'] = intval( $row->rev_len ); } - if ( $this->fld_sizediff && !is_null( $row->rev_len ) && !is_null( $row->rev_parent_id ) ) { - $parentLen = isset( $this->parentLens[$row->rev_parent_id] ) ? $this->parentLens[$row->rev_parent_id] : 0; + if ( $this->fld_sizediff + && !is_null( $row->rev_len ) + && !is_null( $row->rev_parent_id ) + ) { + $parentLen = isset( $this->parentLens[$row->rev_parent_id] ) + ? $this->parentLens[$row->rev_parent_id] + : 0; $vals['sizediff'] = intval( $row->rev_len - $parentLen ); } @@ -361,12 +428,19 @@ class ApiQueryContributions extends ApiQueryBase { } } + if ( $anyHidden && $row->rev_deleted & Revision::DELETED_RESTRICTED ) { + $vals['suppressed'] = ''; + } + return $vals; } private function continueStr( $row ) { - return $row->rev_user_text . '|' . - wfTimestamp( TS_ISO_8601, $row->rev_timestamp ); + if ( $this->multiUserMode ) { + return "$row->rev_user_text|$row->rev_timestamp|$row->rev_id"; + } else { + return "$row->rev_timestamp|$row->rev_id"; + } } public function getCacheMode( $params ) { @@ -429,23 +503,34 @@ class ApiQueryContributions extends ApiQueryBase { '!minor', 'patrolled', '!patrolled', + 'top', + '!top', + 'new', + '!new', ) ), 'tag' => null, - 'toponly' => false, + 'toponly' => array( + ApiBase::PARAM_DFLT => false, + ApiBase::PARAM_DEPRECATED => true, + ), ); } public function getParamDescription() { - global $wgRCMaxAge; $p = $this->getModulePrefix(); + $RCMaxAge = $this->getConfig()->get( 'RCMaxAge' ); + return array( 'limit' => 'The maximum number of contributions to return', 'start' => 'The start timestamp to return from', 'end' => 'The end timestamp to return to', 'continue' => 'When more results are available, use this to continue', 'user' => 'The users to retrieve contributions for', - 'userprefix' => "Retrieve contributions for all users whose names begin with this value. Overrides {$p}user", + 'userprefix' => array( + "Retrieve contributions for all users whose names begin with this value.", + "Overrides {$p}user", + ), 'dir' => $this->getDirectionDescription( $p ), 'namespace' => 'Only list contributions in these namespaces', 'prop' => array( @@ -461,83 +546,18 @@ class ApiQueryContributions extends ApiQueryBase { ' patrolled - Tags patrolled edits', ' tags - Lists tags for the edit', ), - 'show' => array( "Show only items that meet this criteria, e.g. non minor edits only: {$p}show=!minor", - "NOTE: if {$p}show=patrolled or {$p}show=!patrolled is set, revisions older than \$wgRCMaxAge ($wgRCMaxAge) won't be shown", ), + 'show' => array( + "Show only items that meet thse criteria, e.g. non minor edits only: {$p}show=!minor", + "NOTE: If {$p}show=patrolled or {$p}show=!patrolled is set, revisions older than", + "\$wgRCMaxAge ($RCMaxAge) won't be shown", + ), 'tag' => 'Only list revisions tagged with this tag', 'toponly' => 'Only list changes which are the latest revision', ); } - public function getResultProperties() { - return array( - '' => array( - 'userid' => 'integer', - 'user' => 'string', - 'userhidden' => 'boolean' - ), - 'ids' => array( - 'pageid' => 'integer', - 'revid' => 'integer', - 'parentid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ) - ), - 'title' => array( - 'ns' => 'namespace', - 'title' => 'string' - ), - 'timestamp' => array( - 'timestamp' => 'timestamp' - ), - 'flags' => array( - 'new' => 'boolean', - 'minor' => 'boolean', - 'top' => 'boolean' - ), - 'comment' => array( - 'commenthidden' => 'boolean', - 'comment' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'parsedcomment' => array( - 'commenthidden' => 'boolean', - 'parsedcomment' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'patrolled' => array( - 'patrolled' => 'boolean' - ), - 'size' => array( - 'size' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ) - ), - 'sizediff' => array( - 'sizediff' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ) - ) - ); - } - public function getDescription() { - return 'Get all edits by a user'; - } - - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'param_user', 'info' => 'User parameter may not be empty.' ), - array( 'code' => 'param_user', 'info' => 'User name user is not valid' ), - array( 'show' ), - array( 'code' => 'permissiondenied', 'info' => 'You need the patrol right to request the patrolled flag' ), - ) ); + return 'Get all edits by a user.'; } public function getExamples() { |