示例#1
0
 /**
  * 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;
 }
示例#2
0
 /**
  * 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;
 }
示例#3
0
 /**
  * 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;
 }
示例#4
0
 /**
  * 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];
 }
示例#5
0
 /**
  * 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;
 }
示例#6
0
 /**
  * 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);
 }