X-Git-Url: https://scripts.mit.edu/gitweb/autoinstallsdev/mediawiki.git/blobdiff_plain/19e297c21b10b1b8a3acad5e73fc71dcb35db44a..6932310fd58ebef145fa01eb76edf7150284d8ea:/includes/api/ApiQueryCategoryMembers.php diff --git a/includes/api/ApiQueryCategoryMembers.php b/includes/api/ApiQueryCategoryMembers.php index 14beee0a..c570ec99 100644 --- a/includes/api/ApiQueryCategoryMembers.php +++ b/includes/api/ApiQueryCategoryMembers.php @@ -1,10 +1,10 @@ @gmail.com + * Copyright © 2006 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,11 +24,6 @@ * @file */ -if ( !defined( 'MEDIAWIKI' ) ) { - // Eclipse helper - will be ignored in production - require_once( "ApiQueryBase.php" ); -} - /** * A query module to enumerate pages that belong to a category. * @@ -36,7 +31,7 @@ if ( !defined( 'MEDIAWIKI' ) ) { */ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'cm' ); } @@ -52,13 +47,25 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { $this->run( $resultPageSet ); } + /** + * @param string $hexSortkey + * @return bool + */ + private function validateHexSortkey( $hexSortkey ) { + // A hex sortkey has an unbound number of 2 letter pairs + return (bool)preg_match( '/^(?:[a-fA-F0-9]{2})*$/D', $hexSortkey ); + } + + /** + * @param ApiPageSet $resultPageSet + * @return void + */ private function run( $resultPageSet = null ) { $params = $this->extractRequestParams(); - $categoryTitle = Title::newFromText( $params['title'] ); - - if ( is_null( $categoryTitle ) || $categoryTitle->getNamespace() != NS_CATEGORY ) { - $this->dieUsage( 'The category name you entered is not valid', 'invalidcategory' ); + $categoryTitle = $this->getTitleOrPageId( $params )->getTitle(); + if ( $categoryTitle->getNamespace() != NS_CATEGORY ) { + $this->dieWithError( 'apierror-invalidcategory' ); } $prop = array_flip( $params['prop'] ); @@ -70,17 +77,17 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { $fld_type = isset( $prop['type'] ); if ( is_null( $resultPageSet ) ) { - $this->addFields( array( 'cl_from', 'cl_sortkey', 'cl_type', 'page_namespace', 'page_title' ) ); + $this->addFields( [ 'cl_from', 'cl_sortkey', 'cl_type', 'page_namespace', 'page_title' ] ); $this->addFieldsIf( 'page_id', $fld_ids ); $this->addFieldsIf( 'cl_sortkey_prefix', $fld_sortkeyprefix ); } else { $this->addFields( $resultPageSet->getPageTableFields() ); // will include page_ id, ns, title - $this->addFields( array( 'cl_from', 'cl_sortkey', 'cl_type' ) ); + $this->addFields( [ 'cl_from', 'cl_sortkey', 'cl_type' ] ); } $this->addFieldsIf( 'cl_timestamp', $fld_timestamp || $params['sort'] == 'timestamp' ); - $this->addTables( array( 'page', 'categorylinks' ) ); // must be in this order for 'USE INDEX' + $this->addTables( [ 'page', 'categorylinks' ] ); // must be in this order for 'USE INDEX' $this->addWhereFld( 'cl_to', $categoryTitle->getDBkey() ); $queryTypes = $params['type']; @@ -88,52 +95,88 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { // Scanning large datasets for rare categories sucks, and I already told // how to have efficient subcategory access :-) ~~~~ (oh well, domas) - global $wgMiserMode; - $miser_ns = array(); - if ( $wgMiserMode ) { + $miser_ns = []; + if ( $this->getConfig()->get( 'MiserMode' ) ) { $miser_ns = $params['namespace']; } else { $this->addWhereFld( 'page_namespace', $params['namespace'] ); } - $dir = $params['dir'] == 'asc' ? 'newer' : 'older'; + $dir = in_array( $params['dir'], [ 'asc', 'ascending', 'newer' ] ) ? 'newer' : 'older'; if ( $params['sort'] == 'timestamp' ) { - $this->addWhereRange( 'cl_timestamp', + $this->addTimestampWhereRange( 'cl_timestamp', $dir, $params['start'], $params['end'] ); + // Include in ORDER BY for uniqueness + $this->addWhereRange( 'cl_from', $dir, null, null ); + + if ( !is_null( $params['continue'] ) ) { + $cont = explode( '|', $params['continue'] ); + $this->dieContinueUsageIf( count( $cont ) != 2 ); + $op = ( $dir === 'newer' ? '>' : '<' ); + $db = $this->getDB(); + $continueTimestamp = $db->addQuotes( $db->timestamp( $cont[0] ) ); + $continueFrom = (int)$cont[1]; + $this->dieContinueUsageIf( $continueFrom != $cont[1] ); + $this->addWhere( "cl_timestamp $op $continueTimestamp OR " . + "(cl_timestamp = $continueTimestamp AND " . + "cl_from $op= $continueFrom)" + ); + } $this->addOption( 'USE INDEX', 'cl_timestamp' ); } else { if ( $params['continue'] ) { $cont = explode( '|', $params['continue'], 3 ); - if ( count( $cont ) != 3 ) { - $this->dieUsage( 'Invalid continue param. You should pass the original value returned '. - 'by the previous query', '_badcontinue' - ); - } - + $this->dieContinueUsageIf( count( $cont ) != 3 ); + // Remove the types to skip from $queryTypes $contTypeIndex = array_search( $cont[0], $queryTypes ); $queryTypes = array_slice( $queryTypes, $contTypeIndex ); - + // Add a WHERE clause for sortkey and from - // pack( "H*", $foo ) is used to convert hex back to binary - $escSortkey = $this->getDB()->addQuotes( pack( "H*", $cont[1] ) ); + $this->dieContinueUsageIf( !$this->validateHexSortkey( $cont[1] ) ); + $escSortkey = $this->getDB()->addQuotes( hex2bin( $cont[1] ) ); $from = intval( $cont[2] ); $op = $dir == 'newer' ? '>' : '<'; // $contWhere is used further down $contWhere = "cl_sortkey $op $escSortkey OR " . "(cl_sortkey = $escSortkey AND " . "cl_from $op= $from)"; - + // The below produces ORDER BY cl_sortkey, cl_from, possibly with DESC added to each of them + $this->addWhereRange( 'cl_sortkey', $dir, null, null ); + $this->addWhereRange( 'cl_from', $dir, null, null ); } else { + if ( $params['startsortkeyprefix'] !== null ) { + $startsortkey = Collation::singleton()->getSortKey( $params['startsortkeyprefix'] ); + } elseif ( $params['starthexsortkey'] !== null ) { + if ( !$this->validateHexSortkey( $params['starthexsortkey'] ) ) { + $encParamName = $this->encodeParamName( 'starthexsortkey' ); + $this->dieWithError( [ 'apierror-badparameter', $encParamName ], "badvalue_$encParamName" ); + } + $startsortkey = hex2bin( $params['starthexsortkey'] ); + } else { + $startsortkey = $params['startsortkey']; + } + if ( $params['endsortkeyprefix'] !== null ) { + $endsortkey = Collation::singleton()->getSortKey( $params['endsortkeyprefix'] ); + } elseif ( $params['endhexsortkey'] !== null ) { + if ( !$this->validateHexSortkey( $params['endhexsortkey'] ) ) { + $encParamName = $this->encodeParamName( 'endhexsortkey' ); + $this->dieWithError( [ 'apierror-badparameter', $encParamName ], "badvalue_$encParamName" ); + } + $endsortkey = hex2bin( $params['endhexsortkey'] ); + } else { + $endsortkey = $params['endsortkey']; + } + // The below produces ORDER BY cl_sortkey, cl_from, possibly with DESC added to each of them $this->addWhereRange( 'cl_sortkey', $dir, - $params['startsortkey'], - $params['endsortkey'] ); + $startsortkey, + $endsortkey ); $this->addWhereRange( 'cl_from', $dir, null, null ); } $this->addOption( 'USE INDEX', 'cl_sortkey' ); @@ -150,16 +193,16 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { // inconsistencies between ORDER BY cl_type and // WHERE cl_type >= 'foo' making proper paging impossible // and unindexed. - $rows = array(); + $rows = []; $first = true; foreach ( $queryTypes as $type ) { - $extraConds = array( 'cl_type' => $type ); + $extraConds = [ 'cl_type' => $type ]; if ( $first && $contWhere ) { // Continuation condition. Only added to the // first query, otherwise we'll skip things $extraConds[] = $contWhere; } - $res = $this->select( __METHOD__, array( 'where' => $extraConds ) ); + $res = $this->select( __METHOD__, [ 'where' => $extraConds ] ); $rows = array_merge( $rows, iterator_to_array( $res ) ); if ( count( $rows ) >= $limit + 1 ) { break; @@ -173,13 +216,17 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { $res = $this->select( __METHOD__ ); $rows = iterator_to_array( $res ); } + + $result = $this->getResult(); $count = 0; foreach ( $rows as $row ) { - if ( ++ $count > $limit ) { - // We've reached the one extra which shows that there are additional pages to be had. Stop here... - // TODO: Security issue - if the user has no right to view next title, it will still be shown + if ( ++$count > $limit ) { + // We've reached the one extra which shows that there are + // additional pages to be had. Stop here... + // @todo Security issue - if the user has no right to view next + // title, it will still be shown if ( $params['sort'] == 'timestamp' ) { - $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->cl_timestamp ) ); + $this->setContinueEnumParameter( 'continue', "$row->cl_timestamp|$row->cl_from" ); } else { $sortkey = bin2hex( $row->cl_sortkey ); $this->setContinueEnumParameter( 'continue', @@ -198,7 +245,9 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { } if ( is_null( $resultPageSet ) ) { - $vals = array(); + $vals = [ + ApiResult::META_TYPE => 'assoc', + ]; if ( $fld_ids ) { $vals['pageid'] = intval( $row->page_id ); } @@ -212,17 +261,17 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { if ( $fld_sortkeyprefix ) { $vals['sortkeyprefix'] = $row->cl_sortkey_prefix; } - if ( $fld_type ) { + if ( $fld_type ) { $vals['type'] = $row->cl_type; } if ( $fld_timestamp ) { $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $row->cl_timestamp ); } - $fit = $this->getResult()->addValue( array( 'query', $this->getModuleName() ), - null, $vals ); + $fit = $result->addValue( [ 'query', $this->getModuleName() ], + null, $vals ); if ( !$fit ) { if ( $params['sort'] == 'timestamp' ) { - $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->cl_timestamp ) ); + $this->setContinueEnumParameter( 'continue', "$row->cl_timestamp|$row->cl_from" ); } else { $sortkey = bin2hex( $row->cl_sortkey ); $this->setContinueEnumParameter( 'continue', @@ -237,133 +286,111 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { } if ( is_null( $resultPageSet ) ) { - $this->getResult()->setIndexedTagName_internal( - array( 'query', $this->getModuleName() ), 'cm' ); + $result->addIndexedTagName( + [ 'query', $this->getModuleName() ], 'cm' ); } } public function getAllowedParams() { - return array( - 'title' => array( + $ret = [ + 'title' => [ ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true - ), - 'prop' => array( + ], + 'pageid' => [ + ApiBase::PARAM_TYPE => 'integer' + ], + 'prop' => [ ApiBase::PARAM_DFLT => 'ids|title', ApiBase::PARAM_ISMULTI => true, - ApiBase::PARAM_TYPE => array ( + ApiBase::PARAM_TYPE => [ 'ids', 'title', 'sortkey', 'sortkeyprefix', 'type', 'timestamp', - ) - ), - 'namespace' => array ( + ], + ApiBase::PARAM_HELP_MSG_PER_VALUE => [], + ], + 'namespace' => [ ApiBase::PARAM_ISMULTI => true, ApiBase::PARAM_TYPE => 'namespace', - ), - 'type' => array( + ], + 'type' => [ ApiBase::PARAM_ISMULTI => true, ApiBase::PARAM_DFLT => 'page|subcat|file', - ApiBase::PARAM_TYPE => array( + ApiBase::PARAM_TYPE => [ 'page', 'subcat', 'file' - ) - ), - 'continue' => null, - 'limit' => array( + ] + ], + 'continue' => [ + ApiBase::PARAM_HELP_MSG => 'api-help-param-continue', + ], + 'limit' => [ ApiBase::PARAM_TYPE => 'limit', ApiBase::PARAM_DFLT => 10, ApiBase::PARAM_MIN => 1, ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1, ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2 - ), - 'sort' => array( + ], + 'sort' => [ ApiBase::PARAM_DFLT => 'sortkey', - ApiBase::PARAM_TYPE => array( + ApiBase::PARAM_TYPE => [ 'sortkey', 'timestamp' - ) - ), - 'dir' => array( - ApiBase::PARAM_DFLT => 'asc', - ApiBase::PARAM_TYPE => array( + ] + ], + 'dir' => [ + ApiBase::PARAM_DFLT => 'ascending', + ApiBase::PARAM_TYPE => [ 'asc', - 'desc' - ) - ), - 'start' => array( + 'desc', + // Normalising with other modules + 'ascending', + 'descending', + 'newer', + 'older', + ] + ], + 'start' => [ ApiBase::PARAM_TYPE => 'timestamp' - ), - 'end' => array( + ], + 'end' => [ ApiBase::PARAM_TYPE => 'timestamp' - ), - 'startsortkey' => null, - 'endsortkey' => null, - ); - } + ], + 'starthexsortkey' => null, + 'endhexsortkey' => null, + 'startsortkeyprefix' => null, + 'endsortkeyprefix' => null, + 'startsortkey' => [ + ApiBase::PARAM_DEPRECATED => true, + ], + 'endsortkey' => [ + ApiBase::PARAM_DEPRECATED => true, + ], + ]; - public function getParamDescription() { - global $wgMiserMode; - $p = $this->getModulePrefix(); - $desc = array( - 'title' => 'Which category to enumerate (required). Must include Category: prefix', - 'prop' => array( - 'What pieces of information to include', - ' ids - Adds the page ID', - ' title - Adds the title and namespace ID of the page', - ' sortkey - Adds the sortkey used for sorting in the category (hexadecimal string)', - ' sortkeyprefix - Adds the sortkey prefix used for sorting in the category (human-readable part of the sortkey)', - ' type - Adds the type that the page has been categorised as (page, subcat or file)', - ' timestamp - Adds the timestamp of when the page was included', - ), - 'namespace' => 'Only include pages in these namespaces', - 'type' => "What type of category members to include. Ignored when {$p}sort=timestamp is set", - 'sort' => 'Property to sort by', - 'dir' => 'In which direction to sort', - 'start' => "Timestamp to start listing from. Can only be used with {$p}sort=timestamp", - 'end' => "Timestamp to end listing at. Can only be used with {$p}sort=timestamp", - 'startsortkey' => "Sortkey to start listing from. Can only be used with {$p}sort=sortkey", - 'endsortkey' => "Sortkey to end listing at. Can only be used with {$p}sort=sortkey", - 'continue' => 'For large categories, give the value retured from previous query', - 'limit' => 'The maximum number of pages to return.', - ); - if ( $wgMiserMode ) { - $desc['namespace'] = array( - $desc['namespace'], - 'NOTE: Due to $wgMiserMode, using this may result in fewer than "limit" results', - 'returned before continuing; in extreme cases, zero results may be returned.', - 'Note that you can use cmtype=subcat or cmtype=file instead of cmnamespace=14 or 6.', - ); + if ( $this->getConfig()->get( 'MiserMode' ) ) { + $ret['namespace'][ApiBase::PARAM_HELP_MSG_APPEND] = [ + 'api-help-param-limited-in-miser-mode', + ]; } - return $desc; - } - - public function getDescription() { - return 'List all pages in a given category'; - } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'notitle', 'info' => 'The cmtitle parameter is required' ), - array( 'code' => 'invalidcategory', 'info' => 'The category name you entered is not valid' ), - array( 'code' => 'badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ), - ) ); + return $ret; } - protected function getExamples() { - return array( - 'Get first 10 pages in [[Category:Physics]]:', - ' api.php?action=query&list=categorymembers&cmtitle=Category:Physics', - 'Get page info about first 10 pages in [[Category:Physics]]:', - ' api.php?action=query&generator=categorymembers&gcmtitle=Category:Physics&prop=info', - ); + protected function getExamplesMessages() { + return [ + 'action=query&list=categorymembers&cmtitle=Category:Physics' + => 'apihelp-query+categorymembers-example-simple', + 'action=query&generator=categorymembers&gcmtitle=Category:Physics&prop=info' + => 'apihelp-query+categorymembers-example-generator', + ]; } - public function getVersion() { - return __CLASS__ . ': $Id$'; + public function getHelpUrls() { + return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Categorymembers'; } }