- if ( $this->mTitle->userIsWatching() ) {
- $dbw->begin();
- $this->doUnwatch();
- $dbw->commit();
- }
- }
-
- $extraQuery = ''; // Give extensions a chance to modify URL query on update
- wfRunHooks( 'ArticleUpdateBeforeRedirect', array( $this, &$sectionanchor, &$extraQuery ) );
-
- $this->doRedirect( $this->isRedirect( $text ), $sectionanchor, $extraQuery );
- return true;
- }
-
- /**
- * Check flags and add EDIT_NEW or EDIT_UPDATE to them as needed.
- * @param $flags Int
- * @return Int updated $flags
- */
- function checkFlags( $flags ) {
- if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) {
- if ( $this->mTitle->getArticleID() ) {
- $flags |= EDIT_UPDATE;
- } else {
- $flags |= EDIT_NEW;
- }
- }
-
- return $flags;
- }
-
- /**
- * Article::doEdit()
- *
- * Change an existing article or create a new article. Updates RC and all necessary caches,
- * optionally via the deferred update array.
- *
- * $wgUser must be set before calling this function.
- *
- * @param $text String: new text
- * @param $summary String: edit summary
- * @param $flags Integer bitfield:
- * EDIT_NEW
- * Article is known or assumed to be non-existent, create a new one
- * EDIT_UPDATE
- * Article is known or assumed to be pre-existing, update it
- * EDIT_MINOR
- * Mark this edit minor, if the user is allowed to do so
- * EDIT_SUPPRESS_RC
- * Do not log the change in recentchanges
- * EDIT_FORCE_BOT
- * Mark the edit a "bot" edit regardless of user rights
- * EDIT_DEFER_UPDATES
- * Defer some of the updates until the end of index.php
- * EDIT_AUTOSUMMARY
- * Fill in blank summaries with generated text where possible
- *
- * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the article will be detected.
- * If EDIT_UPDATE is specified and the article doesn't exist, the function will an
- * edit-gone-missing error. If EDIT_NEW is specified and the article does exist, an
- * edit-already-exists error will be returned. These two conditions are also possible with
- * auto-detection due to MediaWiki's performance-optimised locking strategy.
- *
- * @param $baseRevId the revision ID this edit was based off, if any
- * @param $user User (optional), $wgUser will be used if not passed
- *
- * @return Status object. Possible errors:
- * edit-hook-aborted: The ArticleSave hook aborted the edit but didn't set the fatal flag of $status
- * edit-gone-missing: In update mode, but the article didn't exist
- * edit-conflict: In update mode, the article changed unexpectedly
- * edit-no-change: Warning that the text was the same as before
- * edit-already-exists: In creation mode, but the article already exists
- *
- * Extensions may define additional errors.
- *
- * $return->value will contain an associative array with members as follows:
- * new: Boolean indicating if the function attempted to create a new article
- * revision: The revision object for the inserted revision, or null
- *
- * Compatibility note: this function previously returned a boolean value indicating success/failure
- */
- public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) {
- global $wgUser, $wgDBtransactions, $wgUseAutomaticEditSummaries;
-
- # Low-level sanity check
- if ( $this->mTitle->getText() === '' ) {
- throw new MWException( 'Something is trying to edit an article with an empty title' );
- }
-
- wfProfileIn( __METHOD__ );
-
- $user = is_null( $user ) ? $wgUser : $user;
- $status = Status::newGood( array() );
-
- # Load $this->mTitle->getArticleID() and $this->mLatest if it's not already
- $this->loadPageData();
-
- $flags = $this->checkFlags( $flags );
-
- if ( !wfRunHooks( 'ArticleSave', array( &$this, &$user, &$text, &$summary,
- $flags & EDIT_MINOR, null, null, &$flags, &$status ) ) )
- {
- wfDebug( __METHOD__ . ": ArticleSave hook aborted save!\n" );
-
- if ( $status->isOK() ) {
- $status->fatal( 'edit-hook-aborted' );
- }
-
- wfProfileOut( __METHOD__ );
- return $status;
- }
-
- # Silently ignore EDIT_MINOR if not allowed
- $isminor = ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' );
- $bot = $flags & EDIT_FORCE_BOT;
-
- $oldtext = $this->getRawText(); // current revision
- $oldsize = strlen( $oldtext );
-
- # Provide autosummaries if one is not provided and autosummaries are enabled.
- if ( $wgUseAutomaticEditSummaries && $flags & EDIT_AUTOSUMMARY && $summary == '' ) {
- $summary = $this->getAutosummary( $oldtext, $text, $flags );
- }
-
- $editInfo = $this->prepareTextForEdit( $text, null, $user );
- $text = $editInfo->pst;
- $newsize = strlen( $text );
-
- $dbw = wfGetDB( DB_MASTER );
- $now = wfTimestampNow();
- $this->mTimestamp = $now;
-
- if ( $flags & EDIT_UPDATE ) {
- # Update article, but only if changed.
- $status->value['new'] = false;
-
- # Make sure the revision is either completely inserted or not inserted at all
- if ( !$wgDBtransactions ) {
- $userAbort = ignore_user_abort( true );
- }
-
- $changed = ( strcmp( $text, $oldtext ) != 0 );
-
- if ( $changed ) {
- $this->mGoodAdjustment = (int)$this->isCountable( $text )
- - (int)$this->isCountable( $oldtext );
- $this->mTotalAdjustment = 0;
-
- if ( !$this->mLatest ) {
- # Article gone missing
- wfDebug( __METHOD__ . ": EDIT_UPDATE specified but article doesn't exist\n" );
- $status->fatal( 'edit-gone-missing' );
-
- wfProfileOut( __METHOD__ );
- return $status;
- }
-
- $revision = new Revision( array(
- 'page' => $this->getId(),
- 'comment' => $summary,
- 'minor_edit' => $isminor,
- 'text' => $text,
- 'parent_id' => $this->mLatest,
- 'user' => $user->getId(),
- 'user_text' => $user->getName(),
- 'timestamp' => $now
- ) );
-
- $dbw->begin();
- $revisionId = $revision->insertOn( $dbw );
-
- # Update page
- #
- # Note that we use $this->mLatest instead of fetching a value from the master DB
- # during the course of this function. This makes sure that EditPage can detect
- # edit conflicts reliably, either by $ok here, or by $article->getTimestamp()
- # before this function is called. A previous function used a separate query, this
- # creates a window where concurrent edits can cause an ignored edit conflict.
- $ok = $this->updateRevisionOn( $dbw, $revision, $this->mLatest );
-
- if ( !$ok ) {
- /* Belated edit conflict! Run away!! */
- $status->fatal( 'edit-conflict' );
-
- # Delete the invalid revision if the DB is not transactional
- if ( !$wgDBtransactions ) {
- $dbw->delete( 'revision', array( 'rev_id' => $revisionId ), __METHOD__ );
- }
-
- $revisionId = 0;
- $dbw->rollback();
- } else {
- global $wgUseRCPatrol;
- wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, $baseRevId, $user ) );
- # Update recentchanges
- if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
- # Mark as patrolled if the user can do so
- $patrolled = $wgUseRCPatrol && !count(
- $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
- # Add RC row to the DB
- $rc = RecentChange::notifyEdit( $now, $this->mTitle, $isminor, $user, $summary,
- $this->mLatest, $this->getTimestamp(), $bot, '', $oldsize, $newsize,
- $revisionId, $patrolled
- );
-
- # Log auto-patrolled edits
- if ( $patrolled ) {
- PatrolLog::record( $rc, true );
- }
- }
- $user->incEditCount();
- $dbw->commit();
- }
- } else {
- $status->warning( 'edit-no-change' );
- $revision = null;
- // Keep the same revision ID, but do some updates on it
- $revisionId = $this->getLatest();
- // Update page_touched, this is usually implicit in the page update
- // Other cache updates are done in onArticleEdit()
- $this->mTitle->invalidateCache();
- }
-
- if ( !$wgDBtransactions ) {
- ignore_user_abort( $userAbort );
- }
-
- // Now that ignore_user_abort is restored, we can respond to fatal errors
- if ( !$status->isOK() ) {
- wfProfileOut( __METHOD__ );
- return $status;
- }
-
- # Invalidate cache of this article and all pages using this article
- # as a template. Partly deferred.
- Article::onArticleEdit( $this->mTitle );
- # Update links tables, site stats, etc.
- $this->editUpdates( $text, $summary, $isminor, $now, $revisionId, $changed, $user );
- } else {
- # Create new article
- $status->value['new'] = true;
-
- # Set statistics members
- # We work out if it's countable after PST to avoid counter drift
- # when articles are created with {{subst:}}
- $this->mGoodAdjustment = (int)$this->isCountable( $text );
- $this->mTotalAdjustment = 1;
-
- $dbw->begin();
-
- # Add the page record; stake our claim on this title!
- # This will return false if the article already exists
- $newid = $this->insertOn( $dbw );
-
- if ( $newid === false ) {
- $dbw->rollback();
- $status->fatal( 'edit-already-exists' );
-
- wfProfileOut( __METHOD__ );
- return $status;
- }
-
- # Save the revision text...
- $revision = new Revision( array(
- 'page' => $newid,
- 'comment' => $summary,
- 'minor_edit' => $isminor,
- 'text' => $text,
- 'user' => $user->getId(),
- 'user_text' => $user->getName(),
- 'timestamp' => $now
- ) );
- $revisionId = $revision->insertOn( $dbw );
-
- $this->mTitle->resetArticleID( $newid );
- # Update the LinkCache. Resetting the Title ArticleID means it will rely on having that already cached (FIXME?)
- LinkCache::singleton()->addGoodLinkObj( $newid, $this->mTitle, strlen( $text ), (bool)Title::newFromRedirect( $text ), $revisionId );
-
- # Update the page record with revision data
- $this->updateRevisionOn( $dbw, $revision, 0 );
-
- wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, false, $user ) );
-
- # Update recentchanges
- if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
- global $wgUseRCPatrol, $wgUseNPPatrol;
-
- # Mark as patrolled if the user can do so
- $patrolled = ( $wgUseRCPatrol || $wgUseNPPatrol ) && !count(
- $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
- # Add RC row to the DB
- $rc = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $user, $summary, $bot,
- '', strlen( $text ), $revisionId, $patrolled );
-
- # Log auto-patrolled edits
- if ( $patrolled ) {
- PatrolLog::record( $rc, true );
- }
- }
- $user->incEditCount();
- $dbw->commit();
-
- # Update links, etc.
- $this->editUpdates( $text, $summary, $isminor, $now, $revisionId, true, $user );
-
- # Clear caches
- Article::onArticleCreate( $this->mTitle );
-
- wfRunHooks( 'ArticleInsertComplete', array( &$this, &$user, $text, $summary,
- $flags & EDIT_MINOR, null, null, &$flags, $revision ) );
- }
-
- # Do updates right now unless deferral was requested
- if ( !( $flags & EDIT_DEFER_UPDATES ) ) {
- wfDoUpdates();
- }
-
- // Return the new revision (or null) to the caller
- $status->value['revision'] = $revision;
-
- wfRunHooks( 'ArticleSaveComplete', array( &$this, &$user, $text, $summary,
- $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ) );
-
- wfProfileOut( __METHOD__ );
- return $status;
- }
-
- /**
- * Output a redirect back to the article.
- * This is typically used after an edit.
- *
- * @param $noRedir Boolean: add redirect=no
- * @param $sectionAnchor String: section to redirect to, including "#"
- * @param $extraQuery String: extra query params
- */
- public function doRedirect( $noRedir = false, $sectionAnchor = '', $extraQuery = '' ) {
- global $wgOut;
-
- if ( $noRedir ) {
- $query = 'redirect=no';
- if ( $extraQuery )
- $query .= "&$extraQuery";
- } else {
- $query = $extraQuery;
- }
-
- $wgOut->redirect( $this->mTitle->getFullURL( $query ) . $sectionAnchor );
- }
-
- /**
- * Mark this particular edit/page as patrolled
- */
- public function markpatrolled() {
- global $wgOut, $wgUser, $wgRequest;
-
- $wgOut->setRobotPolicy( 'noindex,nofollow' );
-
- # If we haven't been given an rc_id value, we can't do anything
- $rcid = (int) $wgRequest->getVal( 'rcid' );
-
- if ( !$wgUser->matchEditToken( $wgRequest->getVal( 'token' ), $rcid ) ) {
- $wgOut->showErrorPage( 'sessionfailure-title', 'sessionfailure' );
- return;
- }
-
- $rc = RecentChange::newFromId( $rcid );
-
- if ( is_null( $rc ) ) {
- $wgOut->showErrorPage( 'markedaspatrollederror', 'markedaspatrollederrortext' );
- return;
- }
-
- # It would be nice to see where the user had actually come from, but for now just guess
- $returnto = $rc->getAttribute( 'rc_type' ) == RC_NEW ? 'Newpages' : 'Recentchanges';
- $return = SpecialPage::getTitleFor( $returnto );
-
- $errors = $rc->doMarkPatrolled();
-
- if ( in_array( array( 'rcpatroldisabled' ), $errors ) ) {
- $wgOut->showErrorPage( 'rcpatroldisabled', 'rcpatroldisabledtext' );
-
- return;
- }
-
- if ( in_array( array( 'hookaborted' ), $errors ) ) {
- // The hook itself has handled any output
- return;
- }
-
- if ( in_array( array( 'markedaspatrollederror-noautopatrol' ), $errors ) ) {
- $wgOut->setPageTitle( wfMsg( 'markedaspatrollederror' ) );
- $wgOut->addWikiMsg( 'markedaspatrollederror-noautopatrol' );
- $wgOut->returnToMain( false, $return );
-
- return;
- }
-
- if ( !empty( $errors ) ) {
- $wgOut->showPermissionsErrorPage( $errors );
-
- return;
- }
-
- # Inform the user
- $wgOut->setPageTitle( wfMsg( 'markedaspatrolled' ) );
- $wgOut->addWikiMsg( 'markedaspatrolledtext', $rc->getTitle()->getPrefixedText() );
- $wgOut->returnToMain( false, $return );
- }
-
- /**
- * User-interface handler for the "watch" action
- */
- public function watch() {
- global $wgUser, $wgOut;
-
- if ( $wgUser->isAnon() ) {
- $wgOut->showErrorPage( 'watchnologin', 'watchnologintext' );
- return;
- }
-
- if ( wfReadOnly() ) {
- $wgOut->readOnlyPage();
- return;
- }
-
- if ( $this->doWatch() ) {
- $wgOut->setPagetitle( wfMsg( 'addedwatch' ) );
- $wgOut->setRobotPolicy( 'noindex,nofollow' );
- $wgOut->addWikiMsg( 'addedwatchtext', $this->mTitle->getPrefixedText() );
- }
-
- $wgOut->returnToMain( true, $this->mTitle->getPrefixedText() );
- }
-
- /**
- * Add this page to $wgUser's watchlist
- * @return bool true on successful watch operation
- */
- public function doWatch() {
- global $wgUser;
-
- if ( $wgUser->isAnon() ) {
- return false;
- }
-
- if ( wfRunHooks( 'WatchArticle', array( &$wgUser, &$this ) ) ) {
- $wgUser->addWatch( $this->mTitle );
- return wfRunHooks( 'WatchArticleComplete', array( &$wgUser, &$this ) );
- }
-
- return false;
- }
-
- /**
- * User interface handler for the "unwatch" action.
- */
- public function unwatch() {
- global $wgUser, $wgOut;
-
- if ( $wgUser->isAnon() ) {
- $wgOut->showErrorPage( 'watchnologin', 'watchnologintext' );
- return;
- }
-
- if ( wfReadOnly() ) {
- $wgOut->readOnlyPage();
- return;
- }
-
- if ( $this->doUnwatch() ) {
- $wgOut->setPagetitle( wfMsg( 'removedwatch' ) );
- $wgOut->setRobotPolicy( 'noindex,nofollow' );
- $wgOut->addWikiMsg( 'removedwatchtext', $this->mTitle->getPrefixedText() );
- }
-
- $wgOut->returnToMain( true, $this->mTitle->getPrefixedText() );
- }
-
- /**
- * Stop watching a page
- * @return bool true on successful unwatch
- */
- public function doUnwatch() {
- global $wgUser;
-
- if ( $wgUser->isAnon() ) {
- return false;
- }
-
- if ( wfRunHooks( 'UnwatchArticle', array( &$wgUser, &$this ) ) ) {
- $wgUser->removeWatch( $this->mTitle );
- return wfRunHooks( 'UnwatchArticleComplete', array( &$wgUser, &$this ) );
- }
-
- return false;
- }
-
- /**
- * action=protect handler
- */
- public function protect() {
- $form = new ProtectionForm( $this );
- $form->execute();
- }
-
- /**
- * action=unprotect handler (alias)
- */
- public function unprotect() {
- $this->protect();
- }
-
- /**
- * Update the article's restriction field, and leave a log entry.
- *
- * @param $limit Array: set of restriction keys
- * @param $reason String
- * @param &$cascade Integer. Set to false if cascading protection isn't allowed.
- * @param $expiry Array: per restriction type expiration
- * @return bool true on success
- */
- public function updateRestrictions( $limit = array(), $reason = '', &$cascade = 0, $expiry = array() ) {
- global $wgUser, $wgContLang;
-
- $restrictionTypes = $this->mTitle->getRestrictionTypes();
-
- $id = $this->mTitle->getArticleID();
-
- if ( $id <= 0 ) {
- wfDebug( "updateRestrictions failed: article id $id <= 0\n" );
- return false;
- }
-
- if ( wfReadOnly() ) {
- wfDebug( "updateRestrictions failed: read-only\n" );
- return false;
- }
-
- if ( !$this->mTitle->userCan( 'protect' ) ) {
- wfDebug( "updateRestrictions failed: insufficient permissions\n" );
- return false;
- }
-
- if ( !$cascade ) {
- $cascade = false;
- }
-
- // Take this opportunity to purge out expired restrictions
- Title::purgeExpiredRestrictions();
-
- # FIXME: Same limitations as described in ProtectionForm.php (line 37);
- # we expect a single selection, but the schema allows otherwise.
- $current = array();
- $updated = Article::flattenRestrictions( $limit );
- $changed = false;
-
- foreach ( $restrictionTypes as $action ) {
- if ( isset( $expiry[$action] ) ) {
- # Get current restrictions on $action
- $aLimits = $this->mTitle->getRestrictions( $action );
- $current[$action] = implode( '', $aLimits );
- # Are any actual restrictions being dealt with here?
- $aRChanged = count( $aLimits ) || !empty( $limit[$action] );
-
- # If something changed, we need to log it. Checking $aRChanged
- # assures that "unprotecting" a page that is not protected does
- # not log just because the expiry was "changed".
- if ( $aRChanged && $this->mTitle->mRestrictionsExpiry[$action] != $expiry[$action] ) {
- $changed = true;
- }
- }
- }
-
- $current = Article::flattenRestrictions( $current );
-
- $changed = ( $changed || $current != $updated );
- $changed = $changed || ( $updated && $this->mTitle->areRestrictionsCascading() != $cascade );
- $protect = ( $updated != '' );
-
- # If nothing's changed, do nothing
- if ( $changed ) {
- if ( wfRunHooks( 'ArticleProtect', array( &$this, &$wgUser, $limit, $reason ) ) ) {
- $dbw = wfGetDB( DB_MASTER );
-
- # Prepare a null revision to be added to the history
- $modified = $current != '' && $protect;
-
- if ( $protect ) {
- $comment_type = $modified ? 'modifiedarticleprotection' : 'protectedarticle';
- } else {
- $comment_type = 'unprotectedarticle';
- }
-
- $comment = $wgContLang->ucfirst( wfMsgForContent( $comment_type, $this->mTitle->getPrefixedText() ) );
-
- # Only restrictions with the 'protect' right can cascade...
- # Otherwise, people who cannot normally protect can "protect" pages via transclusion
- $editrestriction = isset( $limit['edit'] ) ? array( $limit['edit'] ) : $this->mTitle->getRestrictions( 'edit' );
-
- # The schema allows multiple restrictions
- if ( !in_array( 'protect', $editrestriction ) && !in_array( 'sysop', $editrestriction ) ) {
- $cascade = false;
- }
-
- $cascade_description = '';
-
- if ( $cascade ) {
- $cascade_description = ' [' . wfMsgForContent( 'protect-summary-cascade' ) . ']';
- }
-
- if ( $reason ) {
- $comment .= ": $reason";
- }
-
- $editComment = $comment;
- $encodedExpiry = array();
- $protect_description = '';
- foreach ( $limit as $action => $restrictions ) {
- if ( !isset( $expiry[$action] ) )
- $expiry[$action] = Block::infinity();
-
- $encodedExpiry[$action] = Block::encodeExpiry( $expiry[$action], $dbw );
- if ( $restrictions != '' ) {
- $protect_description .= "[$action=$restrictions] (";
- if ( $encodedExpiry[$action] != 'infinity' ) {
- $protect_description .= wfMsgForContent( 'protect-expiring',
- $wgContLang->timeanddate( $expiry[$action], false, false ) ,
- $wgContLang->date( $expiry[$action], false, false ) ,
- $wgContLang->time( $expiry[$action], false, false ) );
- } else {
- $protect_description .= wfMsgForContent( 'protect-expiry-indefinite' );
- }
-
- $protect_description .= ') ';
- }
- }
- $protect_description = trim( $protect_description );
-
- if ( $protect_description && $protect ) {
- $editComment .= " ($protect_description)";
- }
-
- if ( $cascade ) {
- $editComment .= "$cascade_description";
- }
-
- # Update restrictions table
- foreach ( $limit as $action => $restrictions ) {
- if ( $restrictions != '' ) {
- $dbw->replace( 'page_restrictions', array( array( 'pr_page', 'pr_type' ) ),
- array( 'pr_page' => $id,
- 'pr_type' => $action,
- 'pr_level' => $restrictions,
- 'pr_cascade' => ( $cascade && $action == 'edit' ) ? 1 : 0,
- 'pr_expiry' => $encodedExpiry[$action]
- ),
- __METHOD__
- );
- } else {
- $dbw->delete( 'page_restrictions', array( 'pr_page' => $id,
- 'pr_type' => $action ), __METHOD__ );
- }
- }
-
- # Insert a null revision
- $nullRevision = Revision::newNullRevision( $dbw, $id, $editComment, true );
- $nullRevId = $nullRevision->insertOn( $dbw );
-
- $latest = $this->getLatest();
- # Update page record
- $dbw->update( 'page',
- array( /* SET */
- 'page_touched' => $dbw->timestamp(),
- 'page_restrictions' => '',
- 'page_latest' => $nullRevId
- ), array( /* WHERE */
- 'page_id' => $id
- ), 'Article::protect'
- );
-
- wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $nullRevision, $latest, $wgUser ) );
- wfRunHooks( 'ArticleProtectComplete', array( &$this, &$wgUser, $limit, $reason ) );
-
- # Update the protection log
- $log = new LogPage( 'protect' );
- if ( $protect ) {
- $params = array( $protect_description, $cascade ? 'cascade' : '' );
- $log->addEntry( $modified ? 'modify' : 'protect', $this->mTitle, trim( $reason ), $params );
- } else {
- $log->addEntry( 'unprotect', $this->mTitle, $reason );
- }
- } # End hook
- } # End "changed" check
-
- return true;
- }
-
- /**
- * Take an array of page restrictions and flatten it to a string
- * suitable for insertion into the page_restrictions field.
- * @param $limit Array
- * @return String
- */
- protected static function flattenRestrictions( $limit ) {
- if ( !is_array( $limit ) ) {
- throw new MWException( 'Article::flattenRestrictions given non-array restriction set' );
- }
-
- $bits = array();
- ksort( $limit );
-
- foreach ( $limit as $action => $restrictions ) {
- if ( $restrictions != '' ) {
- $bits[] = "$action=$restrictions";
- }
- }
-
- return implode( ':', $bits );
- }
-
- /**
- * Auto-generates a deletion reason
- *
- * @param &$hasHistory Boolean: whether the page has a history
- * @return mixed String containing deletion reason or empty string, or boolean false
- * if no revision occurred
- */
- public function generateReason( &$hasHistory ) {
- global $wgContLang;
-
- $dbw = wfGetDB( DB_MASTER );
- // Get the last revision
- $rev = Revision::newFromTitle( $this->mTitle );
-
- if ( is_null( $rev ) ) {
- return false;
- }
-
- // Get the article's contents
- $contents = $rev->getText();
- $blank = false;
-
- // If the page is blank, use the text from the previous revision,
- // which can only be blank if there's a move/import/protect dummy revision involved
- if ( $contents == '' ) {
- $prev = $rev->getPrevious();
-
- if ( $prev ) {
- $contents = $prev->getText();
- $blank = true;
- }
- }
-
- // Find out if there was only one contributor
- // Only scan the last 20 revisions
- $res = $dbw->select( 'revision', 'rev_user_text',
- array( 'rev_page' => $this->getID(), $dbw->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' ),
- __METHOD__,
- array( 'LIMIT' => 20 )
- );
-
- if ( $res === false ) {
- // This page has no revisions, which is very weird
- return false;
- }
-
- $hasHistory = ( $res->numRows() > 1 );
- $row = $dbw->fetchObject( $res );
-
- if ( $row ) { // $row is false if the only contributor is hidden
- $onlyAuthor = $row->rev_user_text;
- // Try to find a second contributor
- foreach ( $res as $row ) {
- if ( $row->rev_user_text != $onlyAuthor ) { // Bug 22999
- $onlyAuthor = false;
- break;
- }
- }
- } else {
- $onlyAuthor = false;
- }
-
- // Generate the summary with a '$1' placeholder
- if ( $blank ) {
- // The current revision is blank and the one before is also
- // blank. It's just not our lucky day
- $reason = wfMsgForContent( 'exbeforeblank', '$1' );
- } else {
- if ( $onlyAuthor ) {
- $reason = wfMsgForContent( 'excontentauthor', '$1', $onlyAuthor );
- } else {
- $reason = wfMsgForContent( 'excontent', '$1' );
- }
- }
-
- if ( $reason == '-' ) {
- // Allow these UI messages to be blanked out cleanly
- return '';
- }
-
- // Replace newlines with spaces to prevent uglyness
- $contents = preg_replace( "/[\n\r]/", ' ', $contents );
- // Calculate the maximum amount of chars to get
- // Max content length = max comment length - length of the comment (excl. $1) - '...'
- $maxLength = 255 - ( strlen( $reason ) - 2 ) - 3;
- $contents = $wgContLang->truncate( $contents, $maxLength );
- // Remove possible unfinished links
- $contents = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $contents );
- // Now replace the '$1' placeholder
- $reason = str_replace( '$1', $contents, $reason );
-
- return $reason;
- }
-
-
- /*
- * UI entry point for page deletion
- */
- public function delete() {
- global $wgUser, $wgOut, $wgRequest;
-
- $confirm = $wgRequest->wasPosted() &&
- $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) );
-
- $this->DeleteReasonList = $wgRequest->getText( 'wpDeleteReasonList', 'other' );
- $this->DeleteReason = $wgRequest->getText( 'wpReason' );
-
- $reason = $this->DeleteReasonList;
-
- if ( $reason != 'other' && $this->DeleteReason != '' ) {
- // Entry from drop down menu + additional comment
- $reason .= wfMsgForContent( 'colon-separator' ) . $this->DeleteReason;
- } elseif ( $reason == 'other' ) {
- $reason = $this->DeleteReason;
- }
-
- # Flag to hide all contents of the archived revisions
- $suppress = $wgRequest->getVal( 'wpSuppress' ) && $wgUser->isAllowed( 'suppressrevision' );
-
- # This code desperately needs to be totally rewritten
-
- # Read-only check...
- if ( wfReadOnly() ) {
- $wgOut->readOnlyPage();
-
- return;
- }
-
- # Check permissions
- $permission_errors = $this->mTitle->getUserPermissionsErrors( 'delete', $wgUser );
-
- if ( count( $permission_errors ) > 0 ) {
- $wgOut->showPermissionsErrorPage( $permission_errors );
-
- return;
- }
-
- $wgOut->setPagetitle( wfMsg( 'delete-confirm', $this->mTitle->getPrefixedText() ) );
-
- # Better double-check that it hasn't been deleted yet!
- $dbw = wfGetDB( DB_MASTER );
- $conds = $this->mTitle->pageCond();
- $latest = $dbw->selectField( 'page', 'page_latest', $conds, __METHOD__ );
- if ( $latest === false ) {
- $wgOut->showFatalError(
- Html::rawElement(
- 'div',
- array( 'class' => 'error mw-error-cannotdelete' ),
- wfMsgExt( 'cannotdelete', array( 'parse' ), $this->mTitle->getPrefixedText() )
- )
- );
- $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) );
- LogEventsList::showLogExtract(
- $wgOut,
- 'delete',
- $this->mTitle->getPrefixedText()
- );
-
- return;
- }
-
- # Hack for big sites
- $bigHistory = $this->isBigDeletion();
- if ( $bigHistory && !$this->mTitle->userCan( 'bigdelete' ) ) {
- global $wgLang, $wgDeleteRevisionsLimit;
-
- $wgOut->wrapWikiMsg( "<div class='error'>\n$1\n</div>\n",
- array( 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ) );
-
- return;
- }
-
- if ( $confirm ) {
- $this->doDelete( $reason, $suppress );
-
- if ( $wgRequest->getCheck( 'wpWatch' ) && $wgUser->isLoggedIn() ) {
- $this->doWatch();
- } elseif ( $this->mTitle->userIsWatching() ) {
- $this->doUnwatch();
- }
-
- return;
- }
-
- // Generate deletion reason
- $hasHistory = false;
- if ( !$reason ) {
- $reason = $this->generateReason( $hasHistory );
- }
-
- // If the page has a history, insert a warning
- if ( $hasHistory && !$confirm ) {
- global $wgLang;
-
- $skin = $wgUser->getSkin();
- $revisions = $this->estimateRevisionCount();
- //FIXME: lego
- $wgOut->addHTML( '<strong class="mw-delete-warning-revisions">' .
- wfMsgExt( 'historywarning', array( 'parseinline' ), $wgLang->formatNum( $revisions ) ) .
- wfMsgHtml( 'word-separator' ) . $skin->link( $this->mTitle,
- wfMsgHtml( 'history' ),
- array( 'rel' => 'archives' ),
- array( 'action' => 'history' ) ) .
- '</strong>'
- );
-
- if ( $bigHistory ) {
- global $wgDeleteRevisionsLimit;
- $wgOut->wrapWikiMsg( "<div class='error'>\n$1\n</div>\n",
- array( 'delete-warning-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ) );
- }
- }
-
- return $this->confirmDelete( $reason );
- }
-
- /**
- * @return bool whether or not the page surpasses $wgDeleteRevisionsLimit revisions
- */
- public function isBigDeletion() {
- global $wgDeleteRevisionsLimit;
-
- if ( $wgDeleteRevisionsLimit ) {
- $revCount = $this->estimateRevisionCount();
-
- return $revCount > $wgDeleteRevisionsLimit;
- }
-
- return false;
- }
-
- /**
- * @return int approximate revision count
- */
- public function estimateRevisionCount() {
- $dbr = wfGetDB( DB_SLAVE );
-
- // For an exact count...
- // return $dbr->selectField( 'revision', 'COUNT(*)',
- // array( 'rev_page' => $this->getId() ), __METHOD__ );
- return $dbr->estimateRowCount( 'revision', '*',
- array( 'rev_page' => $this->getId() ), __METHOD__ );
- }
-
- /**
- * Get the last N authors
- * @param $num Integer: number of revisions to get
- * @param $revLatest String: the latest rev_id, selected from the master (optional)
- * @return array Array of authors, duplicates not removed
- */
- public function getLastNAuthors( $num, $revLatest = 0 ) {
- wfProfileIn( __METHOD__ );
- // First try the slave
- // If that doesn't have the latest revision, try the master
- $continue = 2;
- $db = wfGetDB( DB_SLAVE );
-
- do {
- $res = $db->select( array( 'page', 'revision' ),
- array( 'rev_id', 'rev_user_text' ),
- array(
- 'page_namespace' => $this->mTitle->getNamespace(),
- 'page_title' => $this->mTitle->getDBkey(),
- 'rev_page = page_id'
- ), __METHOD__, $this->getSelectOptions( array(
- 'ORDER BY' => 'rev_timestamp DESC',
- 'LIMIT' => $num
- ) )
- );
-
- if ( !$res ) {
- wfProfileOut( __METHOD__ );
- return array();
- }
-
- $row = $db->fetchObject( $res );
-
- if ( $continue == 2 && $revLatest && $row->rev_id != $revLatest ) {
- $db = wfGetDB( DB_MASTER );
- $continue--;
- } else {
- $continue = 0;
- }
- } while ( $continue );
-
- $authors = array( $row->rev_user_text );
-
- foreach ( $res as $row ) {
- $authors[] = $row->rev_user_text;
- }
-
- wfProfileOut( __METHOD__ );
- return $authors;
- }
-
- /**
- * Output deletion confirmation dialog
- * FIXME: Move to another file?
- * @param $reason String: prefilled reason
- */
- public function confirmDelete( $reason ) {
- global $wgOut, $wgUser;
-
- wfDebug( "Article::confirmDelete\n" );
-
- $deleteBackLink = $wgUser->getSkin()->linkKnown( $this->mTitle );
- $wgOut->setSubtitle( wfMsgHtml( 'delete-backlink', $deleteBackLink ) );
- $wgOut->setRobotPolicy( 'noindex,nofollow' );
- $wgOut->addWikiMsg( 'confirmdeletetext' );
-
- wfRunHooks( 'ArticleConfirmDelete', array( $this, $wgOut, &$reason ) );
-
- if ( $wgUser->isAllowed( 'suppressrevision' ) ) {
- $suppress = "<tr id=\"wpDeleteSuppressRow\">
- <td></td>
- <td class='mw-input'><strong>" .
- Xml::checkLabel( wfMsg( 'revdelete-suppress' ),
- 'wpSuppress', 'wpSuppress', false, array( 'tabindex' => '4' ) ) .
- "</strong></td>
- </tr>";
- } else {
- $suppress = '';
- }
- $checkWatch = $wgUser->getBoolOption( 'watchdeletion' ) || $this->mTitle->userIsWatching();
-
- $form = Xml::openElement( 'form', array( 'method' => 'post',
- 'action' => $this->mTitle->getLocalURL( 'action=delete' ), 'id' => 'deleteconfirm' ) ) .
- Xml::openElement( 'fieldset', array( 'id' => 'mw-delete-table' ) ) .
- Xml::tags( 'legend', null, wfMsgExt( 'delete-legend', array( 'parsemag', 'escapenoentities' ) ) ) .
- Xml::openElement( 'table', array( 'id' => 'mw-deleteconfirm-table' ) ) .
- "<tr id=\"wpDeleteReasonListRow\">
- <td class='mw-label'>" .
- Xml::label( wfMsg( 'deletecomment' ), 'wpDeleteReasonList' ) .
- "</td>
- <td class='mw-input'>" .
- Xml::listDropDown( 'wpDeleteReasonList',
- wfMsgForContent( 'deletereason-dropdown' ),
- wfMsgForContent( 'deletereasonotherlist' ), '', 'wpReasonDropDown', 1 ) .
- "</td>
- </tr>
- <tr id=\"wpDeleteReasonRow\">
- <td class='mw-label'>" .
- Xml::label( wfMsg( 'deleteotherreason' ), 'wpReason' ) .
- "</td>
- <td class='mw-input'>" .
- Html::input( 'wpReason', $reason, 'text', array(
- 'size' => '60',
- 'maxlength' => '255',
- 'tabindex' => '2',
- 'id' => 'wpReason',
- 'autofocus'
- ) ) .
- "</td>
- </tr>";
-
- # Disallow watching if user is not logged in
- if ( $wgUser->isLoggedIn() ) {
- $form .= "
- <tr>
- <td></td>
- <td class='mw-input'>" .
- Xml::checkLabel( wfMsg( 'watchthis' ),
- 'wpWatch', 'wpWatch', $checkWatch, array( 'tabindex' => '3' ) ) .
- "</td>
- </tr>";
- }
-
- $form .= "
- $suppress
- <tr>
- <td></td>
- <td class='mw-submit'>" .
- Xml::submitButton( wfMsg( 'deletepage' ),
- array( 'name' => 'wpConfirmB', 'id' => 'wpConfirmB', 'tabindex' => '5' ) ) .
- "</td>
- </tr>" .
- Xml::closeElement( 'table' ) .
- Xml::closeElement( 'fieldset' ) .
- Html::hidden( 'wpEditToken', $wgUser->editToken() ) .
- Xml::closeElement( 'form' );
-
- if ( $wgUser->isAllowed( 'editinterface' ) ) {
- $skin = $wgUser->getSkin();
- $title = Title::makeTitle( NS_MEDIAWIKI, 'Deletereason-dropdown' );
- $link = $skin->link(
- $title,
- wfMsgHtml( 'delete-edit-reasonlist' ),
- array(),
- array( 'action' => 'edit' )
- );
- $form .= '<p class="mw-delete-editreasons">' . $link . '</p>';
- }
-
- $wgOut->addHTML( $form );
- $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) );
- LogEventsList::showLogExtract( $wgOut, 'delete',
- $this->mTitle->getPrefixedText()
- );
- }
-
- /**
- * Perform a deletion and output success or failure messages
- */
- public function doDelete( $reason, $suppress = false ) {
- global $wgOut, $wgUser;
-
- $id = $this->mTitle->getArticleID( Title::GAID_FOR_UPDATE );
-
- $error = '';
- if ( wfRunHooks( 'ArticleDelete', array( &$this, &$wgUser, &$reason, &$error ) ) ) {
- if ( $this->doDeleteArticle( $reason, $suppress, $id ) ) {
- $deleted = $this->mTitle->getPrefixedText();
-
- $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) );
- $wgOut->setRobotPolicy( 'noindex,nofollow' );
-
- $loglink = '[[Special:Log/delete|' . wfMsgNoTrans( 'deletionlog' ) . ']]';
-
- $wgOut->addWikiMsg( 'deletedtext', $deleted, $loglink );
- $wgOut->returnToMain( false );
- wfRunHooks( 'ArticleDeleteComplete', array( &$this, &$wgUser, $reason, $id ) );
- }
- } else {
- if ( $error == '' ) {
- $wgOut->showFatalError(
- Html::rawElement(
- 'div',
- array( 'class' => 'error mw-error-cannotdelete' ),
- wfMsgExt( 'cannotdelete', array( 'parse' ), $this->mTitle->getPrefixedText() )
- )
- );
-
- $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) );
-
- LogEventsList::showLogExtract(
- $wgOut,
- 'delete',
- $this->mTitle->getPrefixedText()
- );
- } else {
- $wgOut->showFatalError( $error );
- }
- }
- }
-
- /**
- * Back-end article deletion
- * Deletes the article with database consistency, writes logs, purges caches
- *
- * @param $reason string delete reason for deletion log
- * @param suppress bitfield
- * Revision::DELETED_TEXT
- * Revision::DELETED_COMMENT
- * Revision::DELETED_USER
- * Revision::DELETED_RESTRICTED
- * @param $id int article ID
- * @param $commit boolean defaults to true, triggers transaction end
- * @return boolean true if successful
- */
- public function doDeleteArticle( $reason, $suppress = false, $id = 0, $commit = true ) {
- global $wgDeferredUpdateList, $wgUseTrackbacks;
-
- wfDebug( __METHOD__ . "\n" );
-
- $dbw = wfGetDB( DB_MASTER );
- $t = $this->mTitle->getDBkey();
- $id = $id ? $id : $this->mTitle->getArticleID( Title::GAID_FOR_UPDATE );
-
- if ( $t === '' || $id == 0 ) {
- return false;
- }
-
- $u = new SiteStatsUpdate( 0, 1, - (int)$this->isCountable( $this->getRawText() ), -1 );
- array_push( $wgDeferredUpdateList, $u );
-
- // 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';
- }
-
- $dbw->begin();
- // 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.
- $dbw->insertSelect( 'archive', array( 'page', 'revision' ),
- 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_text_id' => 'rev_text_id',
- 'ar_text' => '\'\'', // Be explicit to appease
- 'ar_flags' => '\'\'', // MySQL's "strict mode"...
- 'ar_len' => 'rev_len',
- 'ar_page_id' => 'page_id',
- 'ar_deleted' => $bitfield
- ), array(
- 'page_id' => $id,
- 'page_id = rev_page'
- ), __METHOD__
- );
-
- # Delete restrictions for it
- $dbw->delete( 'page_restrictions', array ( 'pr_page' => $id ), __METHOD__ );
-
- # Now that it's safely backed up, delete it
- $dbw->delete( 'page', array( 'page_id' => $id ), __METHOD__ );
- $ok = ( $dbw->affectedRows() > 0 ); // getArticleId() uses slave, could be laggy
-
- if ( !$ok ) {
- $dbw->rollback();
- return false;
- }
-
- # Fix category table counts
- $cats = array();
- $res = $dbw->select( 'categorylinks', 'cl_to', array( 'cl_from' => $id ), __METHOD__ );
-
- foreach ( $res as $row ) {
- $cats [] = $row->cl_to;
- }
-
- $this->updateCategoryCounts( array(), $cats );
-
- # If using cascading deletes, we can skip some explicit deletes
- if ( !$dbw->cascadingDeletes() ) {
- $dbw->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ );
-
- if ( $wgUseTrackbacks )
- $dbw->delete( 'trackbacks', array( 'tb_page' => $id ), __METHOD__ );
-
- # Delete outgoing links
- $dbw->delete( 'pagelinks', array( 'pl_from' => $id ) );
- $dbw->delete( 'imagelinks', array( 'il_from' => $id ) );
- $dbw->delete( 'categorylinks', array( 'cl_from' => $id ) );
- $dbw->delete( 'templatelinks', array( 'tl_from' => $id ) );
- $dbw->delete( 'externallinks', array( 'el_from' => $id ) );
- $dbw->delete( 'langlinks', array( 'll_from' => $id ) );
- $dbw->delete( 'redirect', array( 'rd_from' => $id ) );
- }
-
- # If using cleanup triggers, we can skip some manual deletes
- if ( !$dbw->cleanupTriggers() ) {
- # Clean up recentchanges entries...
- $dbw->delete( 'recentchanges',
- array( 'rc_type != ' . RC_LOG,
- 'rc_namespace' => $this->mTitle->getNamespace(),
- 'rc_title' => $this->mTitle->getDBkey() ),
- __METHOD__ );
- $dbw->delete( 'recentchanges',
- array( 'rc_type != ' . RC_LOG, 'rc_cur_id' => $id ),
- __METHOD__ );
- }
-
- # Clear caches
- Article::onArticleDelete( $this->mTitle );
-
- # Clear the cached article id so the interface doesn't act like we exist
- $this->mTitle->resetArticleID( 0 );
-
- # Log the deletion, if the page was suppressed, log it at Oversight instead
- $logtype = $suppress ? 'suppress' : 'delete';
- $log = new LogPage( $logtype );
-
- # Make sure logging got through
- $log->addEntry( 'delete', $this->mTitle, $reason, array() );
-
- if ( $commit ) {
- $dbw->commit();
- }
-
- return true;
- }
-
- /**
- * Roll back the most recent consecutive set of edits to a page
- * from the same user; fails if there are no eligible edits to
- * roll back to, e.g. user is the sole contributor. This function
- * performs permissions checks on $wgUser, then calls commitRollback()
- * to do the dirty work
- *
- * @param $fromP String: Name of the user whose edits to rollback.
- * @param $summary String: Custom summary. Set to default summary if empty.
- * @param $token String: Rollback token.
- * @param $bot Boolean: If true, mark all reverted edits as bot.
- *
- * @param $resultDetails Array: contains result-specific array of additional values
- * 'alreadyrolled' : 'current' (rev)
- * success : 'summary' (str), 'current' (rev), 'target' (rev)
- *
- * @return array of errors, each error formatted as
- * array(messagekey, param1, param2, ...).
- * On success, the array is empty. This array can also be passed to
- * OutputPage::showPermissionsErrorPage().
- */
- public function doRollback( $fromP, $summary, $token, $bot, &$resultDetails ) {
- global $wgUser;
-
- $resultDetails = null;
-
- # Check permissions
- $editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser );
- $rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $wgUser );
- $errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) );
-
- if ( !$wgUser->matchEditToken( $token, array( $this->mTitle->getPrefixedText(), $fromP ) ) ) {
- $errors[] = array( 'sessionfailure' );
- }
-
- if ( $wgUser->pingLimiter( 'rollback' ) || $wgUser->pingLimiter() ) {
- $errors[] = array( 'actionthrottledtext' );
- }
-
- # If there were errors, bail out now
- if ( !empty( $errors ) ) {
- return $errors;
- }
-
- return $this->commitRollback( $fromP, $summary, $bot, $resultDetails );
- }
-
- /**
- * Backend implementation of doRollback(), please refer there for parameter
- * and return value documentation
- *
- * NOTE: This function does NOT check ANY permissions, it just commits the
- * rollback to the DB Therefore, you should only call this function direct-
- * ly if you want to use custom permissions checks. If you don't, use
- * doRollback() instead.
- */
- public function commitRollback( $fromP, $summary, $bot, &$resultDetails ) {
- global $wgUseRCPatrol, $wgUser, $wgLang;
-
- $dbw = wfGetDB( DB_MASTER );
-
- if ( wfReadOnly() ) {
- return array( array( 'readonlytext' ) );
- }
-
- # Get the last editor
- $current = Revision::newFromTitle( $this->mTitle );
- if ( is_null( $current ) ) {
- # Something wrong... no page?
- return array( array( 'notanarticle' ) );
- }
-
- $from = str_replace( '_', ' ', $fromP );
- # User name given should match up with the top revision.
- # If the user was deleted then $from should be empty.
- if ( $from != $current->getUserText() ) {
- $resultDetails = array( 'current' => $current );
- return array( array( 'alreadyrolled',
- htmlspecialchars( $this->mTitle->getPrefixedText() ),
- htmlspecialchars( $fromP ),
- htmlspecialchars( $current->getUserText() )
- ) );
- }
-
- # Get the last edit not by this guy...
- # Note: these may not be public values
- $user = intval( $current->getRawUser() );
- $user_text = $dbw->addQuotes( $current->getRawUserText() );
- $s = $dbw->selectRow( 'revision',
- array( 'rev_id', 'rev_timestamp', 'rev_deleted' ),
- array( 'rev_page' => $current->getPage(),
- "rev_user != {$user} OR rev_user_text != {$user_text}"
- ), __METHOD__,
- array( 'USE INDEX' => 'page_timestamp',
- 'ORDER BY' => 'rev_timestamp DESC' )
- );
- if ( $s === false ) {
- # No one else ever edited this page
- return array( array( 'cantrollback' ) );
- } else if ( $s->rev_deleted & Revision::DELETED_TEXT || $s->rev_deleted & Revision::DELETED_USER ) {
- # Only admins can see this text
- return array( array( 'notvisiblerev' ) );
- }
-
- $set = array();
- if ( $bot && $wgUser->isAllowed( 'markbotedits' ) ) {
- # Mark all reverted edits as bot
- $set['rc_bot'] = 1;
- }
-
- if ( $wgUseRCPatrol ) {
- # Mark all reverted edits as patrolled
- $set['rc_patrolled'] = 1;
- }
-
- if ( count( $set ) ) {
- $dbw->update( 'recentchanges', $set,
- array( /* WHERE */
- 'rc_cur_id' => $current->getPage(),
- 'rc_user_text' => $current->getUserText(),
- "rc_timestamp > '{$s->rev_timestamp}'",
- ), __METHOD__
- );
- }
-
- # Generate the edit summary if necessary
- $target = Revision::newFromId( $s->rev_id );
- if ( empty( $summary ) ) {
- if ( $from == '' ) { // no public user name
- $summary = wfMsgForContent( 'revertpage-nouser' );
- } else {
- $summary = wfMsgForContent( 'revertpage' );
- }
- }
-
- # Allow the custom summary to use the same args as the default message
- $args = array(
- $target->getUserText(), $from, $s->rev_id,
- $wgLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ), true ),
- $current->getId(), $wgLang->timeanddate( $current->getTimestamp() )
- );
- $summary = wfMsgReplaceArgs( $summary, $args );
-
- # Save
- $flags = EDIT_UPDATE;
-
- if ( $wgUser->isAllowed( 'minoredit' ) ) {
- $flags |= EDIT_MINOR;
- }
-
- if ( $bot && ( $wgUser->isAllowed( 'markbotedits' ) || $wgUser->isAllowed( 'bot' ) ) ) {
- $flags |= EDIT_FORCE_BOT;
- }
-
- # Actually store the edit
- $status = $this->doEdit( $target->getText(), $summary, $flags, $target->getId() );
- if ( !empty( $status->value['revision'] ) ) {
- $revId = $status->value['revision']->getId();
- } else {
- $revId = false;
- }
-
- wfRunHooks( 'ArticleRollbackComplete', array( $this, $wgUser, $target, $current ) );
-
- $resultDetails = array(
- 'summary' => $summary,
- 'current' => $current,
- 'target' => $target,
- 'newid' => $revId
- );
-
- return array();
- }
-
- /**
- * User interface for rollback operations
- */
- public function rollback() {
- global $wgUser, $wgOut, $wgRequest;
-
- $details = null;
-
- $result = $this->doRollback(
- $wgRequest->getVal( 'from' ),
- $wgRequest->getText( 'summary' ),
- $wgRequest->getVal( 'token' ),
- $wgRequest->getBool( 'bot' ),
- $details
- );
-
- if ( in_array( array( 'actionthrottledtext' ), $result ) ) {
- $wgOut->rateLimited();
- return;
- }
-
- if ( isset( $result[0][0] ) && ( $result[0][0] == 'alreadyrolled' || $result[0][0] == 'cantrollback' ) ) {
- $wgOut->setPageTitle( wfMsg( 'rollbackfailed' ) );
- $errArray = $result[0];
- $errMsg = array_shift( $errArray );
- $wgOut->addWikiMsgArray( $errMsg, $errArray );
-
- if ( isset( $details['current'] ) ) {
- $current = $details['current'];
-
- if ( $current->getComment() != '' ) {
- $wgOut->addWikiMsgArray( 'editcomment', array(
- $wgUser->getSkin()->formatComment( $current->getComment() ) ), array( 'replaceafter' ) );
- }
- }
-
- return;
- }
-
- # Display permissions errors before read-only message -- there's no
- # point in misleading the user into thinking the inability to rollback
- # is only temporary.
- if ( !empty( $result ) && $result !== array( array( 'readonlytext' ) ) ) {
- # array_diff is completely broken for arrays of arrays, sigh.
- # Remove any 'readonlytext' error manually.
- $out = array();
- foreach ( $result as $error ) {
- if ( $error != array( 'readonlytext' ) ) {
- $out [] = $error;
- }
- }
- $wgOut->showPermissionsErrorPage( $out );
-
- return;
- }
-
- if ( $result == array( array( 'readonlytext' ) ) ) {
- $wgOut->readOnlyPage();
-
- return;
- }
-
- $current = $details['current'];
- $target = $details['target'];
- $newId = $details['newid'];
- $wgOut->setPageTitle( wfMsg( 'actioncomplete' ) );
- $wgOut->setRobotPolicy( 'noindex,nofollow' );
-
- if ( $current->getUserText() === '' ) {
- $old = wfMsg( 'rev-deleted-user' );
- } else {
- $old = $wgUser->getSkin()->userLink( $current->getUser(), $current->getUserText() )
- . $wgUser->getSkin()->userToolLinks( $current->getUser(), $current->getUserText() );
- }
-
- $new = $wgUser->getSkin()->userLink( $target->getUser(), $target->getUserText() )
- . $wgUser->getSkin()->userToolLinks( $target->getUser(), $target->getUserText() );
- $wgOut->addHTML( wfMsgExt( 'rollback-success', array( 'parse', 'replaceafter' ), $old, $new ) );
- $wgOut->returnToMain( false, $this->mTitle );
-
- if ( !$wgRequest->getBool( 'hidediff', false ) && !$wgUser->getBoolOption( 'norollbackdiff', false ) ) {
- $de = new DifferenceEngine( $this->mTitle, $current->getId(), $newId, false, true );
- $de->showDiff( '', '' );
- }
- }
-
- /**
- * Do standard deferred updates after page view
- */
- public function viewUpdates() {
- global $wgDeferredUpdateList, $wgDisableCounters, $wgUser;
- if ( wfReadOnly() ) {
- return;
- }
-
- # Don't update page view counters on views from bot users (bug 14044)
- if ( !$wgDisableCounters && !$wgUser->isAllowed( 'bot' ) && $this->getID() ) {
- $wgDeferredUpdateList[] = new ViewCountUpdate( $this->getID() );
- $wgDeferredUpdateList[] = new SiteStatsUpdate( 1, 0, 0 );
- }
-
- # Update newtalk / watchlist notification status
- $wgUser->clearNotification( $this->mTitle );
- }
-
- /**
- * Prepare text which is about to be saved.
- * Returns a stdclass with source, pst and output members
- */
- public function prepareTextForEdit( $text, $revid = null, User $user = null ) {
- if ( $this->mPreparedEdit && $this->mPreparedEdit->newText == $text && $this->mPreparedEdit->revid == $revid ) {
- // Already prepared
- return $this->mPreparedEdit;
- }
-
- global $wgParser;
-
- if( $user === null ) {
- global $wgUser;
- $user = $wgUser;
- }
- $popts = ParserOptions::newFromUser( $user );
- wfRunHooks( 'ArticlePrepareTextForEdit', array( $this, $popts ) );
-
- $edit = (object)array();
- $edit->revid = $revid;
- $edit->newText = $text;
- $edit->pst = $this->preSaveTransform( $text, $user, $popts );
- $edit->popts = $this->getParserOptions( true );
- $edit->output = $wgParser->parse( $edit->pst, $this->mTitle, $edit->popts, true, true, $revid );
- $edit->oldText = $this->getRawText();
-
- $this->mPreparedEdit = $edit;
-
- return $edit;
- }
-
- /**
- * Do standard deferred updates after page edit.
- * Update links tables, site stats, search index and message cache.
- * Purges pages that include this page if the text was changed here.
- * Every 100th edit, prune the recent changes table.
- *
- * @private
- * @param $text String: New text of the article
- * @param $summary String: Edit summary
- * @param $minoredit Boolean: Minor edit
- * @param $timestamp_of_pagechange String timestamp associated with the page change
- * @param $newid Integer: rev_id value of the new revision
- * @param $changed Boolean: Whether or not the content actually changed
- * @param $user User object: User doing the edit
- */
- public function editUpdates( $text, $summary, $minoredit, $timestamp_of_pagechange, $newid, $changed = true, User $user = null ) {
- global $wgDeferredUpdateList, $wgUser, $wgEnableParserCache;
-
- wfProfileIn( __METHOD__ );
-
- # Parse the text
- # Be careful not to double-PST: $text is usually already PST-ed once
- if ( !$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) {
- wfDebug( __METHOD__ . ": No prepared edit or vary-revision is set...\n" );
- $editInfo = $this->prepareTextForEdit( $text, $newid, $user );
- } else {
- wfDebug( __METHOD__ . ": No vary-revision, using prepared edit...\n" );
- $editInfo = $this->mPreparedEdit;
- }
-
- # Save it to the parser cache
- if ( $wgEnableParserCache ) {
- $parserCache = ParserCache::singleton();
- $parserCache->save( $editInfo->output, $this, $editInfo->popts );
- }
-
- # Update the links tables
- $u = new LinksUpdate( $this->mTitle, $editInfo->output );
- $u->doUpdate();
-
- wfRunHooks( 'ArticleEditUpdates', array( &$this, &$editInfo, $changed ) );
-
- if ( wfRunHooks( 'ArticleEditUpdatesDeleteFromRecentchanges', array( &$this ) ) ) {
- if ( 0 == mt_rand( 0, 99 ) ) {
- // Flush old entries from the `recentchanges` table; we do this on
- // random requests so as to avoid an increase in writes for no good reason
- global $wgRCMaxAge;
-
- $dbw = wfGetDB( DB_MASTER );
- $cutoff = $dbw->timestamp( time() - $wgRCMaxAge );
- $recentchanges = $dbw->tableName( 'recentchanges' );
- $sql = "DELETE FROM $recentchanges WHERE rc_timestamp < '{$cutoff}'";
-
- $dbw->query( $sql );
- }
- }
-
- $id = $this->getID();
- $title = $this->mTitle->getPrefixedDBkey();
- $shortTitle = $this->mTitle->getDBkey();
-
- if ( 0 == $id ) {
- wfProfileOut( __METHOD__ );
- return;
- }
-
- $u = new SiteStatsUpdate( 0, 1, $this->mGoodAdjustment, $this->mTotalAdjustment );
- array_push( $wgDeferredUpdateList, $u );
- $u = new SearchUpdate( $id, $title, $text );
- array_push( $wgDeferredUpdateList, $u );
-
- # If this is another user's talk page, update newtalk
- # Don't do this if $changed = false otherwise some idiot can null-edit a
- # load of user talk pages and piss people off, nor if it's a minor edit
- # by a properly-flagged bot.
- if ( $this->mTitle->getNamespace() == NS_USER_TALK && $shortTitle != $wgUser->getTitleKey() && $changed
- && !( $minoredit && $wgUser->isAllowed( 'nominornewtalk' ) )
- ) {
- if ( wfRunHooks( 'ArticleEditUpdateNewTalk', array( &$this ) ) ) {
- $other = User::newFromName( $shortTitle, false );
- if ( !$other ) {
- wfDebug( __METHOD__ . ": invalid username\n" );
- } elseif ( User::isIP( $shortTitle ) ) {
- // An anonymous user
- $other->setNewtalk( true );
- } elseif ( $other->isLoggedIn() ) {
- $other->setNewtalk( true );
- } else {
- wfDebug( __METHOD__ . ": don't need to notify a nonexistent user\n" );
- }
- }
- }
-
- if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
- MessageCache::singleton()->replace( $shortTitle, $text );
- }
-
- wfProfileOut( __METHOD__ );
- }
-
- /**
- * Perform article updates on a special page creation.
- *
- * @param $rev Revision object
- *
- * @todo This is a shitty interface function. Kill it and replace the
- * other shitty functions like editUpdates and such so it's not needed
- * anymore.
- */
- public function createUpdates( $rev ) {
- $this->mGoodAdjustment = $this->isCountable( $rev->getText() );
- $this->mTotalAdjustment = 1;
- $this->editUpdates( $rev->getText(), $rev->getComment(),
- $rev->isMinor(), wfTimestamp(), $rev->getId(), true );
- }
-
- /**
- * Generate the navigation links when browsing through an article revisions
- * It shows the information as:
- * Revision as of \<date\>; view current revision
- * \<- Previous version | Next Version -\>
- *
- * @param $oldid String: revision ID of this article revision
- */
- public function setOldSubtitle( $oldid = 0 ) {
- global $wgLang, $wgOut, $wgUser, $wgRequest;
-
- if ( !wfRunHooks( 'DisplayOldSubtitle', array( &$this, &$oldid ) ) ) {
- return;
- }
-
- $unhide = $wgRequest->getInt( 'unhide' ) == 1;
-
- # Cascade unhide param in links for easy deletion browsing
- $extraParams = array();
- if ( $wgRequest->getVal( 'unhide' ) ) {
- $extraParams['unhide'] = 1;
- }
-
- $revision = Revision::newFromId( $oldid );
-
- $current = ( $oldid == $this->mLatest );
- $td = $wgLang->timeanddate( $this->mTimestamp, true );
- $tddate = $wgLang->date( $this->mTimestamp, true );
- $tdtime = $wgLang->time( $this->mTimestamp, true );
- $sk = $wgUser->getSkin();
- $lnk = $current
- ? wfMsgHtml( 'currentrevisionlink' )
- : $sk->link(
- $this->mTitle,
- wfMsgHtml( 'currentrevisionlink' ),
- array(),
- $extraParams,
- array( 'known', 'noclasses' )
- );
- $curdiff = $current
- ? wfMsgHtml( 'diff' )
- : $sk->link(
- $this->mTitle,
- wfMsgHtml( 'diff' ),
- array(),
- array(
- 'diff' => 'cur',
- 'oldid' => $oldid
- ) + $extraParams,
- array( 'known', 'noclasses' )
- );
- $prev = $this->mTitle->getPreviousRevisionID( $oldid ) ;
- $prevlink = $prev
- ? $sk->link(
- $this->mTitle,
- wfMsgHtml( 'previousrevision' ),
- array(),
- array(
- 'direction' => 'prev',
- 'oldid' => $oldid
- ) + $extraParams,
- array( 'known', 'noclasses' )
- )
- : wfMsgHtml( 'previousrevision' );
- $prevdiff = $prev
- ? $sk->link(
- $this->mTitle,
- wfMsgHtml( 'diff' ),
- array(),
- array(
- 'diff' => 'prev',
- 'oldid' => $oldid
- ) + $extraParams,
- array( 'known', 'noclasses' )
- )
- : wfMsgHtml( 'diff' );
- $nextlink = $current
- ? wfMsgHtml( 'nextrevision' )
- : $sk->link(
- $this->mTitle,
- wfMsgHtml( 'nextrevision' ),
- array(),
- array(
- 'direction' => 'next',
- 'oldid' => $oldid
- ) + $extraParams,
- array( 'known', 'noclasses' )
- );
- $nextdiff = $current
- ? wfMsgHtml( 'diff' )
- : $sk->link(
- $this->mTitle,
- wfMsgHtml( 'diff' ),
- array(),
- array(
- 'diff' => 'next',
- 'oldid' => $oldid
- ) + $extraParams,
- array( 'known', 'noclasses' )
- );
-
- $cdel = '';