/** * Back-end article deletion * Deletes the article with database consistency, writes logs, purges caches * * @since 1.19 * * @param string $reason Delete reason for deletion log * @param bool $suppress Suppress all revisions and log the deletion in * the suppression log instead of the deletion log * @param int $u1 Unused * @param bool $u2 Unused * @param array|string &$error Array of errors to append to * @param User $user The deleting user * @param array $tags Tags to apply to the deletion action * @return Status Status object; if successful, $status->value is the log_id of the * deletion log entry. If the page couldn't be deleted because it wasn't * found, $status is a non-fatal 'cannotdelete' error */ public function doDeleteArticleReal($reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null, $tags = []) { global $wgUser, $wgContentHandlerUseDB; wfDebug(__METHOD__ . "\n"); $status = Status::newGood(); if ($this->mTitle->getDBkey() === '') { $status->error('cannotdelete', wfEscapeWikiText($this->getTitle()->getPrefixedText())); return $status; } $user = is_null($user) ? $wgUser : $user; if (!Hooks::run('ArticleDelete', [&$this, &$user, &$reason, &$error, &$status, $suppress])) { if ($status->isOK()) { // Hook aborted but didn't set a fatal status $status->fatal('delete-hook-aborted'); } return $status; } $dbw = wfGetDB(DB_MASTER); $dbw->startAtomic(__METHOD__); $this->loadPageData(self::READ_LATEST); $id = $this->getId(); // T98706: lock the page from various other updates but avoid using // WikiPage::READ_LOCKING as that will carry over the FOR UPDATE to // the revisions queries (which also JOIN on user). Only lock the page // row and CAS check on page_latest to see if the trx snapshot matches. $lockedLatest = $this->lockAndGetLatest(); if ($id == 0 || $this->getLatest() != $lockedLatest) { $dbw->endAtomic(__METHOD__); // Page not there or trx snapshot is stale $status->error('cannotdelete', wfEscapeWikiText($this->getTitle()->getPrefixedText())); return $status; } // Given the lock above, we can be confident in the title and page ID values $namespace = $this->getTitle()->getNamespace(); $dbKey = $this->getTitle()->getDBkey(); // At this point we are now comitted to returning an OK // status unless some DB query error or other exception comes up. // This way callers don't have to call rollback() if $status is bad // unless they actually try to catch exceptions (which is rare). // we need to remember the old content so we can use it to generate all deletion updates. $revision = $this->getRevision(); try { $content = $this->getContent(Revision::RAW); } catch (Exception $ex) { wfLogWarning(__METHOD__ . ': failed to load content during deletion! ' . $ex->getMessage()); $content = null; } $fields = Revision::selectFields(); $bitfield = false; // Bitfields to further suppress the content if ($suppress) { $bitfield = Revision::SUPPRESSED_ALL; $fields = array_diff($fields, ['rev_deleted']); } // For now, shunt the revision data into the archive table. // Text is *not* removed from the text table; bulk storage // is left intact to avoid breaking block-compression or // immutable storage schemes. // In the future, we may keep revisions and mark them with // the rev_deleted field, which is reserved for this purpose. // Get all of the page revisions $res = $dbw->select('revision', $fields, ['rev_page' => $id], __METHOD__, 'FOR UPDATE'); // Build their equivalent archive rows $rowsInsert = []; foreach ($res as $row) { $rowInsert = ['ar_namespace' => $namespace, 'ar_title' => $dbKey, 'ar_comment' => $row->rev_comment, 'ar_user' => $row->rev_user, 'ar_user_text' => $row->rev_user_text, 'ar_timestamp' => $row->rev_timestamp, 'ar_minor_edit' => $row->rev_minor_edit, 'ar_rev_id' => $row->rev_id, 'ar_parent_id' => $row->rev_parent_id, 'ar_text_id' => $row->rev_text_id, 'ar_text' => '', 'ar_flags' => '', 'ar_len' => $row->rev_len, 'ar_page_id' => $id, 'ar_deleted' => $suppress ? $bitfield : $row->rev_deleted, 'ar_sha1' => $row->rev_sha1]; if ($wgContentHandlerUseDB) { $rowInsert['ar_content_model'] = $row->rev_content_model; $rowInsert['ar_content_format'] = $row->rev_content_format; } $rowsInsert[] = $rowInsert; } // Copy them into the archive table $dbw->insert('archive', $rowsInsert, __METHOD__); // Save this so we can pass it to the ArticleDeleteComplete hook. $archivedRevisionCount = $dbw->affectedRows(); // Clone the title and wikiPage, so we have the information we need when // we log and run the ArticleDeleteComplete hook. $logTitle = clone $this->mTitle; $wikiPageBeforeDelete = clone $this; // Now that it's safely backed up, delete it $dbw->delete('page', ['page_id' => $id], __METHOD__); $dbw->delete('revision', ['rev_page' => $id], __METHOD__); // Log the deletion, if the page was suppressed, put it in the suppression log instead $logtype = $suppress ? 'suppress' : 'delete'; $logEntry = new ManualLogEntry($logtype, 'delete'); $logEntry->setPerformer($user); $logEntry->setTarget($logTitle); $logEntry->setComment($reason); $logEntry->setTags($tags); $logid = $logEntry->insert(); $dbw->onTransactionPreCommitOrIdle(function () use($dbw, $logEntry, $logid) { // Bug 56776: avoid deadlocks (especially from FileDeleteForm) $logEntry->publish($logid); }, __METHOD__); $dbw->endAtomic(__METHOD__); $this->doDeleteUpdates($id, $content, $revision); Hooks::run('ArticleDeleteComplete', [&$wikiPageBeforeDelete, &$user, $reason, $id, $content, $logEntry, $archivedRevisionCount]); $status->value = $logid; // Show log excerpt on 404 pages rather than just a link $cache = ObjectCache::getMainStashInstance(); $key = wfMemcKey('page-recent-delete', md5($logTitle->getPrefixedText())); $cache->set($key, 1, $cache::TTL_DAY); return $status; }
/** * Show the error text for a missing article. For articles in the MediaWiki * namespace, show the default message text. To be called from Article::view(). */ public function showMissingArticle() { global $wgSend404Code; $outputPage = $this->getContext()->getOutput(); // Whether the page is a root user page of an existing user (but not a subpage) $validUserPage = false; $title = $this->getTitle(); # Show info in user (talk) namespace. Does the user exist? Is he blocked? if ($title->getNamespace() == NS_USER || $title->getNamespace() == NS_USER_TALK) { $parts = explode('/', $title->getText()); $rootPart = $parts[0]; $user = User::newFromName($rootPart, false); $ip = User::isIP($rootPart); $block = Block::newFromTarget($user, $user); if (!($user && $user->isLoggedIn()) && !$ip) { # User does not exist $outputPage->wrapWikiMsg("<div class=\"mw-userpage-userdoesnotexist error\">\n\$1\n</div>", array('userpage-userdoesnotexist-view', wfEscapeWikiText($rootPart))); } elseif (!is_null($block) && $block->getType() != Block::TYPE_AUTO) { # Show log extract if the user is currently blocked LogEventsList::showLogExtract($outputPage, 'block', MWNamespace::getCanonicalName(NS_USER) . ':' . $block->getTarget(), '', array('lim' => 1, 'showIfEmpty' => false, 'msgKey' => array('blocked-notice-logextract', $user->getName()))); $validUserPage = !$title->isSubpage(); } else { $validUserPage = !$title->isSubpage(); } } Hooks::run('ShowMissingArticle', array($this)); # Show delete and move logs if there were any such events. # The logging query can DOS the site when bots/crawlers cause 404 floods, # so be careful showing this. 404 pages must be cheap as they are hard to cache. $cache = ObjectCache::getMainStashInstance(); $key = wfMemcKey('page-recent-delete', md5($title->getPrefixedText())); $loggedIn = $this->getContext()->getUser()->isLoggedIn(); if ($loggedIn || $cache->get($key)) { $logTypes = array('delete', 'move'); $conds = array("log_action != 'revision'"); // Give extensions a chance to hide their (unrelated) log entries Hooks::run('Article::MissingArticleConditions', array(&$conds, $logTypes)); LogEventsList::showLogExtract($outputPage, $logTypes, $title, '', array('lim' => 10, 'conds' => $conds, 'showIfEmpty' => false, 'msgKey' => array($loggedIn ? 'moveddeleted-notice' : 'moveddeleted-notice-recent'))); } if (!$this->mPage->hasViewableContent() && $wgSend404Code && !$validUserPage) { // If there's no backing content, send a 404 Not Found // for better machine handling of broken links. $this->getContext()->getRequest()->response()->statusHeader(404); } // Also apply the robot policy for nonexisting pages (even if a 404 was used for sanity) $policy = $this->getRobotPolicy('view'); $outputPage->setIndexPolicy($policy['index']); $outputPage->setFollowPolicy($policy['follow']); $hookResult = Hooks::run('BeforeDisplayNoArticleText', array($this)); if (!$hookResult) { return; } # Show error message $oldid = $this->getOldID(); if (!$oldid && $title->getNamespace() === NS_MEDIAWIKI && $title->hasSourceText()) { $outputPage->addParserOutput($this->getContentObject()->getParserOutput($title)); } else { if ($oldid) { $text = wfMessage('missing-revision', $oldid)->plain(); } elseif ($title->quickUserCan('create', $this->getContext()->getUser()) && $title->quickUserCan('edit', $this->getContext()->getUser())) { $message = $this->getContext()->getUser()->isLoggedIn() ? 'noarticletext' : 'noarticletextanon'; $text = wfMessage($message)->plain(); } else { $text = wfMessage('noarticletext-nopermission')->plain(); } $dir = $this->getContext()->getLanguage()->getDir(); $lang = $this->getContext()->getLanguage()->getCode(); $outputPage->addWikiText(Xml::openElement('div', array('class' => "noarticletext mw-content-{$dir}", 'dir' => $dir, 'lang' => $lang)) . "\n{$text}\n</div>"); } }
/** * Back-end article deletion * Deletes the article with database consistency, writes logs, purges caches * * @since 1.19 * * @param string $reason Delete reason for deletion log * @param bool $suppress Suppress all revisions and log the deletion in * the suppression log instead of the deletion log * @param int $id Article ID * @param bool $commit Defaults to true, triggers transaction end * @param array &$error Array of errors to append to * @param User $user The deleting user * @return Status Status object; if successful, $status->value is the log_id of the * deletion log entry. If the page couldn't be deleted because it wasn't * found, $status is a non-fatal 'cannotdelete' error */ public function doDeleteArticleReal($reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null) { global $wgUser, $wgContentHandlerUseDB; wfDebug(__METHOD__ . "\n"); $status = Status::newGood(); if ($this->mTitle->getDBkey() === '') { $status->error('cannotdelete', wfEscapeWikiText($this->getTitle()->getPrefixedText())); return $status; } $user = is_null($user) ? $wgUser : $user; if (!Hooks::run('ArticleDelete', array(&$this, &$user, &$reason, &$error, &$status, $suppress))) { if ($status->isOK()) { // Hook aborted but didn't set a fatal status $status->fatal('delete-hook-aborted'); } return $status; } $dbw = wfGetDB(DB_MASTER); $dbw->begin(__METHOD__); if ($id == 0) { $this->loadPageData(self::READ_LATEST); $id = $this->getID(); // T98706: lock the page from various other updates but avoid using // WikiPage::READ_LOCKING as that will carry over the FOR UPDATE to // the revisions queries (which also JOIN on user). Only lock the page // row and CAS check on page_latest to see if the trx snapshot matches. $lockedLatest = $this->lock(); if ($id == 0 || $this->getLatest() != $lockedLatest) { // Page not there or trx snapshot is stale $dbw->rollback(__METHOD__); $status->error('cannotdelete', wfEscapeWikiText($this->getTitle()->getPrefixedText())); return $status; } } // we need to remember the old content so we can use it to generate all deletion updates. $content = $this->getContent(Revision::RAW); // Bitfields to further suppress the content if ($suppress) { $bitfield = 0; // This should be 15... $bitfield |= Revision::DELETED_TEXT; $bitfield |= Revision::DELETED_COMMENT; $bitfield |= Revision::DELETED_USER; $bitfield |= Revision::DELETED_RESTRICTED; } else { $bitfield = 'rev_deleted'; } /** * For now, shunt the revision data into the archive table. * Text is *not* removed from the text table; bulk storage * is left intact to avoid breaking block-compression or * immutable storage schemes. * * For backwards compatibility, note that some older archive * table entries will have ar_text and ar_flags fields still. * * In the future, we may keep revisions and mark them with * the rev_deleted field, which is reserved for this purpose. */ $row = array('ar_namespace' => 'page_namespace', 'ar_title' => 'page_title', 'ar_comment' => 'rev_comment', 'ar_user' => 'rev_user', 'ar_user_text' => 'rev_user_text', 'ar_timestamp' => 'rev_timestamp', 'ar_minor_edit' => 'rev_minor_edit', 'ar_rev_id' => 'rev_id', 'ar_parent_id' => 'rev_parent_id', 'ar_text_id' => 'rev_text_id', 'ar_text' => '\'\'', 'ar_flags' => '\'\'', 'ar_len' => 'rev_len', 'ar_page_id' => 'page_id', 'ar_deleted' => $bitfield, 'ar_sha1' => 'rev_sha1'); if ($wgContentHandlerUseDB) { $row['ar_content_model'] = 'rev_content_model'; $row['ar_content_format'] = 'rev_content_format'; } $dbw->insertSelect('archive', array('page', 'revision'), $row, array('page_id' => $id, 'page_id = rev_page'), __METHOD__); // Now that it's safely backed up, delete it $dbw->delete('page', array('page_id' => $id), __METHOD__); $ok = $dbw->affectedRows() > 0; // $id could be laggy if (!$ok) { $dbw->rollback(__METHOD__); $status->error('cannotdelete', wfEscapeWikiText($this->getTitle()->getPrefixedText())); return $status; } if (!$dbw->cascadingDeletes()) { $dbw->delete('revision', array('rev_page' => $id), __METHOD__); } // Clone the title, so we have the information we need when we log $logTitle = clone $this->mTitle; // Log the deletion, if the page was suppressed, put it in the suppression log instead $logtype = $suppress ? 'suppress' : 'delete'; $logEntry = new ManualLogEntry($logtype, 'delete'); $logEntry->setPerformer($user); $logEntry->setTarget($logTitle); $logEntry->setComment($reason); $logid = $logEntry->insert(); $dbw->onTransactionPreCommitOrIdle(function () use($dbw, $logEntry, $logid) { // Bug 56776: avoid deadlocks (especially from FileDeleteForm) $logEntry->publish($logid); }); if ($commit) { $dbw->commit(__METHOD__); } // Show log excerpt on 404 pages rather than just a link $key = wfMemcKey('page-recent-delete', md5($logTitle->getPrefixedText())); ObjectCache::getMainStashInstance()->set($key, 1, 86400); $this->doDeleteUpdates($id, $content); Hooks::run('ArticleDeleteComplete', array(&$this, &$user, $reason, $id, $content, $logEntry)); $status->value = $logid; return $status; }
/** * @return ChronologyProtector */ protected function newChronologyProtector() { $request = RequestContext::getMain()->getRequest(); $chronProt = new ChronologyProtector(ObjectCache::getMainStashInstance(), array('ip' => $request->getIP(), 'agent' => $request->getHeader('User-Agent'))); if (PHP_SAPI === 'cli') { $chronProt->setEnabled(false); } elseif ($request->getHeader('ChronologyProtection') === 'false') { // Request opted out of using position wait logic. This is useful for requests // done by the job queue or background ETL that do not have a meaningful session. $chronProt->setWaitEnabled(false); } return $chronProt; }
/** * Reduce pending delta counters after updates have been applied * @param array $pd Result of getPendingDeltas(), used for DB update */ protected function removePendingDeltas(array $pd) { $cache = ObjectCache::getMainStashInstance(); foreach ($pd as $type => $deltas) { foreach ($deltas as $sign => $magnitude) { // Lower the pending counter now that we applied these changes $cache->decr($this->getTypeCacheKey($type, $sign), $magnitude); } } }
/** * Set the current status of a chunked upload (used for polling) * * The value will be set in cache for 1 day * * @param User $user * @param string $statusKey * @param array|bool $value * @return void */ public static function setSessionStatus(User $user, $statusKey, $value) { $key = wfMemcKey('uploadstatus', $user->getId() ?: md5($user->getName()), $statusKey); $cache = ObjectCache::getMainStashInstance(); if ($value === false) { $cache->delete($key); } else { $cache->set($key, $value, $cache::TTL_DAY); } }
function clear($index) { ObjectCache::getMainStashInstance()->delete(wfMemcKey('captcha', $index)); }