X-Git-Url: https://scripts.mit.edu/gitweb/autoinstallsdev/mediawiki.git/blobdiff_plain/19e297c21b10b1b8a3acad5e73fc71dcb35db44a..6932310fd58ebef145fa01eb76edf7150284d8ea:/includes/Category.php diff --git a/includes/Category.php b/includes/Category.php index 614933ff..629962d2 100644 --- a/includes/Category.php +++ b/includes/Category.php @@ -1,44 +1,73 @@ mName === null && $this->mID === null ) { throw new MWException( __METHOD__ . ' has both names and IDs null' ); } elseif ( $this->mID === null ) { - $where = array( 'cat_title' => $this->mName ); + $where = [ 'cat_title' => $this->mName ]; } elseif ( $this->mName === null ) { - $where = array( 'cat_id' => $this->mID ); + $where = [ 'cat_id' => $this->mID ]; } else { # Already initialized return true; } - $dbr = wfGetDB( DB_SLAVE ); + + $dbr = wfGetDB( DB_REPLICA ); $row = $dbr->selectRow( 'category', - array( 'cat_id', 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files' ), + [ 'cat_id', 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files' ], $where, __METHOD__ ); @@ -46,12 +75,18 @@ class Category { if ( !$row ) { # Okay, there were no contents. Nothing to initialize. if ( $this->mTitle ) { - # If there is a title object but no record in the category table, treat this as an empty category - $this->mID = false; - $this->mName = $this->mTitle->getDBkey(); - $this->mPages = 0; + # If there is a title object but no record in the category table, + # treat this as an empty category. + $this->mID = false; + $this->mName = $this->mTitle->getDBkey(); + $this->mPages = 0; $this->mSubcats = 0; - $this->mFiles = 0; + $this->mFiles = 0; + + # If the title exists, call refreshCounts to add a row for it. + if ( $mode === self::LAZY_INIT_ROW && $this->mTitle->exists() ) { + DeferredUpdates::addCallableUpdate( [ $this, 'refreshCounts' ] ); + } return true; } else { @@ -59,17 +94,23 @@ class Category { } } - $this->mID = $row->cat_id; - $this->mName = $row->cat_title; - $this->mPages = $row->cat_pages; + $this->mID = $row->cat_id; + $this->mName = $row->cat_title; + $this->mPages = $row->cat_pages; $this->mSubcats = $row->cat_subcats; - $this->mFiles = $row->cat_files; + $this->mFiles = $row->cat_files; - # (bug 13683) If the count is negative, then 1) it's obviously wrong + # (T15683) If the count is negative, then 1) it's obviously wrong # and should not be kept, and 2) we *probably* don't have to scan many # rows to obtain the correct figure, so let's risk a one-time recount. if ( $this->mPages < 0 || $this->mSubcats < 0 || $this->mFiles < 0 ) { - $this->refreshCounts(); + $this->mPages = max( $this->mPages, 0 ); + $this->mSubcats = max( $this->mSubcats, 0 ); + $this->mFiles = max( $this->mFiles, 0 ); + + if ( $mode === self::LAZY_INIT_ROW ) { + DeferredUpdates::addCallableUpdate( [ $this, 'refreshCounts' ] ); + } } return true; @@ -78,7 +119,7 @@ class Category { /** * Factory function. * - * @param $name Array: A category name (no "Category:" prefix). It need + * @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 */ @@ -99,8 +140,8 @@ class Category { /** * Factory function. * - * @param $title Title for the category page - * @return Mixed: category, or false on a totally invalid name + * @param Title $title Title for the category page + * @return Category|bool On a totally invalid name */ public static function newFromTitle( $title ) { $cat = new self(); @@ -114,7 +155,7 @@ class Category { /** * Factory function. * - * @param $id Integer: a category id + * @param int $id A category id * @return Category */ public static function newFromID( $id ) { @@ -126,12 +167,14 @@ class Category { /** * Factory function, for constructing a Category object from a result set * - * @param $row result set row, must contain the cat_xxx fields. If the fields are null, - * the resulting Category object will represent an empty category if a title object - * was given. If the fields are null and no title was given, this method fails and returns false. - * @param $title optional title object for the category represented by the given row. - * May be provided if it is already known, to avoid having to re-create a title object later. - * @return Category + * @param object $row Result set row, must contain the cat_xxx fields. If the + * fields are null, the resulting Category object will represent an empty + * category if a title object was given. If the fields are null and no + * title was given, this method fails and returns false. + * @param Title $title Optional title object for the category represented by + * the given row. May be provided if it is already known, to avoid having + * to re-create a title object later. + * @return Category|false */ public static function newFromRow( $row, $title = null ) { $cat = new self(); @@ -139,7 +182,7 @@ class Category { # NOTE: the row often results from a LEFT JOIN on categorylinks. This may result in # all the cat_xxx fields being null, if the category page exists, but nothing - # was ever added to the category. This case should be treated linke an empty + # was ever added to the category. This case should be treated link an empty # category, if possible. if ( $row->cat_title === null ) { @@ -148,46 +191,69 @@ class Category { # but we can't know that here... return false; } else { - $cat->mName = $title->getDBkey(); # if we have a title object, fetch the category name from there + # if we have a title object, fetch the category name from there + $cat->mName = $title->getDBkey(); } - $cat->mID = false; + $cat->mID = false; $cat->mSubcats = 0; - $cat->mPages = 0; - $cat->mFiles = 0; + $cat->mPages = 0; + $cat->mFiles = 0; } else { - $cat->mName = $row->cat_title; - $cat->mID = $row->cat_id; + $cat->mName = $row->cat_title; + $cat->mID = $row->cat_id; $cat->mSubcats = $row->cat_subcats; - $cat->mPages = $row->cat_pages; - $cat->mFiles = $row->cat_files; + $cat->mPages = $row->cat_pages; + $cat->mFiles = $row->cat_files; } return $cat; } - /** @return mixed DB key name, or false on failure */ - public function getName() { return $this->getX( 'mName' ); } + /** + * @return mixed DB key name, or false on failure + */ + public function getName() { + return $this->getX( 'mName' ); + } - /** @return mixed Category ID, or false on failure */ - public function getID() { return $this->getX( 'mID' ); } + /** + * @return mixed Category ID, or false on failure + */ + public function getID() { + return $this->getX( 'mID' ); + } - /** @return mixed Total number of member pages, or false on failure */ - public function getPageCount() { return $this->getX( 'mPages' ); } + /** + * @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 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' ); } + /** + * @return mixed Number of member files, or false on failure + */ + public function getFileCount() { + return $this->getX( 'mFiles' ); + } /** - * @return mixed The Title for this category, or false on failure. + * @return Title|bool Title for this category, or false on failure. */ public function getTitle() { - if ( $this->mTitle ) return $this->mTitle; + if ( $this->mTitle ) { + return $this->mTitle; + } - if ( !$this->initialize() ) { + if ( !$this->initialize( self::LAZY_INIT_ROW ) ) { return false; } @@ -198,42 +264,48 @@ class Category { /** * Fetch a TitleArray of up to $limit category members, beginning after the * category sort key $offset. - * @param $limit integer - * @param $offset string - * @return TitleArray object for category members. + * @param int|bool $limit + * @param string $offset + * @return TitleArray TitleArray object for category members. */ public function getMembers( $limit = false, $offset = '' ) { - $dbr = wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_REPLICA ); - $conds = array( 'cl_to' => $this->getName(), 'cl_from = page_id' ); - $options = array( 'ORDER BY' => 'cl_sortkey' ); + $conds = [ 'cl_to' => $this->getName(), 'cl_from = page_id' ]; + $options = [ 'ORDER BY' => 'cl_sortkey' ]; if ( $limit ) { - $options[ 'LIMIT' ] = $limit; + $options['LIMIT'] = $limit; } if ( $offset !== '' ) { $conds[] = 'cl_sortkey > ' . $dbr->addQuotes( $offset ); } - return TitleArray::newFromResult( + $result = TitleArray::newFromResult( $dbr->select( - array( 'page', 'categorylinks' ), - array( 'page_id', 'page_namespace', 'page_title', 'page_len', - 'page_is_redirect', 'page_latest' ), + [ 'page', 'categorylinks' ], + [ 'page_id', 'page_namespace', 'page_title', 'page_len', + 'page_is_redirect', 'page_latest' ], $conds, __METHOD__, $options ) ); + + return $result; } - /** Generic accessor */ + /** + * Generic accessor + * @param string $key + * @return bool + */ private function getX( $key ) { - if ( !$this->initialize() ) { + if ( !$this->initialize( self::LAZY_INIT_ROW ) ) { return false; } - return $this-> { $key } ; + return $this->{$key}; } /** @@ -246,61 +318,92 @@ class Category { return false; } - # Note, we must use names for this, since categorylinks does. - if ( $this->mName === null ) { - if ( !$this->initialize() ) { - return false; - } + # If we have just a category name, find out whether there is an + # existing row. Or if we have just an ID, get the name, because + # that's what categorylinks uses. + if ( !$this->initialize( self::LOAD_ONLY ) ) { + return false; } $dbw = wfGetDB( DB_MASTER ); - $dbw->begin(); - - # Insert the row if it doesn't exist yet (e.g., this is being run via - # update.php from a pre-1.16 schema). TODO: This will cause lots and - # lots of gaps on some non-MySQL DBMSes if you run populateCategory.php - # repeatedly. Plus it's an extra query that's unneeded almost all the - # time. This should be rewritten somehow, probably. - $seqVal = $dbw->nextSequenceValue( 'category_cat_id_seq' ); - $dbw->insert( - 'category', - array( - 'cat_id' => $seqVal, - 'cat_title' => $this->mName - ), - __METHOD__, - 'IGNORE' - ); + # Avoid excess contention on the same category (T162121) + $name = __METHOD__ . ':' . md5( $this->mName ); + $scopedLock = $dbw->getScopedLockAndFlush( $name, __METHOD__, 1 ); + if ( !$scopedLock ) { + return false; + } - $cond1 = $dbw->conditional( 'page_namespace=' . NS_CATEGORY, 1, 'NULL' ); - $cond2 = $dbw->conditional( 'page_namespace=' . NS_FILE, 1, 'NULL' ); + $dbw->startAtomic( __METHOD__ ); + + $cond1 = $dbw->conditional( [ 'page_namespace' => NS_CATEGORY ], 1, 'NULL' ); + $cond2 = $dbw->conditional( [ 'page_namespace' => NS_FILE ], 1, 'NULL' ); $result = $dbw->selectRow( - array( 'categorylinks', 'page' ), - array( 'COUNT(*) AS pages', - "COUNT($cond1) AS subcats", - "COUNT($cond2) AS files" - ), - array( 'cl_to' => $this->mName, 'page_id = cl_from' ), + [ 'categorylinks', 'page' ], + [ 'pages' => 'COUNT(*)', + 'subcats' => "COUNT($cond1)", + 'files' => "COUNT($cond2)" + ], + [ 'cl_to' => $this->mName, '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->mName ), - __METHOD__ + [ 'LOCK IN SHARE MODE' ] ); - $dbw->commit(); + + $shouldExist = $result->pages > 0 || $this->getTitle()->exists(); + + if ( $this->mID ) { + if ( $shouldExist ) { + # The category row already exists, so do a plain UPDATE instead + # of INSERT...ON DUPLICATE KEY UPDATE to avoid creating a gap + # in the cat_id sequence. The row may or may not be "affected". + $dbw->update( + 'category', + [ + 'cat_pages' => $result->pages, + 'cat_subcats' => $result->subcats, + 'cat_files' => $result->files + ], + [ 'cat_title' => $this->mName ], + __METHOD__ + ); + } else { + # The category is empty and has no description page, delete it + $dbw->delete( + 'category', + [ 'cat_title' => $this->mName ], + __METHOD__ + ); + $this->mID = false; + } + } elseif ( $shouldExist ) { + # The category row doesn't exist but should, so create it. Use + # upsert in case of races. + $dbw->upsert( + 'category', + [ + 'cat_title' => $this->mName, + 'cat_pages' => $result->pages, + 'cat_subcats' => $result->subcats, + 'cat_files' => $result->files + ], + [ 'cat_title' ], + [ + 'cat_pages' => $result->pages, + 'cat_subcats' => $result->subcats, + 'cat_files' => $result->files + ], + __METHOD__ + ); + // @todo: Should we update $this->mID here? Or not since Category + // objects tend to be short lived enough to not matter? + } + + $dbw->endAtomic( __METHOD__ ); # Now we should update our local counts. - $this->mPages = $result->pages; + $this->mPages = $result->pages; $this->mSubcats = $result->subcats; - $this->mFiles = $result->files; + $this->mFiles = $result->files; - return $ret; + return true; } }