/** * @since 1.19 $context is a second, required parameter * @param Title $title * @param IContextSource $context * @param array $from An array with keys page, subcat, * and file for offset of results of each section (since 1.17) * @param array $until An array with 3 keys for until of each section (since 1.17) * @param array $query */ function __construct($title, IContextSource $context, $from = array(), $until = array(), $query = array()) { $this->title = $title; $this->setContext($context); $this->from = $from; $this->until = $until; $this->limit = $context->getConfig()->get('CategoryPagingLimit'); $this->cat = Category::newFromTitle($title); $this->query = $query; $this->collation = Collation::singleton(); unset($this->query['title']); }
/** * @since 1.19 $context is a second, required parameter * @param Title $title * @param IContextSource $context * @param array $from An array with keys page, subcat, * and file for offset of results of each section (since 1.17) * @param array $until An array with 3 keys for until of each section (since 1.17) * @param array $query */ function __construct($title, IContextSource $context, $from = array(), $until = array(), $query = array()) { $this->title = $title; $this->setContext($context); $this->getOutput()->addModuleStyles(array('mediawiki.action.view.categoryPage.styles')); $this->from = $from; $this->until = $until; $this->limit = $context->getConfig()->get('CategoryPagingLimit'); $this->cat = Category::newFromTitle($title); $this->query = $query; $this->collation = Collation::singleton(); unset($this->query['title']); }
/** * Constructor * * @since 1.19 $context is a second, required parameter * @param $title Title * @param $context IContextSource * @param $from String * @param $until String * @param $query Array */ function __construct($title, IContextSource $context, $from = array(), $until = array(), $query = array()) { global $wgCategoryPagingLimit; $default = array('page' => null, 'subcat' => null, 'file' => null); $this->title = $title; $this->setContext($context); $this->from = array_merge($default, $from); $this->until = array_merge($default, $until); $this->limit = $wgCategoryPagingLimit; $this->cat = Category::newFromTitle($title); $this->query = $query; $this->collation = Collation::singleton(); unset($this->query['title']); }
/** * Get an array of category insertions * * @param array $existing Mapping existing category names to sort keys. If both * match a link in $this, the link will be omitted from the output * * @return array */ private function getCategoryInsertions($existing = array()) { global $wgContLang, $wgCategoryCollation; $diffs = array_diff_assoc($this->mCategories, $existing); $arr = array(); foreach ($diffs as $name => $prefix) { $nt = Title::makeTitleSafe(NS_CATEGORY, $name); $wgContLang->findVariantLink($name, $nt, true); if ($this->mTitle->getNamespace() == NS_CATEGORY) { $type = 'subcat'; } elseif ($this->mTitle->getNamespace() == NS_FILE) { $type = 'file'; } else { $type = 'page'; } # Treat custom sortkeys as a prefix, so that if multiple # things are forced to sort as '*' or something, they'll # sort properly in the category rather than in page_id # order or such. $sortkey = Collation::singleton()->getSortKey($this->mTitle->getCategorySortkey($prefix)); $arr[] = array('cl_from' => $this->mId, 'cl_to' => $name, 'cl_sortkey' => $sortkey, 'cl_timestamp' => $this->mDb->timestamp(), 'cl_sortkey_prefix' => $prefix, 'cl_collation' => $wgCategoryCollation, 'cl_type' => $type); } return $arr; }
/** * Move a title to a new location * * @param $nt Title the new title * @param $auth Bool indicates whether $wgUser's permissions * should be checked * @param $reason String the reason for the move * @param $createRedirect Bool Whether to create a redirect from the old title to the new title. * Ignored if the user doesn't have the suppressredirect right. * @return Mixed true on success, getUserPermissionsErrors()-like array on failure */ public function moveTo(&$nt, $auth = true, $reason = '', $createRedirect = true) { global $wgUser; $err = $this->isValidMoveOperation($nt, $auth, $reason); if (is_array($err)) { // Auto-block user's IP if the account was "hard" blocked $wgUser->spreadAnyEditBlock(); return $err; } // If it is a file, move it first. It is done before all other moving stuff is // done because it's hard to revert $dbw = wfGetDB(DB_MASTER); if ($this->getNamespace() == NS_FILE) { $file = wfLocalFile($this); if ($file->exists()) { $status = $file->move($nt); if (!$status->isOk()) { return $status->getErrorsArray(); } } } $dbw->begin(); # If $file was a LocalFile, its transaction would have closed our own. $pageid = $this->getArticleID(self::GAID_FOR_UPDATE); $protected = $this->isProtected(); $pageCountChange = ($createRedirect ? 1 : 0) - ($nt->exists() ? 1 : 0); // Do the actual move $err = $this->moveToInternal($nt, $reason, $createRedirect); if (is_array($err)) { # @todo FIXME: What about the File we have already moved? $dbw->rollback(); return $err; } $redirid = $this->getArticleID(); // Refresh the sortkey for this row. Be careful to avoid resetting // cl_timestamp, which may disturb time-based lists on some sites. $prefixes = $dbw->select('categorylinks', array('cl_sortkey_prefix', 'cl_to'), array('cl_from' => $pageid), __METHOD__); foreach ($prefixes as $prefixRow) { $prefix = $prefixRow->cl_sortkey_prefix; $catTo = $prefixRow->cl_to; $dbw->update('categorylinks', array('cl_sortkey' => Collation::singleton()->getSortKey($nt->getCategorySortkey($prefix)), 'cl_timestamp=cl_timestamp'), array('cl_from' => $pageid, 'cl_to' => $catTo), __METHOD__); } if ($protected) { # Protect the redirect title as the title used to be... $dbw->insertSelect('page_restrictions', 'page_restrictions', array('pr_page' => $redirid, 'pr_type' => 'pr_type', 'pr_level' => 'pr_level', 'pr_cascade' => 'pr_cascade', 'pr_user' => 'pr_user', 'pr_expiry' => 'pr_expiry'), array('pr_page' => $pageid), __METHOD__, array('IGNORE')); # Update the protection log $log = new LogPage('protect'); $comment = wfMsgForContent('prot_1movedto2', $this->getPrefixedText(), $nt->getPrefixedText()); if ($reason) { $comment .= wfMsgForContent('colon-separator') . $reason; } // @todo FIXME: $params? $log->addEntry('move_prot', $nt, $comment, array($this->getPrefixedText())); } # Update watchlists $oldnamespace = $this->getNamespace() & ~1; $newnamespace = $nt->getNamespace() & ~1; $oldtitle = $this->getDBkey(); $newtitle = $nt->getDBkey(); if ($oldnamespace != $newnamespace || $oldtitle != $newtitle) { WatchedItem::duplicateEntries($this, $nt); } # Update search engine $u = new SearchUpdate($pageid, $nt->getPrefixedDBkey()); $u->doUpdate(); $u = new SearchUpdate($redirid, $this->getPrefixedDBkey(), ''); $u->doUpdate(); $dbw->commit(); # Update site_stats if ($this->isContentPage() && !$nt->isContentPage()) { # No longer a content page # Not viewed, edited, removing $u = new SiteStatsUpdate(0, 1, -1, $pageCountChange); } elseif (!$this->isContentPage() && $nt->isContentPage()) { # Now a content page # Not viewed, edited, adding $u = new SiteStatsUpdate(0, 1, +1, $pageCountChange); } elseif ($pageCountChange) { # Redirect added $u = new SiteStatsUpdate(0, 0, 0, 1); } else { # Nothing special $u = false; } if ($u) { $u->doUpdate(); } # Update message cache for interface messages if ($this->getNamespace() == NS_MEDIAWIKI) { # @bug 17860: old article can be deleted, if this the case, # delete it from message cache if ($this->getArticleID() === 0) { MessageCache::singleton()->replace($this->getDBkey(), false); } else { $oldarticle = new Article($this); MessageCache::singleton()->replace($this->getDBkey(), $oldarticle->getContent()); } } if ($nt->getNamespace() == NS_MEDIAWIKI) { $newarticle = new Article($nt); MessageCache::singleton()->replace($nt->getDBkey(), $newarticle->getContent()); } global $wgUser; wfRunHooks('TitleMoveComplete', array(&$this, &$nt, &$wgUser, $pageid, $redirid)); return true; }
function __construct($title, $from = '', $until = '', $query = array()) { global $wgCategoryPagingLimit; $this->title = $title; $this->from = $from; $this->until = $until; $this->limit = $wgCategoryPagingLimit; $this->cat = Category::newFromTitle($title); $this->query = $query; $this->collation = Collation::singleton(); unset($this->query['title']); }
/** * Move a title to a new location * * @param Title $nt The new title * @param bool $auth Indicates whether $wgUser's permissions * should be checked * @param string $reason The reason for the move * @param bool $createRedirect Whether to create a redirect from the old title to the new title. * Ignored if the user doesn't have the suppressredirect right. * @return array|bool True on success, getUserPermissionsErrors()-like array on failure */ public function moveTo(&$nt, $auth = true, $reason = '', $createRedirect = true) { global $wgUser; $err = $this->isValidMoveOperation($nt, $auth, $reason); if (is_array($err)) { // Auto-block user's IP if the account was "hard" blocked $wgUser->spreadAnyEditBlock(); return $err; } // Check suppressredirect permission if ($auth && !$wgUser->isAllowed('suppressredirect')) { $createRedirect = true; } wfRunHooks('TitleMove', array($this, $nt, $wgUser)); // If it is a file, move it first. // It is done before all other moving stuff is done because it's hard to revert. $dbw = wfGetDB(DB_MASTER); if ($this->getNamespace() == NS_FILE) { $file = wfLocalFile($this); if ($file->exists()) { $status = $file->move($nt); if (!$status->isOk()) { return $status->getErrorsArray(); } } // Clear RepoGroup process cache RepoGroup::singleton()->clearCache($this); RepoGroup::singleton()->clearCache($nt); # clear false negative cache } $dbw->begin(__METHOD__); # If $file was a LocalFile, its transaction would have closed our own. $pageid = $this->getArticleID(self::GAID_FOR_UPDATE); $protected = $this->isProtected(); // Do the actual move $this->moveToInternal($nt, $reason, $createRedirect); // Refresh the sortkey for this row. Be careful to avoid resetting // cl_timestamp, which may disturb time-based lists on some sites. $prefixes = $dbw->select('categorylinks', array('cl_sortkey_prefix', 'cl_to'), array('cl_from' => $pageid), __METHOD__); foreach ($prefixes as $prefixRow) { $prefix = $prefixRow->cl_sortkey_prefix; $catTo = $prefixRow->cl_to; $dbw->update('categorylinks', array('cl_sortkey' => Collation::singleton()->getSortKey($nt->getCategorySortkey($prefix)), 'cl_timestamp=cl_timestamp'), array('cl_from' => $pageid, 'cl_to' => $catTo), __METHOD__); } $redirid = $this->getArticleID(); if ($protected) { # Protect the redirect title as the title used to be... $dbw->insertSelect('page_restrictions', 'page_restrictions', array('pr_page' => $redirid, 'pr_type' => 'pr_type', 'pr_level' => 'pr_level', 'pr_cascade' => 'pr_cascade', 'pr_user' => 'pr_user', 'pr_expiry' => 'pr_expiry'), array('pr_page' => $pageid), __METHOD__, array('IGNORE')); # Update the protection log $log = new LogPage('protect'); $comment = wfMessage('prot_1movedto2', $this->getPrefixedText(), $nt->getPrefixedText())->inContentLanguage()->text(); if ($reason) { $comment .= wfMessage('colon-separator')->inContentLanguage()->text() . $reason; } // @todo FIXME: $params? $logId = $log->addEntry('move_prot', $nt, $comment, array($this->getPrefixedText()), $wgUser); // reread inserted pr_ids for log relation $insertedPrIds = $dbw->select('page_restrictions', 'pr_id', array('pr_page' => $redirid), __METHOD__); $logRelationsValues = array(); foreach ($insertedPrIds as $prid) { $logRelationsValues[] = $prid->pr_id; } $log->addRelations('pr_id', $logRelationsValues, $logId); } // Update *_from_namespace fields as needed if ($this->getNamespace() != $nt->getNamespace()) { $dbw->update('pagelinks', array('pl_from_namespace' => $nt->getNamespace()), array('pl_from' => $pageid), __METHOD__); $dbw->update('templatelinks', array('tl_from_namespace' => $nt->getNamespace()), array('tl_from' => $pageid), __METHOD__); $dbw->update('imagelinks', array('il_from_namespace' => $nt->getNamespace()), array('il_from' => $pageid), __METHOD__); } # Update watchlists $oldtitle = $this->getDBkey(); $newtitle = $nt->getDBkey(); $oldsnamespace = MWNamespace::getSubject($this->getNamespace()); $newsnamespace = MWNamespace::getSubject($nt->getNamespace()); if ($oldsnamespace != $newsnamespace || $oldtitle != $newtitle) { WatchedItem::duplicateEntries($this, $nt); } $dbw->commit(__METHOD__); wfRunHooks('TitleMoveComplete', array(&$this, &$nt, &$wgUser, $pageid, $redirid, $reason)); return true; }
/** * @param ApiPageSet $resultPageSet * @return void */ private function run($resultPageSet = null) { $params = $this->extractRequestParams(); $categoryTitle = $this->getTitleOrPageId($params)->getTitle(); if ($categoryTitle->getNamespace() != NS_CATEGORY) { $this->dieUsage('The category name you entered is not valid', 'invalidcategory'); } $prop = array_flip($params['prop']); $fld_ids = isset($prop['ids']); $fld_title = isset($prop['title']); $fld_sortkey = isset($prop['sortkey']); $fld_sortkeyprefix = isset($prop['sortkeyprefix']); $fld_timestamp = isset($prop['timestamp']); $fld_type = isset($prop['type']); if (is_null($resultPageSet)) { $this->addFields(array('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->addFieldsIf('cl_timestamp', $fld_timestamp || $params['sort'] == 'timestamp'); $this->addTables(array('page', 'categorylinks')); // must be in this order for 'USE INDEX' $this->addWhereFld('cl_to', $categoryTitle->getDBkey()); $queryTypes = $params['type']; $contWhere = false; // Scanning large datasets for rare categories sucks, and I already told // how to have efficient subcategory access :-) ~~~~ (oh well, domas) $miser_ns = array(); if ($this->getConfig()->get('MiserMode')) { $miser_ns = $params['namespace']; } else { $this->addWhereFld('page_namespace', $params['namespace']); } $dir = in_array($params['dir'], array('asc', 'ascending', 'newer')) ? 'newer' : 'older'; if ($params['sort'] == '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); $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 $this->dieContinueUsageIf(!$this->validateHexSortkey($cont[1])); // pack( "H*", $foo ) is used to convert hex back to binary $escSortkey = $this->getDB()->addQuotes(pack('H*', $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'])) { $this->dieUsage('The starthexsortkey provided is not valid', 'bad_starthexsortkey'); } $startsortkey = pack('H*', $params['starthexsortkey']); } else { if ($params['startsortkey'] !== null) { $this->logFeatureUsage('list=categorymembers&cmstartsortkey'); } $startsortkey = $params['startsortkey']; } if ($params['endsortkeyprefix'] !== null) { $endsortkey = Collation::singleton()->getSortkey($params['endsortkeyprefix']); } elseif ($params['endhexsortkey'] !== null) { if (!$this->validateHexSortkey($params['endhexsortkey'])) { $this->dieUsage('The endhexsortkey provided is not valid', 'bad_endhexsortkey'); } $endsortkey = pack('H*', $params['endhexsortkey']); } else { if ($params['endsortkey'] !== null) { $this->logFeatureUsage('list=categorymembers&cmendsortkey'); } $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, $startsortkey, $endsortkey); $this->addWhereRange('cl_from', $dir, null, null); } $this->addOption('USE INDEX', 'cl_sortkey'); } $this->addWhere('cl_from=page_id'); $limit = $params['limit']; $this->addOption('LIMIT', $limit + 1); if ($params['sort'] == 'sortkey') { // Run a separate SELECT query for each value of cl_type. // This is needed because cl_type is an enum, and MySQL has // inconsistencies between ORDER BY cl_type and // WHERE cl_type >= 'foo' making proper paging impossible // and unindexed. $rows = array(); $first = true; foreach ($queryTypes as $type) { $extraConds = array('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)); $rows = array_merge($rows, iterator_to_array($res)); if (count($rows) >= $limit + 1) { break; } $first = false; } } else { // Sorting by timestamp // No need to worry about per-type queries because we // aren't sorting or filtering by type anyway $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 ($params['sort'] == 'timestamp') { $this->setContinueEnumParameter('continue', "{$row->cl_timestamp}|{$row->cl_from}"); } else { $sortkey = bin2hex($row->cl_sortkey); $this->setContinueEnumParameter('continue', "{$row->cl_type}|{$sortkey}|{$row->cl_from}"); } break; } // Since domas won't tell anyone what he told long ago, apply // cmnamespace here. This means the query may return 0 actual // results, but on the other hand it could save returning 5000 // useless results to the client. ~~~~ if (count($miser_ns) && !in_array($row->page_namespace, $miser_ns)) { continue; } if (is_null($resultPageSet)) { $vals = array(ApiResult::META_TYPE => 'assoc'); if ($fld_ids) { $vals['pageid'] = intval($row->page_id); } if ($fld_title) { $title = Title::makeTitle($row->page_namespace, $row->page_title); ApiQueryBase::addTitleInfo($vals, $title); } if ($fld_sortkey) { $vals['sortkey'] = bin2hex($row->cl_sortkey); } if ($fld_sortkeyprefix) { $vals['sortkeyprefix'] = $row->cl_sortkey_prefix; } if ($fld_type) { $vals['type'] = $row->cl_type; } if ($fld_timestamp) { $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->cl_timestamp); } $fit = $result->addValue(array('query', $this->getModuleName()), null, $vals); if (!$fit) { if ($params['sort'] == 'timestamp') { $this->setContinueEnumParameter('continue', "{$row->cl_timestamp}|{$row->cl_from}"); } else { $sortkey = bin2hex($row->cl_sortkey); $this->setContinueEnumParameter('continue', "{$row->cl_type}|{$sortkey}|{$row->cl_from}"); } break; } } else { $resultPageSet->processDbRow($row); } } if (is_null($resultPageSet)) { $result->addIndexedTagName(array('query', $this->getModuleName()), 'cm'); } }
public function execute() { global $wgCategoryCollation, $wgMiserMode; $dbw = wfGetDB(DB_MASTER); $force = $this->getOption('force'); $options = array('LIMIT' => self::BATCH_SIZE); if ($force) { $options['ORDER BY'] = 'cl_from, cl_to'; $collationConds = array(); } else { $collationConds = array(0 => 'cl_collation != ' . $dbw->addQuotes($wgCategoryCollation)); if (!$wgMiserMode) { $count = $dbw->selectField('categorylinks', 'COUNT(*)', $collationConds, __METHOD__); if ($count == 0) { $this->output("Collations up-to-date.\n"); return; } $this->output("Fixing collation for {$count} rows.\n"); } } $count = 0; $row = false; $batchConds = array(); do { $this->output('Processing next ' . self::BATCH_SIZE . ' rows... '); $res = $dbw->select(array('categorylinks', 'page'), array('cl_from', 'cl_to', 'cl_sortkey_prefix', 'cl_collation', 'cl_sortkey', 'page_namespace', 'page_title'), array_merge($collationConds, $batchConds, array('cl_from = page_id')), __METHOD__, $options); $dbw->begin(); foreach ($res as $row) { $title = Title::newFromRow($row); if (!$row->cl_collation) { # This is an old-style row, so the sortkey needs to be # converted. if ($row->cl_sortkey == $title->getText() || $row->cl_sortkey == $title->getPrefixedText()) { $prefix = ''; } else { # Custom sortkey, use it as a prefix $prefix = $row->cl_sortkey; } } else { $prefix = $row->cl_sortkey_prefix; } # cl_type will be wrong for lots of pages if cl_collation is 0, # so let's update it while we're here. if ($title->getNamespace() == NS_CATEGORY) { $type = 'subcat'; } elseif ($title->getNamespace() == NS_FILE) { $type = 'file'; } else { $type = 'page'; } $dbw->update('categorylinks', array('cl_sortkey' => Collation::singleton()->getSortKey($title->getCategorySortkey($prefix)), 'cl_sortkey_prefix' => $prefix, 'cl_collation' => $wgCategoryCollation, 'cl_type' => $type, 'cl_timestamp = cl_timestamp'), array('cl_from' => $row->cl_from, 'cl_to' => $row->cl_to), __METHOD__); } $dbw->commit(); if ($force && $row) { $encFrom = $dbw->addQuotes($row->cl_from); $encTo = $dbw->addQuotes($row->cl_to); $batchConds = array("(cl_from = {$encFrom} AND cl_to > {$encTo}) " . " OR cl_from > {$encFrom}"); } $count += $res->numRows(); $this->output("{$count} done.\n"); $this->syncDBs(); } while ($res->numRows() == self::BATCH_SIZE); }
/** * @param User $user * @param string $reason * @param bool $createRedirect * @return Status */ public function move(User $user, $reason, $createRedirect) { global $wgCategoryCollation; Hooks::run('TitleMove', array($this->oldTitle, $this->newTitle, $user)); // If it is a file, move it first. // It is done before all other moving stuff is done because it's hard to revert. $dbw = wfGetDB(DB_MASTER); if ($this->oldTitle->getNamespace() == NS_FILE) { $file = wfLocalFile($this->oldTitle); $file->load(File::READ_LATEST); if ($file->exists()) { $status = $file->move($this->newTitle); if (!$status->isOk()) { return $status; } } // Clear RepoGroup process cache RepoGroup::singleton()->clearCache($this->oldTitle); RepoGroup::singleton()->clearCache($this->newTitle); # clear false negative cache } $dbw->begin(__METHOD__); # If $file was a LocalFile, its transaction would have closed our own. $pageid = $this->oldTitle->getArticleID(Title::GAID_FOR_UPDATE); $protected = $this->oldTitle->isProtected(); // Do the actual move $this->moveToInternal($user, $this->newTitle, $reason, $createRedirect); // Refresh the sortkey for this row. Be careful to avoid resetting // cl_timestamp, which may disturb time-based lists on some sites. // @todo This block should be killed, it's duplicating code // from LinksUpdate::getCategoryInsertions() and friends. $prefixes = $dbw->select('categorylinks', array('cl_sortkey_prefix', 'cl_to'), array('cl_from' => $pageid), __METHOD__); if ($this->newTitle->getNamespace() == NS_CATEGORY) { $type = 'subcat'; } elseif ($this->newTitle->getNamespace() == NS_FILE) { $type = 'file'; } else { $type = 'page'; } foreach ($prefixes as $prefixRow) { $prefix = $prefixRow->cl_sortkey_prefix; $catTo = $prefixRow->cl_to; $dbw->update('categorylinks', array('cl_sortkey' => Collation::singleton()->getSortKey($this->newTitle->getCategorySortkey($prefix)), 'cl_collation' => $wgCategoryCollation, 'cl_type' => $type, 'cl_timestamp=cl_timestamp'), array('cl_from' => $pageid, 'cl_to' => $catTo), __METHOD__); } $redirid = $this->oldTitle->getArticleID(); if ($protected) { # Protect the redirect title as the title used to be... $dbw->insertSelect('page_restrictions', 'page_restrictions', array('pr_page' => $redirid, 'pr_type' => 'pr_type', 'pr_level' => 'pr_level', 'pr_cascade' => 'pr_cascade', 'pr_user' => 'pr_user', 'pr_expiry' => 'pr_expiry'), array('pr_page' => $pageid), __METHOD__, array('IGNORE')); // Build comment for log $comment = wfMessage('prot_1movedto2', $this->oldTitle->getPrefixedText(), $this->newTitle->getPrefixedText())->inContentLanguage()->text(); if ($reason) { $comment .= wfMessage('colon-separator')->inContentLanguage()->text() . $reason; } // reread inserted pr_ids for log relation $insertedPrIds = $dbw->select('page_restrictions', 'pr_id', array('pr_page' => $redirid), __METHOD__); $logRelationsValues = array(); foreach ($insertedPrIds as $prid) { $logRelationsValues[] = $prid->pr_id; } // Update the protection log $logEntry = new ManualLogEntry('protect', 'move_prot'); $logEntry->setTarget($this->newTitle); $logEntry->setComment($comment); $logEntry->setPerformer($user); $logEntry->setParameters(array('4::oldtitle' => $this->oldTitle->getPrefixedText())); $logEntry->setRelations(array('pr_id' => $logRelationsValues)); $logId = $logEntry->insert(); $logEntry->publish($logId); } // Update *_from_namespace fields as needed if ($this->oldTitle->getNamespace() != $this->newTitle->getNamespace()) { $dbw->update('pagelinks', array('pl_from_namespace' => $this->newTitle->getNamespace()), array('pl_from' => $pageid), __METHOD__); $dbw->update('templatelinks', array('tl_from_namespace' => $this->newTitle->getNamespace()), array('tl_from' => $pageid), __METHOD__); $dbw->update('imagelinks', array('il_from_namespace' => $this->newTitle->getNamespace()), array('il_from' => $pageid), __METHOD__); } # Update watchlists $oldtitle = $this->oldTitle->getDBkey(); $newtitle = $this->newTitle->getDBkey(); $oldsnamespace = MWNamespace::getSubject($this->oldTitle->getNamespace()); $newsnamespace = MWNamespace::getSubject($this->newTitle->getNamespace()); if ($oldsnamespace != $newsnamespace || $oldtitle != $newtitle) { WatchedItem::duplicateEntries($this->oldTitle, $this->newTitle); } $dbw->commit(__METHOD__); Hooks::run('TitleMoveComplete', array(&$this->oldTitle, &$this->newTitle, &$user, $pageid, $redirid, $reason)); return Status::newGood(); }
public function execute() { global $wgCategoryCollation; $dbw = $this->getDB( DB_MASTER ); $force = $this->getOption( 'force' ); $dryRun = $this->getOption( 'dry-run' ); $verboseStats = $this->getOption( 'verbose-stats' ); if ( $this->hasOption( 'target-collation' ) ) { $collationName = $this->getOption( 'target-collation' ); $collation = Collation::factory( $collationName ); } else { $collationName = $wgCategoryCollation; $collation = Collation::singleton(); } // Collation sanity check: in some cases the constructor will work, // but this will raise an exception, breaking all category pages $collation->getFirstLetter( 'MediaWiki' ); $options = array( 'LIMIT' => self::BATCH_SIZE, 'ORDER BY' => 'cl_to, cl_type, cl_from', 'STRAIGHT_JOIN', ); if ( $force || $dryRun ) { $collationConds = array(); } else { if ( $this->hasOption( 'previous-collation' ) ) { $collationConds['cl_collation'] = $this->getOption( 'previous-collation' ); } else { $collationConds = array( 0 => 'cl_collation != ' . $dbw->addQuotes( $collationName ) ); } $count = $dbw->estimateRowCount( 'categorylinks', '*', $collationConds, __METHOD__ ); // Improve estimate if feasible if ( $count < 1000000 ) { $count = $dbw->selectField( 'categorylinks', 'COUNT(*)', $collationConds, __METHOD__ ); } if ( $count == 0 ) { $this->output( "Collations up-to-date.\n" ); return; } $this->output( "Fixing collation for $count rows.\n" ); } $count = 0; $batchCount = 0; $batchConds = array(); do { $this->output( "Selecting next " . self::BATCH_SIZE . " rows..." ); $res = $dbw->select( array( 'categorylinks', 'page' ), array( 'cl_from', 'cl_to', 'cl_sortkey_prefix', 'cl_collation', 'cl_sortkey', 'cl_type', 'page_namespace', 'page_title' ), array_merge( $collationConds, $batchConds, array( 'cl_from = page_id' ) ), __METHOD__, $options ); $this->output( " processing..." ); if ( !$dryRun ) { $dbw->begin( __METHOD__ ); } foreach ( $res as $row ) { $title = Title::newFromRow( $row ); if ( !$row->cl_collation ) { # This is an old-style row, so the sortkey needs to be # converted. if ( $row->cl_sortkey == $title->getText() || $row->cl_sortkey == $title->getPrefixedText() ) { $prefix = ''; } else { # Custom sortkey, use it as a prefix $prefix = $row->cl_sortkey; } } else { $prefix = $row->cl_sortkey_prefix; } # cl_type will be wrong for lots of pages if cl_collation is 0, # so let's update it while we're here. if ( $title->getNamespace() == NS_CATEGORY ) { $type = 'subcat'; } elseif ( $title->getNamespace() == NS_FILE ) { $type = 'file'; } else { $type = 'page'; } $newSortKey = $collation->getSortKey( $title->getCategorySortkey( $prefix ) ); if ( $verboseStats ) { $this->updateSortKeySizeHistogram( $newSortKey ); } if ( !$dryRun ) { $dbw->update( 'categorylinks', array( 'cl_sortkey' => $newSortKey, 'cl_sortkey_prefix' => $prefix, 'cl_collation' => $collationName, 'cl_type' => $type, 'cl_timestamp = cl_timestamp', ), array( 'cl_from' => $row->cl_from, 'cl_to' => $row->cl_to ), __METHOD__ ); } } if ( !$dryRun ) { $dbw->commit( __METHOD__ ); } if ( $row ) { $batchConds = array( $this->getBatchCondition( $row ) ); } $count += $res->numRows(); $this->output( "$count done.\n" ); if ( !$dryRun && ++$batchCount % self::SYNC_INTERVAL == 0 ) { $this->output( "Waiting for slaves ... " ); wfWaitForSlaves(); $this->output( "done\n" ); } } while ( $res->numRows() == self::BATCH_SIZE ); $this->output( "$count rows processed\n" ); if ( $verboseStats ) { $this->output( "\n" ); $this->showSortKeySizeHistogram(); } }
public function execute() { global $wgCategoryCollation; $dbw = $this->getDB(DB_MASTER); $dbr = $this->getDB(DB_SLAVE); $force = $this->getOption('force'); $dryRun = $this->getOption('dry-run'); $verboseStats = $this->getOption('verbose-stats'); if ($this->hasOption('target-collation')) { $collationName = $this->getOption('target-collation'); $collation = Collation::factory($collationName); } else { $collationName = $wgCategoryCollation; $collation = Collation::singleton(); } // Collation sanity check: in some cases the constructor will work, // but this will raise an exception, breaking all category pages $collation->getFirstLetter('MediaWiki'); // Locally at least, (my local is a rather old version of mysql) // mysql seems to filesort if there is both an equality // (but not for an inequality) condition on cl_collation in the // WHERE and it is also the first item in the ORDER BY. if ($this->hasOption('previous-collation')) { $orderBy = 'cl_to, cl_type, cl_from'; } else { $orderBy = 'cl_collation, cl_to, cl_type, cl_from'; } $options = ['LIMIT' => self::BATCH_SIZE, 'ORDER BY' => $orderBy, 'STRAIGHT_JOIN']; if ($force || $dryRun) { $collationConds = []; } else { if ($this->hasOption('previous-collation')) { $collationConds['cl_collation'] = $this->getOption('previous-collation'); } else { $collationConds = [0 => 'cl_collation != ' . $dbw->addQuotes($collationName)]; } $count = $dbr->estimateRowCount('categorylinks', '*', $collationConds, __METHOD__); // Improve estimate if feasible if ($count < 1000000) { $count = $dbr->selectField('categorylinks', 'COUNT(*)', $collationConds, __METHOD__); } if ($count == 0) { $this->output("Collations up-to-date.\n"); return; } $this->output("Fixing collation for {$count} rows.\n"); wfWaitForSlaves(); } $count = 0; $batchCount = 0; $batchConds = []; do { $this->output("Selecting next " . self::BATCH_SIZE . " rows..."); // cl_type must be selected as a number for proper paging because // enums suck. if ($dbw->getType() === 'mysql') { $clType = 'cl_type+0 AS "cl_type_numeric"'; } else { $clType = 'cl_type'; } $res = $dbw->select(['categorylinks', 'page'], ['cl_from', 'cl_to', 'cl_sortkey_prefix', 'cl_collation', 'cl_sortkey', $clType, 'page_namespace', 'page_title'], array_merge($collationConds, $batchConds, ['cl_from = page_id']), __METHOD__, $options); $this->output(" processing..."); if (!$dryRun) { $this->beginTransaction($dbw, __METHOD__); } foreach ($res as $row) { $title = Title::newFromRow($row); if (!$row->cl_collation) { # This is an old-style row, so the sortkey needs to be # converted. if ($row->cl_sortkey == $title->getText() || $row->cl_sortkey == $title->getPrefixedText()) { $prefix = ''; } else { # Custom sortkey, use it as a prefix $prefix = $row->cl_sortkey; } } else { $prefix = $row->cl_sortkey_prefix; } # cl_type will be wrong for lots of pages if cl_collation is 0, # so let's update it while we're here. if ($title->getNamespace() == NS_CATEGORY) { $type = 'subcat'; } elseif ($title->getNamespace() == NS_FILE) { $type = 'file'; } else { $type = 'page'; } $newSortKey = $collation->getSortKey($title->getCategorySortkey($prefix)); if ($verboseStats) { $this->updateSortKeySizeHistogram($newSortKey); } if (!$dryRun) { $dbw->update('categorylinks', ['cl_sortkey' => $newSortKey, 'cl_sortkey_prefix' => $prefix, 'cl_collation' => $collationName, 'cl_type' => $type, 'cl_timestamp = cl_timestamp'], ['cl_from' => $row->cl_from, 'cl_to' => $row->cl_to], __METHOD__); } if ($row) { $batchConds = [$this->getBatchCondition($row, $dbw)]; } } if (!$dryRun) { $this->commitTransaction($dbw, __METHOD__); } $count += $res->numRows(); $this->output("{$count} done.\n"); if (!$dryRun && ++$batchCount % self::SYNC_INTERVAL == 0) { $this->output("Waiting for slaves ... "); wfWaitForSlaves(); $this->output("done\n"); } } while ($res->numRows() == self::BATCH_SIZE); $this->output("{$count} rows processed\n"); if ($verboseStats) { $this->output("\n"); $this->showSortKeySizeHistogram(); } }
/** * @param User $user * @param string $reason * @param bool $createRedirect * @return Status */ public function move(User $user, $reason, $createRedirect) { global $wgCategoryCollation; Hooks::run('TitleMove', [$this->oldTitle, $this->newTitle, $user]); // If it is a file, move it first. // It is done before all other moving stuff is done because it's hard to revert. $dbw = wfGetDB(DB_MASTER); if ($this->oldTitle->getNamespace() == NS_FILE) { $file = wfLocalFile($this->oldTitle); $file->load(File::READ_LATEST); if ($file->exists()) { $status = $file->move($this->newTitle); if (!$status->isOK()) { return $status; } } // Clear RepoGroup process cache RepoGroup::singleton()->clearCache($this->oldTitle); RepoGroup::singleton()->clearCache($this->newTitle); # clear false negative cache } $dbw->startAtomic(__METHOD__); Hooks::run('TitleMoveStarting', [$this->oldTitle, $this->newTitle, $user]); $pageid = $this->oldTitle->getArticleID(Title::GAID_FOR_UPDATE); $protected = $this->oldTitle->isProtected(); // Do the actual move; if this fails, it will throw an MWException(!) $nullRevision = $this->moveToInternal($user, $this->newTitle, $reason, $createRedirect); // Refresh the sortkey for this row. Be careful to avoid resetting // cl_timestamp, which may disturb time-based lists on some sites. // @todo This block should be killed, it's duplicating code // from LinksUpdate::getCategoryInsertions() and friends. $prefixes = $dbw->select('categorylinks', ['cl_sortkey_prefix', 'cl_to'], ['cl_from' => $pageid], __METHOD__); if ($this->newTitle->getNamespace() == NS_CATEGORY) { $type = 'subcat'; } elseif ($this->newTitle->getNamespace() == NS_FILE) { $type = 'file'; } else { $type = 'page'; } foreach ($prefixes as $prefixRow) { $prefix = $prefixRow->cl_sortkey_prefix; $catTo = $prefixRow->cl_to; $dbw->update('categorylinks', ['cl_sortkey' => Collation::singleton()->getSortKey($this->newTitle->getCategorySortkey($prefix)), 'cl_collation' => $wgCategoryCollation, 'cl_type' => $type, 'cl_timestamp=cl_timestamp'], ['cl_from' => $pageid, 'cl_to' => $catTo], __METHOD__); } $redirid = $this->oldTitle->getArticleID(); if ($protected) { # Protect the redirect title as the title used to be... $res = $dbw->select('page_restrictions', '*', ['pr_page' => $pageid], __METHOD__, 'FOR UPDATE'); $rowsInsert = []; foreach ($res as $row) { $rowsInsert[] = ['pr_page' => $redirid, 'pr_type' => $row->pr_type, 'pr_level' => $row->pr_level, 'pr_cascade' => $row->pr_cascade, 'pr_user' => $row->pr_user, 'pr_expiry' => $row->pr_expiry]; } $dbw->insert('page_restrictions', $rowsInsert, __METHOD__, ['IGNORE']); // Build comment for log $comment = wfMessage('prot_1movedto2', $this->oldTitle->getPrefixedText(), $this->newTitle->getPrefixedText())->inContentLanguage()->text(); if ($reason) { $comment .= wfMessage('colon-separator')->inContentLanguage()->text() . $reason; } // reread inserted pr_ids for log relation $insertedPrIds = $dbw->select('page_restrictions', 'pr_id', ['pr_page' => $redirid], __METHOD__); $logRelationsValues = []; foreach ($insertedPrIds as $prid) { $logRelationsValues[] = $prid->pr_id; } // Update the protection log $logEntry = new ManualLogEntry('protect', 'move_prot'); $logEntry->setTarget($this->newTitle); $logEntry->setComment($comment); $logEntry->setPerformer($user); $logEntry->setParameters(['4::oldtitle' => $this->oldTitle->getPrefixedText()]); $logEntry->setRelations(['pr_id' => $logRelationsValues]); $logId = $logEntry->insert(); $logEntry->publish($logId); } // Update *_from_namespace fields as needed if ($this->oldTitle->getNamespace() != $this->newTitle->getNamespace()) { $dbw->update('pagelinks', ['pl_from_namespace' => $this->newTitle->getNamespace()], ['pl_from' => $pageid], __METHOD__); $dbw->update('templatelinks', ['tl_from_namespace' => $this->newTitle->getNamespace()], ['tl_from' => $pageid], __METHOD__); $dbw->update('imagelinks', ['il_from_namespace' => $this->newTitle->getNamespace()], ['il_from' => $pageid], __METHOD__); } # Update watchlists $oldtitle = $this->oldTitle->getDBkey(); $newtitle = $this->newTitle->getDBkey(); $oldsnamespace = MWNamespace::getSubject($this->oldTitle->getNamespace()); $newsnamespace = MWNamespace::getSubject($this->newTitle->getNamespace()); if ($oldsnamespace != $newsnamespace || $oldtitle != $newtitle) { $store = MediaWikiServices::getInstance()->getWatchedItemStore(); $store->duplicateAllAssociatedEntries($this->oldTitle, $this->newTitle); } Hooks::run('TitleMoveCompleting', [$this->oldTitle, $this->newTitle, $user, $pageid, $redirid, $reason, $nullRevision]); $dbw->endAtomic(__METHOD__); $params = [&$this->oldTitle, &$this->newTitle, &$user, $pageid, $redirid, $reason, $nullRevision]; // Keep each single hook handler atomic DeferredUpdates::addUpdate(new AtomicSectionUpdate($dbw, __METHOD__, function () use($params) { Hooks::run('TitleMoveComplete', $params); })); return Status::newGood(); }