/** * Record a log event for a change being patrolled * * @param int|RecentChange $rc Change identifier or RecentChange object * @param bool $auto Was this patrol event automatic? * @param User $user User performing the action or null to use $wgUser * @param string|string[] $tags Change tags to add to the patrol log entry * ($user should be able to add the specified tags before this is called) * * @return bool */ public static function record($rc, $auto = false, User $user = null, $tags = null) { global $wgLogAutopatrol; // do not log autopatrolled edits if setting disables it if ($auto && !$wgLogAutopatrol) { return false; } if (!$rc instanceof RecentChange) { $rc = RecentChange::newFromId($rc); if (!is_object($rc)) { return false; } } if (!$user) { global $wgUser; $user = $wgUser; } $action = $auto ? 'autopatrol' : 'patrol'; $entry = new ManualLogEntry('patrol', $action); $entry->setTarget($rc->getTitle()); $entry->setParameters(self::buildParams($rc, $auto)); $entry->setPerformer($user); $entry->setTags($tags); $logid = $entry->insert(); if (!$auto) { $entry->publish($logid, 'udp'); } return true; }
/** * 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; }
/** * Process the form * * Change tags can be provided via $data['Tags'], but the calling function * must check if the tags can be added by the user prior to this function. * * @param array $data * @param IContextSource $context * @throws ErrorPageError * @return array|bool Array(message key, parameters) on failure, True on success */ public static function processUnblock(array $data, IContextSource $context) { $performer = $context->getUser(); $target = $data['Target']; $block = Block::newFromTarget($data['Target']); if (!$block instanceof Block) { return [['ipb_cant_unblock', $target]]; } # bug 15810: blocked admins should have limited access here. This # won't allow sysops to remove autoblocks on themselves, but they # should have ipblock-exempt anyway $status = SpecialBlock::checkUnblockSelf($target, $performer); if ($status !== true) { throw new ErrorPageError('badaccess', $status); } # If the specified IP is a single address, and the block is a range block, don't # unblock the whole range. list($target, $type) = SpecialBlock::getTargetAndType($target); if ($block->getType() == Block::TYPE_RANGE && $type == Block::TYPE_IP) { $range = $block->getTarget(); return [['ipb_blocked_as_range', $target, $range]]; } # If the name was hidden and the blocking user cannot hide # names, then don't allow any block removals... if (!$performer->isAllowed('hideuser') && $block->mHideName) { return ['unblock-hideuser']; } # Delete block if (!$block->delete()) { return ['ipb_cant_unblock', htmlspecialchars($block->getTarget())]; } # Unset _deleted fields as needed if ($block->mHideName) { # Something is deeply FUBAR if this is not a User object, but who knows? $id = $block->getTarget() instanceof User ? $block->getTarget()->getId() : User::idFromName($block->getTarget()); RevisionDeleteUser::unsuppressUserName($block->getTarget(), $id); } # Redact the name (IP address) for autoblocks if ($block->getType() == Block::TYPE_AUTO) { $page = Title::makeTitle(NS_USER, '#' . $block->getId()); } else { $page = $block->getTarget() instanceof User ? $block->getTarget()->getUserPage() : Title::makeTitle(NS_USER, $block->getTarget()); } # Make log entry $logEntry = new ManualLogEntry('block', 'unblock'); $logEntry->setTarget($page); $logEntry->setComment($data['Reason']); $logEntry->setPerformer($performer); if (isset($data['Tags'])) { $logEntry->setTags($data['Tags']); } $logId = $logEntry->insert(); $logEntry->publish($logId); return true; }
/** * Restore the given (or all) text and file revisions for the page. * Once restored, the items will be removed from the archive tables. * The deletion log will be updated with an undeletion notice. * * This also sets Status objects, $this->fileStatus and $this->revisionStatus * (depending what operations are attempted). * * @param array $timestamps Pass an empty array to restore all revisions, * otherwise list the ones to undelete. * @param string $comment * @param array $fileVersions * @param bool $unsuppress * @param User $user User performing the action, or null to use $wgUser * @param string|string[] $tags Change tags to add to log entry * ($user should be able to add the specified tags before this is called) * @return array(number of file revisions restored, number of image revisions * restored, log message) on success, false on failure. */ function undelete($timestamps, $comment = '', $fileVersions = [], $unsuppress = false, User $user = null, $tags = null) { // If both the set of text revisions and file revisions are empty, // restore everything. Otherwise, just restore the requested items. $restoreAll = empty($timestamps) && empty($fileVersions); $restoreText = $restoreAll || !empty($timestamps); $restoreFiles = $restoreAll || !empty($fileVersions); if ($restoreFiles && $this->title->getNamespace() == NS_FILE) { $img = wfLocalFile($this->title); $img->load(File::READ_LATEST); $this->fileStatus = $img->restore($fileVersions, $unsuppress); if (!$this->fileStatus->isOK()) { return false; } $filesRestored = $this->fileStatus->successCount; } else { $filesRestored = 0; } if ($restoreText) { $this->revisionStatus = $this->undeleteRevisions($timestamps, $unsuppress, $comment); if (!$this->revisionStatus->isOK()) { return false; } $textRestored = $this->revisionStatus->getValue(); } else { $textRestored = 0; } // Touch the log! if ($textRestored && $filesRestored) { $reason = wfMessage('undeletedrevisions-files')->numParams($textRestored, $filesRestored)->inContentLanguage()->text(); } elseif ($textRestored) { $reason = wfMessage('undeletedrevisions')->numParams($textRestored)->inContentLanguage()->text(); } elseif ($filesRestored) { $reason = wfMessage('undeletedfiles')->numParams($filesRestored)->inContentLanguage()->text(); } else { wfDebug("Undelete: nothing undeleted...\n"); return false; } if (trim($comment) != '') { $reason .= wfMessage('colon-separator')->inContentLanguage()->text() . $comment; } if ($user === null) { global $wgUser; $user = $wgUser; } $logEntry = new ManualLogEntry('delete', 'restore'); $logEntry->setPerformer($user); $logEntry->setTarget($this->title); $logEntry->setComment($reason); $logEntry->setTags($tags); Hooks::run('ArticleUndeleteLogEntry', [$this, &$logEntry, $user]); $logid = $logEntry->insert(); $logEntry->publish($logid); return [$textRestored, $filesRestored, $reason]; }
/** * Really delete the file * * @param Title $title * @param File $file * @param string $oldimage Archive name * @param string $reason Reason of the deletion * @param bool $suppress Whether to mark all deleted versions as restricted * @param User $user User object performing the request * @param array $tags Tags to apply to the deletion action * @throws MWException * @return bool|Status */ public static function doDelete(&$title, &$file, &$oldimage, $reason, $suppress, User $user = null, $tags = []) { if ($user === null) { global $wgUser; $user = $wgUser; } if ($oldimage) { $page = null; $status = $file->deleteOld($oldimage, $reason, $suppress, $user); if ($status->ok) { // Need to do a log item $logComment = wfMessage('deletedrevision', $oldimage)->inContentLanguage()->text(); if (trim($reason) != '') { $logComment .= wfMessage('colon-separator')->inContentLanguage()->text() . $reason; } $logtype = $suppress ? 'suppress' : 'delete'; $logEntry = new ManualLogEntry($logtype, 'delete'); $logEntry->setPerformer($user); $logEntry->setTarget($title); $logEntry->setComment($logComment); $logEntry->setTags($tags); $logid = $logEntry->insert(); $logEntry->publish($logid); $status->value = $logid; } } else { $status = Status::newFatal('cannotdelete', wfEscapeWikiText($title->getPrefixedText())); $page = WikiPage::factory($title); $dbw = wfGetDB(DB_MASTER); $dbw->startAtomic(__METHOD__); // delete the associated article first $error = ''; $deleteStatus = $page->doDeleteArticleReal($reason, $suppress, 0, false, $error, $user, $tags); // doDeleteArticleReal() returns a non-fatal error status if the page // or revision is missing, so check for isOK() rather than isGood() if ($deleteStatus->isOK()) { $status = $file->delete($reason, $suppress, $user); if ($status->isOK()) { $status->value = $deleteStatus->value; // log id $dbw->endAtomic(__METHOD__); } else { // Page deleted but file still there? rollback page delete wfGetLBFactory()->rollbackMasterChanges(__METHOD__); } } else { // Done; nothing changed $dbw->endAtomic(__METHOD__); } } if ($status->isOK()) { Hooks::run('FileDeleteComplete', [&$file, &$oldimage, &$page, &$user, &$reason]); } return $status; }
/** * Update the article's restriction field, and leave a log entry. * This works for protection both existing and non-existing pages. * * @param array $limit Set of restriction keys * @param array $expiry Per restriction type expiration * @param int &$cascade Set to false if cascading protection isn't allowed. * @param string $reason * @param User $user The user updating the restrictions * @param string|string[] $tags Change tags to add to the pages and protection log entries * ($user should be able to add the specified tags before this is called) * @return Status Status object; if action is taken, $status->value is the log_id of the * protection log entry. */ public function doUpdateRestrictions(array $limit, array $expiry, &$cascade, $reason, User $user, $tags = null) { global $wgCascadingRestrictionLevels, $wgContLang; if (wfReadOnly()) { return Status::newFatal('readonlytext', wfReadOnlyReason()); } $this->loadPageData('fromdbmaster'); $restrictionTypes = $this->mTitle->getRestrictionTypes(); $id = $this->getId(); if (!$cascade) { $cascade = false; } // Take this opportunity to purge out expired restrictions Title::purgeExpiredRestrictions(); // @todo FIXME: Same limitations as described in ProtectionForm.php (line 37); // we expect a single selection, but the schema allows otherwise. $isProtected = false; $protect = false; $changed = false; $dbw = wfGetDB(DB_MASTER); foreach ($restrictionTypes as $action) { if (!isset($expiry[$action]) || $expiry[$action] === $dbw->getInfinity()) { $expiry[$action] = 'infinity'; } if (!isset($limit[$action])) { $limit[$action] = ''; } elseif ($limit[$action] != '') { $protect = true; } // Get current restrictions on $action $current = implode('', $this->mTitle->getRestrictions($action)); if ($current != '') { $isProtected = true; } if ($limit[$action] != $current) { $changed = true; } elseif ($limit[$action] != '') { // Only check expiry change if the action is actually being // protected, since expiry does nothing on an not-protected // action. if ($this->mTitle->getRestrictionExpiry($action) != $expiry[$action]) { $changed = true; } } } if (!$changed && $protect && $this->mTitle->areRestrictionsCascading() != $cascade) { $changed = true; } // If nothing has changed, do nothing if (!$changed) { return Status::newGood(); } if (!$protect) { // No protection at all means unprotection $revCommentMsg = 'unprotectedarticle'; $logAction = 'unprotect'; } elseif ($isProtected) { $revCommentMsg = 'modifiedarticleprotection'; $logAction = 'modify'; } else { $revCommentMsg = 'protectedarticle'; $logAction = 'protect'; } // Truncate for whole multibyte characters $reason = $wgContLang->truncate($reason, 255); $logRelationsValues = []; $logRelationsField = null; $logParamsDetails = []; // Null revision (used for change tag insertion) $nullRevision = null; if ($id) { // Protection of existing page if (!Hooks::run('ArticleProtect', [&$this, &$user, $limit, $reason])) { return Status::newGood(); } // Only certain restrictions can cascade... $editrestriction = isset($limit['edit']) ? [$limit['edit']] : $this->mTitle->getRestrictions('edit'); foreach (array_keys($editrestriction, 'sysop') as $key) { $editrestriction[$key] = 'editprotected'; // backwards compatibility } foreach (array_keys($editrestriction, 'autoconfirmed') as $key) { $editrestriction[$key] = 'editsemiprotected'; // backwards compatibility } $cascadingRestrictionLevels = $wgCascadingRestrictionLevels; foreach (array_keys($cascadingRestrictionLevels, 'sysop') as $key) { $cascadingRestrictionLevels[$key] = 'editprotected'; // backwards compatibility } foreach (array_keys($cascadingRestrictionLevels, 'autoconfirmed') as $key) { $cascadingRestrictionLevels[$key] = 'editsemiprotected'; // backwards compatibility } // The schema allows multiple restrictions if (!array_intersect($editrestriction, $cascadingRestrictionLevels)) { $cascade = false; } // insert null revision to identify the page protection change as edit summary $latest = $this->getLatest(); $nullRevision = $this->insertProtectNullRevision($revCommentMsg, $limit, $expiry, $cascade, $reason, $user); if ($nullRevision === null) { return Status::newFatal('no-null-revision', $this->mTitle->getPrefixedText()); } $logRelationsField = 'pr_id'; // Update restrictions table foreach ($limit as $action => $restrictions) { $dbw->delete('page_restrictions', ['pr_page' => $id, 'pr_type' => $action], __METHOD__); if ($restrictions != '') { $cascadeValue = $cascade && $action == 'edit' ? 1 : 0; $dbw->insert('page_restrictions', ['pr_id' => $dbw->nextSequenceValue('page_restrictions_pr_id_seq'), 'pr_page' => $id, 'pr_type' => $action, 'pr_level' => $restrictions, 'pr_cascade' => $cascadeValue, 'pr_expiry' => $dbw->encodeExpiry($expiry[$action])], __METHOD__); $logRelationsValues[] = $dbw->insertId(); $logParamsDetails[] = ['type' => $action, 'level' => $restrictions, 'expiry' => $expiry[$action], 'cascade' => (bool) $cascadeValue]; } } // Clear out legacy restriction fields $dbw->update('page', ['page_restrictions' => ''], ['page_id' => $id], __METHOD__); Hooks::run('NewRevisionFromEditComplete', [$this, $nullRevision, $latest, $user]); Hooks::run('ArticleProtectComplete', [&$this, &$user, $limit, $reason]); } else { // Protection of non-existing page (also known as "title protection") // Cascade protection is meaningless in this case $cascade = false; if ($limit['create'] != '') { $dbw->replace('protected_titles', [['pt_namespace', 'pt_title']], ['pt_namespace' => $this->mTitle->getNamespace(), 'pt_title' => $this->mTitle->getDBkey(), 'pt_create_perm' => $limit['create'], 'pt_timestamp' => $dbw->timestamp(), 'pt_expiry' => $dbw->encodeExpiry($expiry['create']), 'pt_user' => $user->getId(), 'pt_reason' => $reason], __METHOD__); $logParamsDetails[] = ['type' => 'create', 'level' => $limit['create'], 'expiry' => $expiry['create']]; } else { $dbw->delete('protected_titles', ['pt_namespace' => $this->mTitle->getNamespace(), 'pt_title' => $this->mTitle->getDBkey()], __METHOD__); } } $this->mTitle->flushRestrictions(); InfoAction::invalidateCache($this->mTitle); if ($logAction == 'unprotect') { $params = []; } else { $protectDescriptionLog = $this->protectDescriptionLog($limit, $expiry); $params = ['4::description' => $protectDescriptionLog, '5:bool:cascade' => $cascade, 'details' => $logParamsDetails]; } // Update the protection log $logEntry = new ManualLogEntry('protect', $logAction); $logEntry->setTarget($this->mTitle); $logEntry->setComment($reason); $logEntry->setPerformer($user); $logEntry->setParameters($params); if (!is_null($nullRevision)) { $logEntry->setAssociatedRevId($nullRevision->getId()); } $logEntry->setTags($tags); if ($logRelationsField !== null && count($logRelationsValues)) { $logEntry->setRelations([$logRelationsField => $logRelationsValues]); } $logId = $logEntry->insert(); $logEntry->publish($logId); return Status::newGood($logId); }