diff options
Diffstat (limited to 'includes/Article.php')
-rw-r--r-- | includes/Article.php | 718 |
1 files changed, 438 insertions, 280 deletions
diff --git a/includes/Article.php b/includes/Article.php index 7ba55c54..0544db7d 100644 --- a/includes/Article.php +++ b/includes/Article.php @@ -37,23 +37,11 @@ class Article { /**@}}*/ /** - * Constants used by internal components to get rollback results - */ - const SUCCESS = 0; // Operation successful - const PERM_DENIED = 1; // Permission denied - const BLOCKED = 2; // User has been blocked - const READONLY = 3; // Wiki is in read-only mode - const BAD_TOKEN = 4; // Invalid token specified - const BAD_TITLE = 5; // $this is not a valid Article - const ALREADY_ROLLED = 6; // Someone else already rolled this back. $from and $summary will be set - const ONLY_AUTHOR = 7; // User is the only author of the page - - /** * Constructor and clear the article * @param $title Reference to a Title object. * @param $oldId Integer revision ID, null to fetch from request, zero for current */ - function __construct( &$title, $oldId = null ) { + function __construct( Title $title, $oldId = null ) { $this->mTitle =& $title; $this->mOldId = $oldId; $this->clear(); @@ -135,6 +123,7 @@ class Article { $this->mRevIdFetched = 0; $this->mRedirectUrl = false; $this->mLatest = false; + $this->mPreparedEdit = false; } /** @@ -622,8 +611,9 @@ class Article { */ function view() { global $wgUser, $wgOut, $wgRequest, $wgContLang; - global $wgEnableParserCache, $wgStylePath, $wgUseRCPatrol, $wgParser; + global $wgEnableParserCache, $wgStylePath, $wgParser; global $wgUseTrackbacks, $wgNamespaceRobotPolicies, $wgArticleRobotPolicies; + global $wgDefaultRobotPolicy; $sk = $wgUser->getSkin(); wfProfileIn( __METHOD__ ); @@ -645,6 +635,7 @@ class Article { $rcid = $wgRequest->getVal( 'rcid' ); $rdfrom = $wgRequest->getVal( 'rdfrom' ); $diffOnly = $wgRequest->getBool( 'diffonly', $wgUser->getOption( 'diffonly' ) ); + $purge = $wgRequest->getVal( 'action' ) == 'purge'; $wgOut->setArticleFlag( true ); @@ -657,8 +648,7 @@ class Article { # Honour customised robot policies for this namespace $policy = $wgNamespaceRobotPolicies[$ns]; } else { - # Default to encourage indexing and following links - $policy = 'index,follow'; + $policy = $wgDefaultRobotPolicy; } $wgOut->setRobotPolicy( $policy ); @@ -668,7 +658,7 @@ class Article { if ( !is_null( $diff ) ) { $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); - $de = new DifferenceEngine( $this->mTitle, $oldid, $diff, $rcid ); + $de = new DifferenceEngine( $this->mTitle, $oldid, $diff, $rcid, $purge ); // DifferenceEngine directly fetched the revision: $this->mRevIdFetched = $de->mNewid; $de->showDiffPage( $diffOnly ); @@ -780,11 +770,11 @@ class Article { $this->setOldSubtitle( isset($this->mOldId) ? $this->mOldId : $oldid ); if( $this->mRevision->isDeleted( Revision::DELETED_TEXT ) ) { if( !$this->mRevision->userCan( Revision::DELETED_TEXT ) ) { - $wgOut->addWikiText( wfMsg( 'rev-deleted-text-permission' ) ); + $wgOut->addWikiMsg( 'rev-deleted-text-permission' ); $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); return; } else { - $wgOut->addWikiText( wfMsg( 'rev-deleted-text-view' ) ); + $wgOut->addWikiMsg( 'rev-deleted-text-view' ); // and we are allowed to see... } } @@ -862,12 +852,12 @@ class Article { # check if we're displaying a [[User talk:x.x.x.x]] anonymous talk page if( $ns == NS_USER_TALK && User::isIP( $this->mTitle->getText() ) ) { - $wgOut->addWikiText( wfMsg('anontalkpagetext') ); + $wgOut->addWikiMsg('anontalkpagetext'); } # If we have been passed an &rcid= parameter, we want to give the user a # chance to mark this new article as patrolled. - if ( $wgUseRCPatrol && !is_null( $rcid ) && $rcid != 0 && $wgUser->isAllowed( 'patrol' ) ) { + if( !is_null( $rcid ) && $rcid != 0 && $wgUser->isAllowed( 'patrol' ) && $this->mTitle->exists() ) { $wgOut->addHTML( "<div class='patrollink'>" . wfMsgHtml( 'markaspatrolledlink', @@ -914,31 +904,29 @@ class Article { $o->tb_name, $rmvtxt); } - $wgOut->addWikitext(wfMsg('trackbackbox', $tbtext)); + $wgOut->addWikiMsg( 'trackbackbox', $tbtext ); } function deletetrackback() { global $wgUser, $wgRequest, $wgOut, $wgTitle; if (!$wgUser->matchEditToken($wgRequest->getVal('token'))) { - $wgOut->addWikitext(wfMsg('sessionfailure')); + $wgOut->addWikiMsg( 'sessionfailure' ); return; } - if ((!$wgUser->isAllowed('delete'))) { - $wgOut->permissionRequired( 'delete' ); - return; - } + $permission_errors = $this->mTitle->getUserPermissionsErrors( 'delete', $wgUser ); - if (wfReadOnly()) { - $wgOut->readOnlyPage(); + if (count($permission_errors)>0) + { + $wgOut->showPermissionsErrorPage( $permission_errors ); return; } $db = wfGetDB(DB_MASTER); $db->delete('trackbacks', array('tb_id' => $wgRequest->getInt('tbid'))); $wgTitle->invalidateCache(); - $wgOut->addWikiText(wfMsg('trackbackdeleteok')); + $wgOut->addWikiMsg('trackbackdeleteok'); } function render() { @@ -960,7 +948,7 @@ class Article { } } else { $msg = $wgOut->parse( wfMsg( 'confirm_purge' ) ); - $action = $this->mTitle->escapeLocalURL( 'action=purge' ); + $action = htmlspecialchars( $_SERVER['REQUEST_URI'] ); $button = htmlspecialchars( wfMsg( 'confirm_purge_button' ) ); $msg = str_replace( '$1', "<form method=\"post\" action=\"$action\">\n" . @@ -990,6 +978,15 @@ class Article { $update = SquidUpdate::newSimplePurge( $this->mTitle ); $update->doUpdate(); } + if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { + global $wgMessageCache; + if ( $this->getID() == 0 ) { + $text = false; + } else { + $text = $this->getContent(); + } + $wgMessageCache->replace( $this->mTitle->getDBkey(), $text ); + } $this->view(); } @@ -1200,10 +1197,11 @@ class Article { /** * @deprecated use Article::doEdit() */ - function insertNewArticle( $text, $summary, $isminor, $watchthis, $suppressRC=false, $comment=false ) { + function insertNewArticle( $text, $summary, $isminor, $watchthis, $suppressRC=false, $comment=false, $bot=false ) { $flags = EDIT_NEW | EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY | ( $isminor ? EDIT_MINOR : 0 ) | - ( $suppressRC ? EDIT_SUPPRESS_RC : 0 ); + ( $suppressRC ? EDIT_SUPPRESS_RC : 0 ) | + ( $bot ? EDIT_FORCE_BOT : 0 ); # If this is a comment, add the summary as headline if ( $comment && $summary != "" ) { @@ -1322,7 +1320,7 @@ class Article { # Silently ignore EDIT_MINOR if not allowed $isminor = ( $flags & EDIT_MINOR ) && $wgUser->isAllowed('minoredit'); - $bot = $wgUser->isAllowed( 'bot' ) || ( $flags & EDIT_FORCE_BOT ); + $bot = $flags & EDIT_FORCE_BOT; $oldtext = $this->getContent(); $oldsize = strlen( $oldtext ); @@ -1331,7 +1329,8 @@ class Article { if ($flags & EDIT_AUTOSUMMARY && $summary == '') $summary = $this->getAutosummary( $oldtext, $text, $flags ); - $text = $this->preSaveTransform( $text ); + $editInfo = $this->prepareTextForEdit( $text ); + $text = $editInfo->pst; $newsize = strlen( $text ); $dbw = wfGetDB( DB_MASTER ); @@ -1347,8 +1346,10 @@ class Article { $lastRevision = 0; $revisionId = 0; + + $changed = ( strcmp( $text, $oldtext ) != 0 ); - if ( 0 != strcmp( $text, $oldtext ) ) { + if ( $changed ) { $this->mGoodAdjustment = (int)$this->isCountable( $text ) - (int)$this->isCountable( $oldtext ); $this->mTotalAdjustment = 0; @@ -1413,9 +1414,8 @@ class Article { # 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. - $changed = ( strcmp( $oldtext, $text ) != 0 ); $this->editUpdates( $text, $summary, $isminor, $now, $revisionId, $changed ); } } else { @@ -1451,7 +1451,7 @@ class Article { $rcid = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $wgUser, $summary, $bot, '', strlen( $text ), $revisionId ); # Mark as patrolled if the user can - if( $GLOBALS['wgUseRCPatrol'] && $wgUser->isAllowed( 'autopatrol' ) ) { + if( ($GLOBALS['wgUseRCPatrol'] || $GLOBALS['wgUseNPPatrol']) && $wgUser->isAllowed( 'autopatrol' ) ) { RecentChange::markPatrolled( $rcid ); PatrolLog::record( $rcid, true ); } @@ -1509,28 +1509,42 @@ class Article { } /** - * Mark this particular edit as patrolled + * Mark this particular edit/page as patrolled */ function markpatrolled() { - global $wgOut, $wgRequest, $wgUseRCPatrol, $wgUser; + global $wgOut, $wgRequest, $wgUseRCPatrol, $wgUseNPPatrol, $wgUser; $wgOut->setRobotPolicy( 'noindex,nofollow' ); - # Check RC patrol config. option - if( !$wgUseRCPatrol ) { + # Check patrol config options + + if ( !($wgUseNPPatrol || $wgUseRCPatrol)) { $wgOut->errorPage( 'rcpatroldisabled', 'rcpatroldisabledtext' ); + return; + } + + # If we haven't been given an rc_id value, we can't do anything + $rcid = (int) $wgRequest->getVal('rcid'); + $rc = $rcid ? RecentChange::newFromId($rcid) : null; + if ( is_null ( $rc ) ) + { + $wgOut->errorPage( 'markedaspatrollederror', 'markedaspatrollederrortext' ); return; } - # Check permissions - if( !$wgUser->isAllowed( 'patrol' ) ) { - $wgOut->permissionRequired( 'patrol' ); + if ( !$wgUseRCPatrol && $rc->mAttribs['rc_type'] != RC_NEW) { + // Only new pages can be patrolled if the general patrolling is off....??? + // @fixme -- is this necessary? Shouldn't we only bother controlling the + // front end here? + $wgOut->errorPage( 'rcpatroldisabled', 'rcpatroldisabledtext' ); return; } + + # Check permissions + $permission_errors = $this->mTitle->getUserPermissionsErrors( 'patrol', $wgUser ); - # If we haven't been given an rc_id value, we can't do anything - $rcid = $wgRequest->getVal( 'rcid' ); - if( !$rcid ) { - $wgOut->errorPage( 'markedaspatrollederror', 'markedaspatrollederrortext' ); + if (count($permission_errors)>0) + { + $wgOut->showPermissionsErrorPage( $permission_errors ); return; } @@ -1539,7 +1553,10 @@ class Article { return; } - $return = SpecialPage::getTitleFor( 'Recentchanges' ); + #It would be nice to see where the user had actually come from, but for now just guess + $returnto = $rc->mAttribs['rc_type'] == RC_NEW ? 'Newpages' : 'Recentchanges'; + $return = Title::makeTitle( NS_SPECIAL, $returnto ); + # If it's left up to us, check that the user is allowed to patrol this edit # If the user has the "autopatrol" right, then we'll assume there are no # other conditions stopping them doing so @@ -1552,7 +1569,7 @@ class Article { # The user made this edit, and can't patrol it # Tell them so, and then back off $wgOut->setPageTitle( wfMsg( 'markedaspatrollederror' ) ); - $wgOut->addWikiText( wfMsgNoTrans( 'markedaspatrollederror-noautopatrol' ) ); + $wgOut->addWikiMsg( 'markedaspatrollederror-noautopatrol' ); $wgOut->returnToMain( false, $return ); return; } @@ -1565,7 +1582,7 @@ class Article { # Inform the user $wgOut->setPageTitle( wfMsg( 'markedaspatrolled' ) ); - $wgOut->addWikiText( wfMsgNoTrans( 'markedaspatrolledtext' ) ); + $wgOut->addWikiMsg( 'markedaspatrolledtext' ); $wgOut->returnToMain( false, $return ); } @@ -1590,9 +1607,7 @@ class Article { $wgOut->setPagetitle( wfMsg( 'addedwatch' ) ); $wgOut->setRobotpolicy( 'noindex,nofollow' ); - $link = wfEscapeWikiText( $this->mTitle->getPrefixedText() ); - $text = wfMsg( 'addedwatchtext', $link ); - $wgOut->addWikiText( $text ); + $wgOut->addWikiMsg( 'addedwatchtext', $this->mTitle->getPrefixedText() ); } $wgOut->returnToMain( true, $this->mTitle->getPrefixedText() ); @@ -1637,9 +1652,7 @@ class Article { $wgOut->setPagetitle( wfMsg( 'removedwatch' ) ); $wgOut->setRobotpolicy( 'noindex,nofollow' ); - $link = wfEscapeWikiText( $this->mTitle->getPrefixedText() ); - $text = wfMsg( 'removedwatchtext', $link ); - $wgOut->addWikiText( $text ); + $wgOut->addWikiMsg( 'removedwatchtext', $this->mTitle->getPrefixedText() ); } $wgOut->returnToMain( true, $this->mTitle->getPrefixedText() ); @@ -1690,7 +1703,7 @@ class Article { global $wgUser, $wgRestrictionTypes, $wgContLang; $id = $this->mTitle->getArticleID(); - if( !$wgUser->isAllowed( 'protect' ) || wfReadOnly() || $id == 0 ) { + if( array() != $this->mTitle->getUserPermissionsErrors( 'protect', $wgUser ) || wfReadOnly() || $id == 0 ) { return false; } @@ -1726,7 +1739,7 @@ class Article { $expiry_description = ''; if ( $encodedExpiry != 'infinity' ) { - $expiry_description = ' (' . wfMsgForContent( 'protect-expiring', $wgContLang->timeanddate( $expiry ) ).')'; + $expiry_description = ' (' . wfMsgForContent( 'protect-expiring', $wgContLang->timeanddate( $expiry, false, false ) ).')'; } # Prepare a null revision to be added to the history @@ -1757,10 +1770,8 @@ class Article { $comment .= "$expiry_description"; if ( $cascade ) $comment .= "$cascade_description"; - - $nullRevision = Revision::newNullRevision( $dbw, $id, $comment, true ); - $nullRevId = $nullRevision->insertOn( $dbw ); - + + $rowsAffected = false; # Update restrictions table foreach( $limit as $action => $restrictions ) { if ($restrictions != '' ) { @@ -1768,11 +1779,22 @@ class Article { array( 'pr_page' => $id, 'pr_type' => $action , 'pr_level' => $restrictions, 'pr_cascade' => $cascade ? 1 : 0 , 'pr_expiry' => $encodedExpiry ), __METHOD__ ); + if($dbw->affectedRows() != 0) + $rowsAffected = true; } else { $dbw->delete( 'page_restrictions', array( 'pr_page' => $id, 'pr_type' => $action ), __METHOD__ ); + if($dbw->affectedRows() != 0) + $rowsAffected = true; } } + if(!$rowsAffected) + // No change + return true; + + # Insert a null revision + $nullRevision = Revision::newNullRevision( $dbw, $id, $comment, true ); + $nullRevId = $nullRevision->insertOn( $dbw ); # Update page record $dbw->update( 'page', @@ -1788,6 +1810,8 @@ class Article { # Update the protection log $log = new LogPage( 'protect' ); + + if( $protect ) { $log->addEntry( $modified ? 'modify' : 'protect', $this->mTitle, trim( $reason . " [$updated]$cascade_description$expiry_description" ) ); @@ -1821,35 +1845,121 @@ class Article { } return implode( ':', $bits ); } + + /** + * Auto-generates a deletion reason + * @param bool &$hasHistory Whether the page has a history + */ + 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 + $limit = 20; + $res = $dbw->select('revision', 'rev_user_text', array('rev_page' => $this->getID()), __METHOD__, + array('LIMIT' => $limit)); + if($res === false) + // This page has no revisions, which is very weird + return false; + if($res->numRows() > 1) + $hasHistory = true; + else + $hasHistory = false; + $row = $dbw->fetchObject($res); + $onlyAuthor = $row->rev_user_text; + // Try to find a second contributor + while( $row = $dbw->fetchObject($res) ) { + if($row->rev_user_text != $onlyAuthor) { + $onlyAuthor = false; + break; + } + } + $dbw->freeResult($res); + + // 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'); + } + + // 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 */ function delete() { global $wgUser, $wgOut, $wgRequest; + $confirm = $wgRequest->wasPosted() && - $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ); - $reason = $wgRequest->getText( 'wpReason' ); + $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 .= ': ' . $this->DeleteReason; + } elseif ( $reason == 'other' ) { + $reason = $this->DeleteReason; + } # This code desperately needs to be totally rewritten - # Check permissions - if( $wgUser->isAllowed( 'delete' ) ) { - if( $wgUser->isBlocked( !$confirm ) ) { - $wgOut->blockedPage(); - return; - } - } else { - $wgOut->permissionRequired( 'delete' ); + # Read-only check... + if ( wfReadOnly() ) { + $wgOut->readOnlyPage(); return; } + + # Check permissions + $permission_errors = $this->mTitle->getUserPermissionsErrors( 'delete', $wgUser ); - if( wfReadOnly() ) { - $wgOut->readOnlyPage(); + if (count($permission_errors)>0) { + $wgOut->showPermissionsErrorPage( $permission_errors ); return; } - $wgOut->setPagetitle( wfMsg( 'confirmdelete' ) ); + $wgOut->setPagetitle( wfMsg( 'delete-confirm', $this->mTitle->getPrefixedText() ) ); # Better double-check that it hasn't been deleted yet! $dbw = wfGetDB( DB_MASTER ); @@ -1860,6 +1970,15 @@ class Article { return; } + # Hack for big sites + $bigHistory = $this->isBigDeletion(); + if( $bigHistory && !$this->mTitle->userCan( 'bigdelete' ) ) { + global $wgLang, $wgDeleteRevisionsLimit; + $wgOut->wrapWikiMsg( "<div class='error'>\n$1</div>\n", + array( 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ) ); + return; + } + if( $confirm ) { $this->doDelete( $reason ); if( $wgRequest->getCheck( 'wpWatch' ) ) { @@ -1870,77 +1989,47 @@ class Article { return; } - # determine whether this page has earlier revisions - # and insert a warning if it does - $maxRevisions = 20; - $authors = $this->getLastNAuthors( $maxRevisions, $latest ); + // Generate deletion reason + $hasHistory = false; + if ( !$reason ) $reason = $this->generateReason($hasHistory); - if( count( $authors ) > 1 && !$confirm ) { + // If the page has a history, insert a warning + if( $hasHistory && !$confirm ) { $skin=$wgUser->getSkin(); $wgOut->addHTML( '<strong>' . wfMsg( 'historywarning' ) . ' ' . $skin->historyLink() . '</strong>' ); - } - - # If a single user is responsible for all revisions, find out who they are - if ( count( $authors ) == $maxRevisions ) { - // Query bailed out, too many revisions to find out if they're all the same - $authorOfAll = false; - } else { - $authorOfAll = reset( $authors ); - foreach ( $authors as $author ) { - if ( $authorOfAll != $author ) { - $authorOfAll = false; - break; - } - } - } - # Fetch article text - $rev = Revision::newFromTitle( $this->mTitle ); - - if( !is_null( $rev ) ) { - # if this is a mini-text, we can paste part of it into the deletion reason - $text = $rev->getText(); - - #if this is empty, an earlier revision may contain "useful" text - $blanked = false; - if( $text == '' ) { - $prev = $rev->getPrevious(); - if( $prev ) { - $text = $prev->getText(); - $blanked = true; - } - } - - $length = strlen( $text ); - - # this should not happen, since it is not possible to store an empty, new - # page. Let's insert a standard text in case it does, though - if( $length == 0 && $reason === '' ) { - $reason = wfMsgForContent( 'exblank' ); - } - - if( $reason === '' ) { - # comment field=255, let's grep the first 150 to have some user - # space left - global $wgContLang; - $text = $wgContLang->truncate( $text, 150, '...' ); - - # let's strip out newlines - $text = preg_replace( "/[\n\r]/", '', $text ); - - if( !$blanked ) { - if( $authorOfAll === false ) { - $reason = wfMsgForContent( 'excontent', $text ); - } else { - $reason = wfMsgForContent( 'excontentauthor', $text, $authorOfAll ); - } - } else { - $reason = wfMsgForContent( 'exbeforeblank', $text ); - } + if( $bigHistory ) { + global $wgLang, $wgDeleteRevisionsLimit; + $wgOut->wrapWikiMsg( "<div class='error'>\n$1</div>\n", + array( 'delete-warning-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ) ); } } - + return $this->confirmDelete( '', $reason ); } + + /** + * @return bool whether or not the page surpasses $wgDeleteRevisionsLimit revisions + */ + function isBigDeletion() { + global $wgDeleteRevisionsLimit; + if( $wgDeleteRevisionsLimit ) { + $revCount = $this->estimateRevisionCount(); + return $revCount > $wgDeleteRevisionsLimit; + } + return false; + } + + /** + * @return int approximate revision count + */ + function estimateRevisionCount() { + $dbr = wfGetDB(); + // 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 @@ -1990,51 +2079,59 @@ class Article { /** * Output deletion confirmation dialog + * @param $par string FIXME: do we need this parameter? One Call from Article::delete with '' only. + * @param $reason string Prefilled reason */ function confirmDelete( $par, $reason ) { - global $wgOut, $wgUser; + global $wgOut, $wgUser, $wgContLang; + $align = $wgContLang->isRtl() ? 'left' : 'right'; wfDebug( "Article::confirmDelete\n" ); - $sub = htmlspecialchars( $this->mTitle->getPrefixedText() ); - $wgOut->setSubtitle( wfMsg( 'deletesub', $sub ) ); + $wgOut->setSubtitle( wfMsg( 'delete-backlink', $wgUser->getSkin()->makeKnownLinkObj( $this->mTitle ) ) ); $wgOut->setRobotpolicy( 'noindex,nofollow' ); - $wgOut->addWikiText( wfMsg( 'confirmdeletetext' ) ); - - $formaction = $this->mTitle->escapeLocalURL( 'action=delete' . $par ); - - $confirm = htmlspecialchars( wfMsg( 'deletepage' ) ); - $delcom = htmlspecialchars( wfMsg( 'deletecomment' ) ); - $token = htmlspecialchars( $wgUser->editToken() ); - $watch = Xml::checkLabel( wfMsg( 'watchthis' ), 'wpWatch', 'wpWatch', $wgUser->getBoolOption( 'watchdeletion' ) || $this->mTitle->userIsWatching(), array( 'tabindex' => '2' ) ); - - $wgOut->addHTML( " -<form id='deleteconfirm' method='post' action=\"{$formaction}\"> - <table border='0'> - <tr> - <td align='right'> - <label for='wpReason'>{$delcom}:</label> - </td> - <td align='left'> - <input type='text' size='60' name='wpReason' id='wpReason' value=\"" . htmlspecialchars( $reason ) . "\" tabindex=\"1\" /> - </td> - </tr> - <tr> - <td> </td> - <td>$watch</td> - </tr> - <tr> - <td> </td> - <td> - <input type='submit' name='wpConfirmB' id='wpConfirmB' value=\"{$confirm}\" tabindex=\"3\" /> - </td> - </tr> - </table> - <input type='hidden' name='wpEditToken' value=\"{$token}\" /> -</form>\n" ); - - $wgOut->returnToMain( false ); - + $wgOut->addWikiMsg( 'confirmdeletetext' ); + + $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->mTitle->getLocalURL( 'action=delete' . $par ), 'id' => 'deleteconfirm' ) ) . + Xml::openElement( 'fieldset' ) . + Xml::element( 'legend', array(), wfMsg( 'delete-legend' ) ) . + Xml::openElement( 'table' ) . + "<tr id=\"wpDeleteReasonListRow\"> + <td align='$align'>" . + Xml::label( wfMsg( 'deletecomment' ), 'wpDeleteReasonList' ) . + "</td> + <td>" . + Xml::listDropDown( 'wpDeleteReasonList', + wfMsgForContent( 'deletereason-dropdown' ), + wfMsgForContent( 'deletereasonotherlist' ), '', 'wpReasonDropDown', 1 ) . + "</td> + </tr> + <tr id=\"wpDeleteReasonRow\"> + <td align='$align'>" . + Xml::label( wfMsg( 'deleteotherreason' ), 'wpReason' ) . + "</td> + <td>" . + Xml::input( 'wpReason', 60, $reason, array( 'type' => 'text', 'maxlength' => '255', 'tabindex' => '2', 'id' => 'wpReason' ) ) . + "</td> + </tr> + <tr> + <td></td> + <td>" . + Xml::checkLabel( wfMsg( 'watchthis' ), 'wpWatch', 'wpWatch', $wgUser->getBoolOption( 'watchdeletion' ) || $this->mTitle->userIsWatching(), array( 'tabindex' => '3' ) ) . + "</td> + </tr> + <tr> + <td></td> + <td>" . + Xml::submitButton( wfMsg( 'deletepage' ), array( 'name' => 'wpConfirmB', 'id' => 'wpConfirmB', 'tabindex' => '4' ) ) . + "</td> + </tr>" . + Xml::closeElement( 'table' ) . + Xml::closeElement( 'fieldset' ) . + Xml::hidden( 'wpEditToken', $wgUser->editToken() ) . + Xml::closeElement( 'form' ); + + $wgOut->addHTML( $form ); $this->showLogExtract( $wgOut ); } @@ -2062,15 +2159,14 @@ class Article { if (wfRunHooks('ArticleDelete', array(&$this, &$wgUser, &$reason))) { if ( $this->doDeleteArticle( $reason ) ) { - $deleted = wfEscapeWikiText( $this->mTitle->getPrefixedText() ); + $deleted = $this->mTitle->getPrefixedText(); $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) ); $wgOut->setRobotpolicy( 'noindex,nofollow' ); - $loglink = '[[Special:Log/delete|' . wfMsg( 'deletionlog' ) . ']]'; - $text = wfMsg( 'deletedtext', $deleted, $loglink ); + $loglink = '[[Special:Log/delete|' . wfMsgNoTrans( 'deletionlog' ) . ']]'; - $wgOut->addWikiText( $text ); + $wgOut->addWikiMsg( 'deletedtext', $deleted, $loglink ); $wgOut->returnToMain( false ); wfRunHooks('ArticleDeleteComplete', array(&$this, &$wgUser, $reason)); } else { @@ -2180,50 +2276,76 @@ class Article { /** * 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 + * 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 string $fromP - Name of the user whose edits to rollback. * @param string $summary - Custom summary. Set to default summary if empty. * @param string $token - Rollback token. - * @param bool $bot - If true, mark all reverted edits as bot. + * @param bool $bot - If true, mark all reverted edits as bot. * - * @param array $resultDetails contains result-specific dict of additional values - * ALREADY_ROLLED : 'current' (rev) - * SUCCESS : 'summary' (str), 'current' (rev), 'target' (rev) + * @param array $resultDetails contains result-specific array of additional values + * 'alreadyrolled' : 'current' (rev) + * success : 'summary' (str), 'current' (rev), 'target' (rev) * - * @return self::SUCCESS on succes, self::* on failure + * @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, $wgUseRCPatrol; + global $wgUser; $resultDetails = null; - - if( $wgUser->isAllowed( 'rollback' ) ) { - if( $wgUser->isBlocked() ) { - return self::BLOCKED; - } - } else { - return self::PERM_DENIED; - } - - if ( wfReadOnly() ) { - return self::READONLY; - } + + # Check permissions + $errors = array_merge( $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser ), + $this->mTitle->getUserPermissionsErrors( 'rollback', $wgUser ) ); if( !$wgUser->matchEditToken( $token, array( $this->mTitle->getPrefixedText(), $fromP ) ) ) - return self::BAD_TOKEN; + $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; $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 self::BAD_TITLE; + return array(array('notanarticle')); } $from = str_replace( '_', ' ', $fromP ); if( $from != $current->getUserText() ) { $resultDetails = array( 'current' => $current ); - return self::ALREADY_ROLLED; + return array(array('alreadyrolled', + htmlspecialchars($this->mTitle->getPrefixedText()), + htmlspecialchars($fromP), + htmlspecialchars($current->getUserText()) + )); } # Get the last edit not by this guy @@ -2231,21 +2353,19 @@ class Article { $user_text = $dbw->addQuotes( $current->getUserText() ); $s = $dbw->selectRow( 'revision', array( 'rev_id', 'rev_timestamp' ), - array( - 'rev_page' => $current->getPage(), + array( 'rev_page' => $current->getPage(), "rev_user <> {$user} OR rev_user_text <> {$user_text}" ), __METHOD__, - array( - 'USE INDEX' => 'page_timestamp', + array( 'USE INDEX' => 'page_timestamp', 'ORDER BY' => 'rev_timestamp DESC' ) ); if( $s === false ) { - # Something wrong - return self::ONLY_AUTHOR; + # No one else ever edited this page + return array(array('cantrollback')); } $set = array(); - if ( $bot ) { + if ( $bot && $wgUser->isAllowed('markbotedits') ) { # Mark all reverted edits as bot $set['rc_bot'] = 1; } @@ -2264,23 +2384,36 @@ class Article { ); } - # Get the edit summary + # Generate the edit summary if necessary $target = Revision::newFromId( $s->rev_id ); if( empty( $summary ) ) - $summary = wfMsgForContent( 'revertpage', $target->getUserText(), $from ); + { + global $wgLang; + $summary = wfMsgForContent( 'revertpage', + $target->getUserText(), $from, + $s->rev_id, $wgLang->timeanddate(wfTimestamp(TS_MW, $s->rev_timestamp), true), + $current->getId(), $wgLang->timeanddate($current->getTimestamp()) + ); + } # Save - $flags = EDIT_UPDATE | EDIT_MINOR; - if( $bot ) + $flags = EDIT_UPDATE; + + if ($wgUser->isAllowed('minoredit')) + $flags |= EDIT_MINOR; + + if( $bot && ($wgUser->isAllowed('markbotedits') || $wgUser->isAllowed('bot')) ) $flags |= EDIT_FORCE_BOT; $this->doEdit( $target->getText(), $summary, $flags ); + wfRunHooks( 'ArticleRollbackComplete', array( $this, $wgUser, $target ) ); + $resultDetails = array( 'summary' => $summary, 'current' => $current, 'target' => $target, ); - return self::SUCCESS; + return array(); } /** @@ -2288,8 +2421,8 @@ class Article { */ function rollback() { global $wgUser, $wgOut, $wgRequest, $wgUseRCPatrol; - $details = null; + $result = $this->doRollback( $wgRequest->getVal( 'from' ), $wgRequest->getText( 'summary' ), @@ -2298,58 +2431,44 @@ class Article { $details ); - switch( $result ) { - case self::BLOCKED: - $wgOut->blockedPage(); - break; - case self::PERM_DENIED: - $wgOut->permissionRequired( 'rollback' ); - break; - case self::READONLY: - $wgOut->readOnlyPage( $this->getContent() ); - break; - case self::BAD_TOKEN: - $wgOut->setPageTitle( wfMsg( 'rollbackfailed' ) ); - $wgOut->addWikiText( wfMsg( 'sessionfailure' ) ); - break; - case self::BAD_TITLE: - $wgOut->addHtml( wfMsg( 'notanarticle' ) ); - break; - case self::ALREADY_ROLLED: - $current = $details['current']; - $wgOut->setPageTitle( wfMsg( 'rollbackfailed' ) ); - $wgOut->addWikiText( - wfMsg( 'alreadyrolled', - htmlspecialchars( $this->mTitle->getPrefixedText() ), - htmlspecialchars( $wgRequest->getVal( 'from' ) ), - htmlspecialchars( $current->getUserText() ) - ) - ); - if( $current->getComment() != '' ) { - $wgOut->addHtml( wfMsg( 'editcomment', - $wgUser->getSkin()->formatComment( $current->getComment() ) ) ); + if( in_array( array( 'blocked' ), $result ) ) { + $wgOut->blockedPage(); + return; + } + if( in_array( array( 'actionthrottledtext' ), $result ) ) { + $wgOut->rateLimited(); + 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. Re- + # move any 'readonlytext' error manually. + $out = array(); + foreach( $result as $error ) { + if( $error != array( 'readonlytext' ) ) { + $out []= $error; } - break; - case self::ONLY_AUTHOR: - $wgOut->setPageTitle( wfMsg( 'rollbackfailed' ) ); - $wgOut->addHtml( wfMsg( 'cantrollback' ) ); - break; - case self::SUCCESS: - $current = $details['current']; - $target = $details['target']; - $wgOut->setPageTitle( wfMsg( 'actioncomplete' ) ); - $wgOut->setRobotPolicy( 'noindex,nofollow' ); - $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 ); - break; - default: - throw new MWException( __METHOD__ . ": Unknown return value `{$result}`" ); + } + $wgOut->showPermissionsErrorPage( $out ); + return; + } + if( $result == array( array('readonlytext') ) ) { + $wgOut->readOnlyPage(); + return; } + $current = $details['current']; + $target = $details['target']; + $wgOut->setPageTitle( wfMsg( 'actioncomplete' ) ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); + $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 ); } @@ -2375,6 +2494,29 @@ class Article { } /** + * Prepare text which is about to be saved. + * Returns a stdclass with source, pst and output members + */ + function prepareTextForEdit( $text, $revid=null ) { + if ( $this->mPreparedEdit && $this->mPreparedEdit->newText == $text && $this->mPreparedEdit->revid == $revid) { + // Already prepared + return $this->mPreparedEdit; + } + global $wgParser; + $edit = (object)array(); + $edit->revid = $revid; + $edit->newText = $text; + $edit->pst = $this->preSaveTransform( $text ); + $options = new ParserOptions; + $options->setTidy( true ); + $options->enableLimitReport(); + $edit->output = $wgParser->parse( $edit->pst, $this->mTitle, $options, true, true, $revid ); + $edit->oldText = $this->getContent(); + $this->mPreparedEdit = $edit; + return $edit; + } + + /** * Do standard deferred updates after page edit. * Update links tables, site stats, search index and message cache. * Every 100th edit, prune the recent changes table. @@ -2388,21 +2530,28 @@ class Article { * @param $changed Whether or not the content actually changed */ function editUpdates( $text, $summary, $minoredit, $timestamp_of_pagechange, $newid, $changed = true ) { - global $wgDeferredUpdateList, $wgMessageCache, $wgUser, $wgParser; + global $wgDeferredUpdateList, $wgMessageCache, $wgUser, $wgParser, $wgEnableParserCache; wfProfileIn( __METHOD__ ); # Parse the text - $options = new ParserOptions; - $options->setTidy(true); - $poutput = $wgParser->parse( $text, $this->mTitle, $options, true, true, $newid ); + # 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 ); + } else { + wfDebug( __METHOD__ . ": No vary-revision, using prepared edit...\n" ); + $editInfo = $this->mPreparedEdit; + } # Save it to the parser cache - $parserCache =& ParserCache::singleton(); - $parserCache->save( $poutput, $this, $wgUser ); + if ( $wgEnableParserCache ) { + $parserCache =& ParserCache::singleton(); + $parserCache->save( $editInfo->output, $this, $wgUser ); + } # Update the links tables - $u = new LinksUpdate( $this->mTitle, $poutput ); + $u = new LinksUpdate( $this->mTitle, $editInfo->output ); $u->doUpdate(); if( wfRunHooks( 'ArticleEditUpdatesDeleteFromRecentchanges', array( &$this ) ) ) { @@ -2756,6 +2905,7 @@ class Article { $title->touchLinks(); $title->purgeSquid(); + $title->deleteTitleProtection(); } static function onArticleDelete( $title ) { @@ -2773,6 +2923,10 @@ class Article { if( $title->getNamespace() == NS_MEDIAWIKI) { $wgMessageCache->replace( $title->getDBkey(), false ); } + if( $title->getNamespace() == NS_IMAGE ) { + $update = new HTMLCacheUpdate( $title, 'imagelinks' ); + $update->doUpdate(); + } } /** @@ -2782,9 +2936,11 @@ class Article { global $wgDeferredUpdateList, $wgUseFileCache; // Invalidate caches of articles which include this page - $update = new HTMLCacheUpdate( $title, 'templatelinks' ); - $wgDeferredUpdateList[] = $update; + $wgDeferredUpdateList[] = new HTMLCacheUpdate( $title, 'templatelinks' ); + // Invalidate the caches of all pages which redirect here + $wgDeferredUpdateList[] = new HTMLCacheUpdate( $title, 'redirect' ); + # Purge squid for this page only $title->purgeSquid(); @@ -3009,14 +3165,16 @@ class Article { * @param bool $cache */ public function outputWikiText( $text, $cache = true ) { - global $wgParser, $wgUser, $wgOut; + global $wgParser, $wgUser, $wgOut, $wgEnableParserCache; $popts = $wgOut->parserOptions(); $popts->setTidy(true); + $popts->enableLimitReport(); $parserOutput = $wgParser->parse( $text, $this->mTitle, $popts, true, true, $this->getRevIdFetched() ); $popts->setTidy(false); - if ( $cache && $this && $parserOutput->getCacheTime() != -1 ) { + $popts->enableLimitReport( false ); + if ( $wgEnableParserCache && $cache && $this && $parserOutput->getCacheTime() != -1 ) { $parserCache =& ParserCache::singleton(); $parserCache->save( $parserOutput, $this, $wgUser ); } |