MediaWiki  master
ApiQueryCategoryMembers.php
Go to the documentation of this file.
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 }