MediaWiki
master
|
00001 <?php 00032 class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { 00033 00034 public function __construct( $query, $moduleName ) { 00035 parent::__construct( $query, $moduleName, 'cm' ); 00036 } 00037 00038 public function execute() { 00039 $this->run(); 00040 } 00041 00042 public function getCacheMode( $params ) { 00043 return 'public'; 00044 } 00045 00046 public function executeGenerator( $resultPageSet ) { 00047 $this->run( $resultPageSet ); 00048 } 00049 00054 private function run( $resultPageSet = null ) { 00055 $params = $this->extractRequestParams(); 00056 00057 $categoryTitle = $this->getTitleOrPageId( $params )->getTitle(); 00058 if ( $categoryTitle->getNamespace() != NS_CATEGORY ) { 00059 $this->dieUsage( 'The category name you entered is not valid', 'invalidcategory' ); 00060 } 00061 00062 $prop = array_flip( $params['prop'] ); 00063 $fld_ids = isset( $prop['ids'] ); 00064 $fld_title = isset( $prop['title'] ); 00065 $fld_sortkey = isset( $prop['sortkey'] ); 00066 $fld_sortkeyprefix = isset( $prop['sortkeyprefix'] ); 00067 $fld_timestamp = isset( $prop['timestamp'] ); 00068 $fld_type = isset( $prop['type'] ); 00069 00070 if ( is_null( $resultPageSet ) ) { 00071 $this->addFields( array( 'cl_from', 'cl_sortkey', 'cl_type', 'page_namespace', 'page_title' ) ); 00072 $this->addFieldsIf( 'page_id', $fld_ids ); 00073 $this->addFieldsIf( 'cl_sortkey_prefix', $fld_sortkeyprefix ); 00074 } else { 00075 $this->addFields( $resultPageSet->getPageTableFields() ); // will include page_ id, ns, title 00076 $this->addFields( array( 'cl_from', 'cl_sortkey', 'cl_type' ) ); 00077 } 00078 00079 $this->addFieldsIf( 'cl_timestamp', $fld_timestamp || $params['sort'] == 'timestamp' ); 00080 00081 $this->addTables( array( 'page', 'categorylinks' ) ); // must be in this order for 'USE INDEX' 00082 00083 $this->addWhereFld( 'cl_to', $categoryTitle->getDBkey() ); 00084 $queryTypes = $params['type']; 00085 $contWhere = false; 00086 00087 // Scanning large datasets for rare categories sucks, and I already told 00088 // how to have efficient subcategory access :-) ~~~~ (oh well, domas) 00089 global $wgMiserMode; 00090 $miser_ns = array(); 00091 if ( $wgMiserMode ) { 00092 $miser_ns = $params['namespace']; 00093 } else { 00094 $this->addWhereFld( 'page_namespace', $params['namespace'] ); 00095 } 00096 00097 $dir = in_array( $params['dir'], array( 'asc', 'ascending', 'newer' ) ) ? 'newer' : 'older'; 00098 00099 if ( $params['sort'] == 'timestamp' ) { 00100 $this->addTimestampWhereRange( 'cl_timestamp', 00101 $dir, 00102 $params['start'], 00103 $params['end'] ); 00104 00105 $this->addOption( 'USE INDEX', 'cl_timestamp' ); 00106 } else { 00107 if ( $params['continue'] ) { 00108 $cont = explode( '|', $params['continue'], 3 ); 00109 if ( count( $cont ) != 3 ) { 00110 $this->dieUsage( 'Invalid continue param. You should pass the original value returned '. 00111 'by the previous query', '_badcontinue' 00112 ); 00113 } 00114 00115 // Remove the types to skip from $queryTypes 00116 $contTypeIndex = array_search( $cont[0], $queryTypes ); 00117 $queryTypes = array_slice( $queryTypes, $contTypeIndex ); 00118 00119 // Add a WHERE clause for sortkey and from 00120 // pack( "H*", $foo ) is used to convert hex back to binary 00121 $escSortkey = $this->getDB()->addQuotes( pack( "H*", $cont[1] ) ); 00122 $from = intval( $cont[2] ); 00123 $op = $dir == 'newer' ? '>' : '<'; 00124 // $contWhere is used further down 00125 $contWhere = "cl_sortkey $op $escSortkey OR " . 00126 "(cl_sortkey = $escSortkey AND " . 00127 "cl_from $op= $from)"; 00128 // The below produces ORDER BY cl_sortkey, cl_from, possibly with DESC added to each of them 00129 $this->addWhereRange( 'cl_sortkey', $dir, null, null ); 00130 $this->addWhereRange( 'cl_from', $dir, null, null ); 00131 } else { 00132 $startsortkey = $params['startsortkeyprefix'] !== null ? 00133 Collation::singleton()->getSortkey( $params['startsortkeyprefix'] ) : 00134 $params['startsortkey']; 00135 $endsortkey = $params['endsortkeyprefix'] !== null ? 00136 Collation::singleton()->getSortkey( $params['endsortkeyprefix'] ) : 00137 $params['endsortkey']; 00138 00139 // The below produces ORDER BY cl_sortkey, cl_from, possibly with DESC added to each of them 00140 $this->addWhereRange( 'cl_sortkey', 00141 $dir, 00142 $startsortkey, 00143 $endsortkey ); 00144 $this->addWhereRange( 'cl_from', $dir, null, null ); 00145 } 00146 $this->addOption( 'USE INDEX', 'cl_sortkey' ); 00147 } 00148 00149 $this->addWhere( 'cl_from=page_id' ); 00150 00151 $limit = $params['limit']; 00152 $this->addOption( 'LIMIT', $limit + 1 ); 00153 00154 if ( $params['sort'] == 'sortkey' ) { 00155 // Run a separate SELECT query for each value of cl_type. 00156 // This is needed because cl_type is an enum, and MySQL has 00157 // inconsistencies between ORDER BY cl_type and 00158 // WHERE cl_type >= 'foo' making proper paging impossible 00159 // and unindexed. 00160 $rows = array(); 00161 $first = true; 00162 foreach ( $queryTypes as $type ) { 00163 $extraConds = array( 'cl_type' => $type ); 00164 if ( $first && $contWhere ) { 00165 // Continuation condition. Only added to the 00166 // first query, otherwise we'll skip things 00167 $extraConds[] = $contWhere; 00168 } 00169 $res = $this->select( __METHOD__, array( 'where' => $extraConds ) ); 00170 $rows = array_merge( $rows, iterator_to_array( $res ) ); 00171 if ( count( $rows ) >= $limit + 1 ) { 00172 break; 00173 } 00174 $first = false; 00175 } 00176 } else { 00177 // Sorting by timestamp 00178 // No need to worry about per-type queries because we 00179 // aren't sorting or filtering by type anyway 00180 $res = $this->select( __METHOD__ ); 00181 $rows = iterator_to_array( $res ); 00182 } 00183 00184 $result = $this->getResult(); 00185 $count = 0; 00186 foreach ( $rows as $row ) { 00187 if ( ++ $count > $limit ) { 00188 // We've reached the one extra which shows that there are additional pages to be had. Stop here... 00189 // TODO: Security issue - if the user has no right to view next title, it will still be shown 00190 if ( $params['sort'] == 'timestamp' ) { 00191 $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->cl_timestamp ) ); 00192 } else { 00193 $sortkey = bin2hex( $row->cl_sortkey ); 00194 $this->setContinueEnumParameter( 'continue', 00195 "{$row->cl_type}|$sortkey|{$row->cl_from}" 00196 ); 00197 } 00198 break; 00199 } 00200 00201 // Since domas won't tell anyone what he told long ago, apply 00202 // cmnamespace here. This means the query may return 0 actual 00203 // results, but on the other hand it could save returning 5000 00204 // useless results to the client. ~~~~ 00205 if ( count( $miser_ns ) && !in_array( $row->page_namespace, $miser_ns ) ) { 00206 continue; 00207 } 00208 00209 if ( is_null( $resultPageSet ) ) { 00210 $vals = array(); 00211 if ( $fld_ids ) { 00212 $vals['pageid'] = intval( $row->page_id ); 00213 } 00214 if ( $fld_title ) { 00215 $title = Title::makeTitle( $row->page_namespace, $row->page_title ); 00216 ApiQueryBase::addTitleInfo( $vals, $title ); 00217 } 00218 if ( $fld_sortkey ) { 00219 $vals['sortkey'] = bin2hex( $row->cl_sortkey ); 00220 } 00221 if ( $fld_sortkeyprefix ) { 00222 $vals['sortkeyprefix'] = $row->cl_sortkey_prefix; 00223 } 00224 if ( $fld_type ) { 00225 $vals['type'] = $row->cl_type; 00226 } 00227 if ( $fld_timestamp ) { 00228 $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $row->cl_timestamp ); 00229 } 00230 $fit = $result->addValue( array( 'query', $this->getModuleName() ), 00231 null, $vals ); 00232 if ( !$fit ) { 00233 if ( $params['sort'] == 'timestamp' ) { 00234 $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->cl_timestamp ) ); 00235 } else { 00236 $sortkey = bin2hex( $row->cl_sortkey ); 00237 $this->setContinueEnumParameter( 'continue', 00238 "{$row->cl_type}|$sortkey|{$row->cl_from}" 00239 ); 00240 } 00241 break; 00242 } 00243 } else { 00244 $resultPageSet->processDbRow( $row ); 00245 } 00246 } 00247 00248 if ( is_null( $resultPageSet ) ) { 00249 $result->setIndexedTagName_internal( 00250 array( 'query', $this->getModuleName() ), 'cm' ); 00251 } 00252 } 00253 00254 public function getAllowedParams() { 00255 return array( 00256 'title' => array( 00257 ApiBase::PARAM_TYPE => 'string', 00258 ), 00259 'pageid' => array( 00260 ApiBase::PARAM_TYPE => 'integer' 00261 ), 00262 'prop' => array( 00263 ApiBase::PARAM_DFLT => 'ids|title', 00264 ApiBase::PARAM_ISMULTI => true, 00265 ApiBase::PARAM_TYPE => array ( 00266 'ids', 00267 'title', 00268 'sortkey', 00269 'sortkeyprefix', 00270 'type', 00271 'timestamp', 00272 ) 00273 ), 00274 'namespace' => array ( 00275 ApiBase::PARAM_ISMULTI => true, 00276 ApiBase::PARAM_TYPE => 'namespace', 00277 ), 00278 'type' => array( 00279 ApiBase::PARAM_ISMULTI => true, 00280 ApiBase::PARAM_DFLT => 'page|subcat|file', 00281 ApiBase::PARAM_TYPE => array( 00282 'page', 00283 'subcat', 00284 'file' 00285 ) 00286 ), 00287 'continue' => null, 00288 'limit' => array( 00289 ApiBase::PARAM_TYPE => 'limit', 00290 ApiBase::PARAM_DFLT => 10, 00291 ApiBase::PARAM_MIN => 1, 00292 ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1, 00293 ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2 00294 ), 00295 'sort' => array( 00296 ApiBase::PARAM_DFLT => 'sortkey', 00297 ApiBase::PARAM_TYPE => array( 00298 'sortkey', 00299 'timestamp' 00300 ) 00301 ), 00302 'dir' => array( 00303 ApiBase::PARAM_DFLT => 'ascending', 00304 ApiBase::PARAM_TYPE => array( 00305 'asc', 00306 'desc', 00307 // Normalising with other modules 00308 'ascending', 00309 'descending', 00310 'newer', 00311 'older', 00312 ) 00313 ), 00314 'start' => array( 00315 ApiBase::PARAM_TYPE => 'timestamp' 00316 ), 00317 'end' => array( 00318 ApiBase::PARAM_TYPE => 'timestamp' 00319 ), 00320 'startsortkey' => null, 00321 'endsortkey' => null, 00322 'startsortkeyprefix' => null, 00323 'endsortkeyprefix' => null, 00324 ); 00325 } 00326 00327 public function getParamDescription() { 00328 global $wgMiserMode; 00329 $p = $this->getModulePrefix(); 00330 $desc = array( 00331 'title' => "Which category to enumerate (required). Must include Category: prefix. Cannot be used together with {$p}pageid", 00332 'pageid' => "Page ID of the category to enumerate. Cannot be used together with {$p}title", 00333 'prop' => array( 00334 'What pieces of information to include', 00335 ' ids - Adds the page ID', 00336 ' title - Adds the title and namespace ID of the page', 00337 ' sortkey - Adds the sortkey used for sorting in the category (hexadecimal string)', 00338 ' sortkeyprefix - Adds the sortkey prefix used for sorting in the category (human-readable part of the sortkey)', 00339 ' type - Adds the type that the page has been categorised as (page, subcat or file)', 00340 ' timestamp - Adds the timestamp of when the page was included', 00341 ), 00342 'namespace' => 'Only include pages in these namespaces', 00343 'type' => "What type of category members to include. Ignored when {$p}sort=timestamp is set", 00344 'sort' => 'Property to sort by', 00345 'dir' => 'In which direction to sort', 00346 'start' => "Timestamp to start listing from. Can only be used with {$p}sort=timestamp", 00347 'end' => "Timestamp to end listing at. Can only be used with {$p}sort=timestamp", 00348 'startsortkey' => "Sortkey to start listing from. Must be given in binary format. Can only be used with {$p}sort=sortkey", 00349 'endsortkey' => "Sortkey to end listing at. Must be given in binary format. Can only be used with {$p}sort=sortkey", 00350 'startsortkeyprefix' => "Sortkey prefix to start listing from. Can only be used with {$p}sort=sortkey. Overrides {$p}startsortkey", 00351 'endsortkeyprefix' => "Sortkey prefix to end listing BEFORE (not at, if this value occurs it will not be included!). Can only be used with {$p}sort=sortkey. Overrides {$p}endsortkey", 00352 'continue' => 'For large categories, give the value returned from previous query', 00353 'limit' => 'The maximum number of pages to return.', 00354 ); 00355 00356 if ( $wgMiserMode ) { 00357 $desc['namespace'] = array( 00358 $desc['namespace'], 00359 "NOTE: Due to \$wgMiserMode, using this may result in fewer than \"{$p}limit\" results", 00360 'returned before continuing; in extreme cases, zero results may be returned.', 00361 "Note that you can use {$p}type=subcat or {$p}type=file instead of {$p}namespace=14 or 6.", 00362 ); 00363 } 00364 return $desc; 00365 } 00366 00367 public function getResultProperties() { 00368 return array( 00369 'ids' => array( 00370 'pageid' => 'integer' 00371 ), 00372 'title' => array( 00373 'ns' => 'namespace', 00374 'title' => 'string' 00375 ), 00376 'sortkey' => array( 00377 'sortkey' => 'string' 00378 ), 00379 'sortkeyprefix' => array( 00380 'sortkeyprefix' => 'string' 00381 ), 00382 'type' => array( 00383 'type' => array( 00384 ApiBase::PROP_TYPE => array( 00385 'page', 00386 'subcat', 00387 'file' 00388 ) 00389 ) 00390 ), 00391 'timestamp' => array( 00392 'timestamp' => 'timestamp' 00393 ) 00394 ); 00395 } 00396 00397 public function getDescription() { 00398 return 'List all pages in a given category'; 00399 } 00400 00401 public function getPossibleErrors() { 00402 return array_merge( parent::getPossibleErrors(), 00403 $this->getTitleOrPageIdErrorMessage(), 00404 array( 00405 array( 'code' => 'invalidcategory', 'info' => 'The category name you entered is not valid' ), 00406 array( 'code' => 'badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ), 00407 ) 00408 ); 00409 } 00410 00411 public function getExamples() { 00412 return array( 00413 'api.php?action=query&list=categorymembers&cmtitle=Category:Physics' => 'Get first 10 pages in [[Category:Physics]]', 00414 'api.php?action=query&generator=categorymembers&gcmtitle=Category:Physics&prop=info' => 'Get page info about first 10 pages in [[Category:Physics]]', 00415 ); 00416 } 00417 00418 public function getHelpUrls() { 00419 return 'https://www.mediawiki.org/wiki/API:Categorymembers'; 00420 } 00421 00422 public function getVersion() { 00423 return __CLASS__ . ': $Id$'; 00424 } 00425 }