307 lines
8.6 KiB
PHP
307 lines
8.6 KiB
PHP
<?php
|
|
/**
|
|
* Two classes, Category and CategoryList, to deal with categories. To reduce
|
|
* code duplication, most of the logic is implemented for lists of categories,
|
|
* and then single categories are a special case. We use a separate class for
|
|
* CategoryList so as to discourage stupid slow memory-hogging stuff like manu-
|
|
* ally iterating through arrays of Titles and Articles, which we do way too
|
|
* much, when a smarter class can do stuff all in one query.
|
|
*
|
|
* Category(List) objects are immutable, strictly speaking. If you call me-
|
|
* thods that change the database, like to refresh link counts, the objects
|
|
* will be appropriately reinitialized. Member variables are lazy-initialized.
|
|
*
|
|
* TODO: Move some stuff from CategoryPage.php to here, and use that.
|
|
*
|
|
* @author Simetrical
|
|
*/
|
|
|
|
abstract class CategoryListBase {
|
|
# FIXME: Is storing all member variables as simple arrays a good idea?
|
|
# Should we use some kind of associative array instead?
|
|
/** Names of all member categories, normalized to DB-key form */
|
|
protected $mNames = null;
|
|
/** IDs of all member categories */
|
|
protected $mIDs = null;
|
|
/**
|
|
* Counts of membership (cat_pages, cat_subcats, cat_files) for all member
|
|
* categories
|
|
*/
|
|
protected $mPages = null, $mSubcats = null, $mFiles = null;
|
|
|
|
protected function __construct() {}
|
|
|
|
/** See CategoryList::newFromNames for details. */
|
|
protected function setNames( $names ) {
|
|
if( !is_array( $names ) ) {
|
|
throw new MWException( __METHOD__.' passed non-array' );
|
|
}
|
|
$this->mNames = array_diff(
|
|
array_map(
|
|
array( 'CategoryListBase', 'setNamesCallback' ),
|
|
$names
|
|
),
|
|
array( false )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param string $name Name of a putative category
|
|
* @return mixed Normalized name, or false if the name was invalid.
|
|
*/
|
|
private static function setNamesCallback( $name ) {
|
|
$title = Title::newFromText( "Category:$name" );
|
|
if( !is_object( $title ) ) {
|
|
return false;
|
|
}
|
|
return $title->getDBKey();
|
|
}
|
|
|
|
/**
|
|
* Set up all member variables using a database query.
|
|
* @return bool True on success, false on failure.
|
|
*/
|
|
protected function initialize() {
|
|
if( $this->mNames === null && $this->mIDs === null ) {
|
|
throw new MWException( __METHOD__.' has both names and IDs null' );
|
|
}
|
|
$dbr = wfGetDB( DB_SLAVE );
|
|
if( $this->mIDs === null ) {
|
|
$where = array( 'cat_title' => $this->mNames );
|
|
} elseif( $this->mNames === null ) {
|
|
$where = array( 'cat_id' => $this->mIDs );
|
|
} else {
|
|
# Already initialized
|
|
return true;
|
|
}
|
|
$res = $dbr->select(
|
|
'category',
|
|
array( 'cat_id', 'cat_title', 'cat_pages', 'cat_subcats',
|
|
'cat_files' ),
|
|
$where,
|
|
__METHOD__
|
|
);
|
|
if( !$res->fetchRow() ) {
|
|
# Okay, there were no contents. Nothing to initialize.
|
|
return false;
|
|
}
|
|
$res->rewind();
|
|
$this->mIDs = $this->mNames = $this->mPages = $this->mSubcats =
|
|
$this->mFiles = array();
|
|
while( $row = $res->fetchRow() ) {
|
|
$this->mIDs []= $row['cat_id'];
|
|
$this->mNames []= $row['cat_title'];
|
|
$this->mPages []= $row['cat_pages'];
|
|
$this->mSubcats []= $row['cat_subcats'];
|
|
$this->mFiles []= $row['cat_files'];
|
|
}
|
|
$res->free();
|
|
}
|
|
}
|
|
|
|
/** @todo make iterable. */
|
|
class CategoryList extends CategoryListBase {
|
|
/**
|
|
* Factory function. Any provided elements that don't correspond to a cat-
|
|
* egory that actually exists will be silently dropped. FIXME: Is this
|
|
* sane error-handling?
|
|
*
|
|
* @param array $names An array of category names. They need not be norma-
|
|
* lized, with spaces replaced by underscores.
|
|
* @return CategoryList
|
|
*/
|
|
public static function newFromNames( $names ) {
|
|
$cat = new self();
|
|
$cat->setNames( $names );
|
|
return $cat;
|
|
}
|
|
|
|
/**
|
|
* Factory function. Any provided elements that don't correspond to a cat-
|
|
* egory that actually exists will be silently dropped. FIXME: Is this
|
|
* sane error-handling?
|
|
*
|
|
* @param array $ids An array of category ids
|
|
* @return CategoryList
|
|
*/
|
|
public static function newFromIDs( $ids ) {
|
|
if( !is_array( $ids ) ) {
|
|
throw new MWException( __METHOD__.' passed non-array' );
|
|
}
|
|
$cat = new self();
|
|
$cat->mIds = $ids;
|
|
return $cat;
|
|
}
|
|
|
|
/** @return array Simple array of DB key names */
|
|
public function getNames() {
|
|
$this->initialize();
|
|
return $this->mNames;
|
|
}
|
|
/**
|
|
* FIXME: Is this a good return type?
|
|
*
|
|
* @return array Associative array of DB key name => ID
|
|
*/
|
|
public function getIDs() {
|
|
$this->initialize();
|
|
return array_fill_keys( $this->mNames, $this->mIDs );
|
|
}
|
|
/**
|
|
* FIXME: Is this a good return type?
|
|
*
|
|
* @return array Associative array of DB key name => array(pages, subcats,
|
|
* files)
|
|
*/
|
|
public function getCounts() {
|
|
$this->initialize();
|
|
$ret = array();
|
|
foreach( array_keys( $this->mNames ) as $i ) {
|
|
$ret[$this->mNames[$i]] = array(
|
|
$this->mPages[$i],
|
|
$this->mSubcats[$i],
|
|
$this->mFiles[$i]
|
|
);
|
|
}
|
|
return $ret;
|
|
}
|
|
}
|
|
|
|
class Category extends CategoryListBase {
|
|
/**
|
|
* Factory function.
|
|
*
|
|
* @param array $name A category name (no "Category:" prefix). It need
|
|
* not be normalized, with spaces replaced by underscores.
|
|
* @return mixed Category, or false on a totally invalid name
|
|
*/
|
|
public static function newFromName( $name ) {
|
|
$cat = new self();
|
|
$cat->setNames( array( $name ) );
|
|
if( count( $cat->mNames ) !== 1 ) {
|
|
return false;
|
|
}
|
|
return $cat;
|
|
}
|
|
|
|
/**
|
|
* Factory function.
|
|
*
|
|
* @param array $id A category id
|
|
* @return Category
|
|
*/
|
|
public static function newFromIDs( $id ) {
|
|
$cat = new self();
|
|
$cat->mIDs = array( $id );
|
|
return $cat;
|
|
}
|
|
|
|
/** @return mixed DB key name, or false on failure */
|
|
public function getName() { return $this->getX( 'mNames' ); }
|
|
/** @return mixed Category ID, or false on failure */
|
|
public function getID() { return $this->getX( 'mIDs' ); }
|
|
/** @return mixed Total number of member pages, or false on failure */
|
|
public function getPageCount() { return $this->getX( 'mPages' ); }
|
|
/** @return mixed Number of subcategories, or false on failure */
|
|
public function getSubcatCount() { return $this->getX( 'mSubcats' ); }
|
|
/** @return mixed Number of member files, or false on failure */
|
|
public function getFileCount() { return $this->getX( 'mFiles' ); }
|
|
|
|
/**
|
|
* This is not implemented in the base class, because arrays of Titles are
|
|
* evil.
|
|
*
|
|
* @return mixed The Title for this category, or false on failure.
|
|
*/
|
|
public function getTitle() {
|
|
if( !$this->initialize() ) {
|
|
return false;
|
|
}
|
|
return Title::makeTitleSafe( NS_CATEGORY, $this->mNames[0] );
|
|
}
|
|
|
|
/** Generic accessor */
|
|
private function getX( $key ) {
|
|
if( !$this->initialize() ) {
|
|
return false;
|
|
}
|
|
return $this->{$key}[0];
|
|
}
|
|
|
|
/**
|
|
* Override the parent class so that we can return false if things muck
|
|
* up, i.e., the name/ID we got was invalid. Currently CategoryList si-
|
|
* lently eats errors so as not to kill the whole array for one bad name.
|
|
*
|
|
* @return bool True on success, false on failure.
|
|
*/
|
|
protected function initialize() {
|
|
parent::initialize();
|
|
if( count( $this->mNames ) != 1 || count( $this->mIDs ) != 1 ) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Refresh the counts for this category.
|
|
*
|
|
* FIXME: If there were some way to do this in MySQL 4 without an UPDATE
|
|
* for every row, it would be nice to move this to the parent class.
|
|
*
|
|
* @return bool True on success, false on failure
|
|
*/
|
|
public function refreshCounts() {
|
|
if( wfReadOnly() ) {
|
|
return false;
|
|
}
|
|
$dbw = wfGetDB( DB_MASTER );
|
|
$dbw->begin();
|
|
# Note, we must use names for this, since categorylinks does.
|
|
if( $this->mNames === null ) {
|
|
if( !$this->initialize() ) {
|
|
return false;
|
|
}
|
|
} else {
|
|
# Let's be sure that the row exists in the table. We don't need to
|
|
# do this if we got the row from the table in initialization!
|
|
$dbw->insert(
|
|
'category',
|
|
array( 'cat_title' => $this->mNames[0] ),
|
|
__METHOD__,
|
|
'IGNORE'
|
|
);
|
|
}
|
|
|
|
$cond1 = $dbw->conditional( 'page_namespace='.NS_CATEGORY, 1, 'NULL' );
|
|
$cond2 = $dbw->conditional( 'page_namespace='.NS_IMAGE, 1, 'NULL' );
|
|
$result = $dbw->selectRow(
|
|
array( 'categorylinks', 'page' ),
|
|
array( 'COUNT(*) AS pages',
|
|
"COUNT($cond1) AS subcats",
|
|
"COUNT($cond2) AS files"
|
|
),
|
|
array( 'cl_to' => $this->mNames[0], 'page_id = cl_from' ),
|
|
__METHOD__,
|
|
'LOCK IN SHARE MODE'
|
|
);
|
|
$ret = $dbw->update(
|
|
'category',
|
|
array(
|
|
'cat_pages' => $result->pages,
|
|
'cat_subcats' => $result->subcats,
|
|
'cat_files' => $result->files
|
|
),
|
|
array( 'cat_title' => $this->mNames[0] ),
|
|
__METHOD__
|
|
);
|
|
$dbw->commit();
|
|
|
|
# Now we should update our local counts.
|
|
$this->mPages = array( $result->pages );
|
|
$this->mSubcats = array( $result->subcats );
|
|
$this->mFiles = array( $result->files );
|
|
|
|
return $ret;
|
|
}
|
|
}
|