X-Git-Url: https://git.heureux-cyclage.org/?a=blobdiff_plain;f=includes%2FArticle.php;h=10ff07574a76c62e539d94945361c083cd7de500;hb=fc62cc17c5df2d838a3825a87e052f699b17363b;hp=473e01d4ccb56c6e26d87474acb95ea8cc872e21;hpb=e0320af09e4a42ccf7715a66a8acd0dccc8af498;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/Article.php b/includes/Article.php index 473e01d4cc..10ff07574a 100644 --- a/includes/Article.php +++ b/includes/Article.php @@ -36,25 +36,12 @@ class Article { var $mUserText; //!< /**@}}*/ - /** - * 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 - const RATE_LIMITED = 8; - /** * 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(); @@ -151,7 +138,7 @@ class Article { * @return Return the text of this revision */ function getContent() { - global $wgUser, $wgOut; + global $wgUser, $wgOut, $wgMessageCache; wfProfileIn( __METHOD__ ); @@ -160,6 +147,7 @@ class Article { $wgOut->setRobotpolicy( 'noindex,nofollow' ); if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { + $wgMessageCache->loadAllMessages(); $ret = wfMsgWeirdKey ( $this->mTitle->getText() ) ; } else { $ret = wfMsg( $wgUser->isLoggedIn() ? 'noarticletext' : 'noarticletextanon' ); @@ -390,8 +378,7 @@ class Article { // FIXME: Horrible, horrible! This content-loading interface just plain sucks. // We should instead work with the Revision object when we need it... - $this->mContent = $revision->userCan( Revision::DELETED_TEXT ) ? $revision->getRawText() : ""; - //$this->mContent = $revision->getText(); + $this->mContent = $revision->revText(); // Loads if user is allowed $this->mUser = $revision->getUser(); $this->mUserText = $revision->getUserText(); @@ -626,6 +613,7 @@ class Article { global $wgUser, $wgOut, $wgRequest, $wgContLang; global $wgEnableParserCache, $wgStylePath, $wgParser; global $wgUseTrackbacks, $wgNamespaceRobotPolicies, $wgArticleRobotPolicies; + global $wgDefaultRobotPolicy; $sk = $wgUser->getSkin(); wfProfileIn( __METHOD__ ); @@ -660,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 ); @@ -783,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... } } @@ -865,7 +852,7 @@ 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 @@ -917,14 +904,14 @@ 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; } @@ -939,7 +926,7 @@ class Article { $db = wfGetDB(DB_MASTER); $db->delete('trackbacks', array('tb_id' => $wgRequest->getInt('tbid'))); $wgTitle->invalidateCache(); - $wgOut->addWikiText(wfMsg('trackbackdeleteok')); + $wgOut->addWikiMsg('trackbackdeleteok'); } function render() { @@ -991,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(); } @@ -1071,7 +1067,6 @@ class Article { $result = $dbw->affectedRows() != 0; if ($result) { - // FIXME: Should the result from updateRedirectOn() be returned instead? $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect ); } @@ -1201,10 +1196,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 != "" ) { @@ -1323,7 +1319,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 ); @@ -1498,6 +1494,7 @@ class Article { * * @param boolean $noRedir Add redirect=no * @param string $sectionAnchor section to redirect to, including "#" + * @param string $extraq, extra query params */ function doRedirect( $noRedir = false, $sectionAnchor = '', $extraq = '' ) { global $wgOut; @@ -1534,7 +1531,7 @@ class Article { return; } - if ( !$wgUseRCPatrol && $rc->mAttribs['rc_type'] != RC_NEW) { + if ( !$wgUseRCPatrol && $rc->getAttribute( '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? @@ -1557,7 +1554,7 @@ class Article { } #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'; + $returnto = $rc->getAttribute( '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 @@ -1572,20 +1569,24 @@ 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; } } - # Mark the edit as patrolled - RecentChange::markPatrolled( $rcid ); - PatrolLog::record( $rcid ); - wfRunHooks( 'MarkPatrolledComplete', array( &$rcid, &$wgUser, false ) ); + # Check that the revision isn't patrolled already + # Prevents duplicate log entries + if( !$rc->getAttribute( 'rc_patrolled' ) ) { + # Mark the edit as patrolled + RecentChange::markPatrolled( $rcid ); + PatrolLog::record( $rcid ); + wfRunHooks( 'MarkPatrolledComplete', array( &$rcid, &$wgUser, false ) ); + } # Inform the user $wgOut->setPageTitle( wfMsg( 'markedaspatrolled' ) ); - $wgOut->addWikiText( wfMsgNoTrans( 'markedaspatrolledtext' ) ); + $wgOut->addWikiMsg( 'markedaspatrolledtext' ); $wgOut->returnToMain( false, $return ); } @@ -1610,9 +1611,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() ); @@ -1657,9 +1656,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() ); @@ -1746,7 +1743,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 @@ -1761,7 +1758,8 @@ class Article { foreach( $limit as $action => $restrictions ) { # Check if the group level required to edit also can protect pages # Otherwise, people who cannot normally protect can "protect" pages via transclusion - $cascade = ( $cascade && isset($wgGroupPermissions[$restrictions]['protect']) && $wgGroupPermissions[$restrictions]['protect'] ); + $cascade = ( $cascade && isset($wgGroupPermissions[$restrictions]['protect']) && + $wgGroupPermissions[$restrictions]['protect'] ); } $cascade_description = ''; @@ -1777,10 +1775,7 @@ class Article { $comment .= "$expiry_description"; if ( $cascade ) $comment .= "$cascade_description"; - - $nullRevision = Revision::newNullRevision( $dbw, $id, $comment, true ); - $nullRevId = $nullRevision->insertOn( $dbw ); - + # Update restrictions table foreach( $limit as $action => $restrictions ) { if ($restrictions != '' ) { @@ -1794,6 +1789,10 @@ class Article { } } + # Insert a null revision + $nullRevision = Revision::newNullRevision( $dbw, $id, $comment, true ); + $nullRevId = $nullRevision->insertOn( $dbw ); + # Update page record $dbw->update( 'page', array( /* SET */ @@ -1808,6 +1807,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" ) ); @@ -1938,9 +1939,17 @@ class Article { } elseif ( $reason == 'other' ) { $reason = $this->DeleteReason; } + # Flag to hide all contents of the archived revisions + $suppress = $wgRequest->getVal( 'wpSuppress' ) && $wgUser->isAllowed('deleterevision'); # 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 ); @@ -1949,7 +1958,7 @@ class Article { 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 ); @@ -1960,8 +1969,17 @@ class Article { return; } + # Hack for big sites + $bigHistory = $this->isBigDeletion(); + if( $bigHistory && !$this->mTitle->userCan( 'bigdelete' ) ) { + global $wgLang, $wgDeleteRevisionsLimit; + $wgOut->wrapWikiMsg( "
\n$1
\n", + array( 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ) ); + return; + } + if( $confirm ) { - $this->doDelete( $reason ); + $this->doDelete( $reason, $suppress ); if( $wgRequest->getCheck( 'wpWatch' ) ) { $this->doWatch(); } elseif( $this->mTitle->userIsWatching() ) { @@ -1978,10 +1996,39 @@ class Article { if( $hasHistory && !$confirm ) { $skin=$wgUser->getSkin(); $wgOut->addHTML( '' . wfMsg( 'historywarning' ) . ' ' . $skin->historyLink() . '' ); + if( $bigHistory ) { + global $wgLang, $wgDeleteRevisionsLimit; + $wgOut->wrapWikiMsg( "
\n$1
\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 @@ -2031,96 +2078,74 @@ 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' ) ); + $wgOut->addWikiMsg( 'confirmdeletetext' ); - $formaction = $this->mTitle->escapeLocalURL( 'action=delete' . $par ); - - $confirm = htmlspecialchars( wfMsg( 'deletepage' ) ); - $delcom = Xml::label( wfMsg( 'deletecomment' ), 'wpDeleteReasonList' ); - $token = htmlspecialchars( $wgUser->editToken() ); - $watch = Xml::checkLabel( wfMsg( 'watchthis' ), 'wpWatch', 'wpWatch', $wgUser->getBoolOption( 'watchdeletion' ) || $this->mTitle->userIsWatching(), array( 'tabindex' => '2' ) ); - - $mDeletereasonother = Xml::label( wfMsg( 'deleteotherreason' ), 'wpReason' ); - $mDeletereasonotherlist = wfMsgHtml( 'deletereasonotherlist' ); - $scDeleteReasonList = wfMsgForContent( 'deletereason-dropdown' ); - - $deleteReasonList = ''; - if ( $scDeleteReasonList != '' && $scDeleteReasonList != '-' ) { - $deleteReasonList = ""; - $optgroup = ""; - foreach ( explode( "\n", $scDeleteReasonList ) as $option) { - $value = trim( htmlspecialchars($option) ); - if ( $value == '' ) { - continue; - } elseif ( substr( $value, 0, 1) == '*' && substr( $value, 1, 1) != '*' ) { - // A new group is starting ... - $value = trim( substr( $value, 1 ) ); - $deleteReasonList .= "$optgroup"; - $optgroup = ""; - } elseif ( substr( $value, 0, 2) == '**' ) { - // groupmember - $selected = ""; - $value = trim( substr( $value, 2 ) ); - if ( $this->DeleteReasonList === $value) - $selected = ' selected="selected"'; - $deleteReasonList .= ""; - } else { - // groupless delete reason - $selected = ""; - if ( $this->DeleteReasonList === $value) - $selected = ' selected="selected"'; - $deleteReasonList .= "$optgroup"; - $optgroup = ""; - } + if( $wgUser->isAllowed( 'deleterevision' ) ) { + $suppress = ""; + $suppress .= Xml::checkLabel( wfMsg( 'revdelete-suppress' ), 'wpSuppress', 'wpSuppress', false, array( 'tabindex' => '2' ) ); + $suppress .= ""; + } else { + $suppress = ''; + } + + $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->mTitle->getLocalURL( 'action=delete' . $par ), 'id' => 'deleteconfirm' ) ) . + Xml::openElement( 'fieldset', array( 'id' => 'mw-delete-table' ) ) . + Xml::element( 'legend', null, wfMsg( 'delete-legend' ) ) . + Xml::openElement( 'table' ) . + " + " . + Xml::label( wfMsg( 'deletecomment' ), 'wpDeleteReasonList' ) . + " + " . + Xml::listDropDown( 'wpDeleteReasonList', + wfMsgForContent( 'deletereason-dropdown' ), + wfMsgForContent( 'deletereasonotherlist' ), '', 'wpReasonDropDown', 1 ) . + " + + + " . + Xml::label( wfMsg( 'deleteotherreason' ), 'wpReason' ) . + " + " . + Xml::input( 'wpReason', 60, $reason, array( 'type' => 'text', 'maxlength' => '255', 'tabindex' => '2', 'id' => 'wpReason' ) ) . + " + + + + " . + Xml::checkLabel( wfMsg( 'watchthis' ), 'wpWatch', 'wpWatch', $wgUser->getBoolOption( 'watchdeletion' ) || $this->mTitle->userIsWatching(), array( 'tabindex' => '3' ) ) . + " + + $suppress + + + " . + Xml::submitButton( wfMsg( 'deletepage' ), array( 'name' => 'wpConfirmB', 'id' => 'wpConfirmB', 'tabindex' => '4' ) ) . + " + " . + Xml::closeElement( 'table' ) . + Xml::closeElement( 'fieldset' ) . + Xml::hidden( 'wpEditToken', $wgUser->editToken() ) . + Xml::closeElement( 'form' ); + + if ( $wgUser->isAllowed( 'editinterface' ) ) { + $skin = $wgUser->getSkin(); + $link = $skin->makeLink ( 'MediaWiki:Deletereason-dropdown', wfMsgHtml( 'delete-edit-reasonlist' ) ); + $form .= '

' . $link . '

'; } - $deleteReasonList .= $optgroup; - } - $wgOut->addHTML( " -
- - - - - - - - - - - - - - - - - -
- $delcom: - - -
- $mDeletereasonother - - -
 $watch
  - -
- -
\n" ); - - $wgOut->returnToMain( false, $this->mTitle ); + $wgOut->addHTML( $form ); $this->showLogExtract( $wgOut ); } @@ -2129,7 +2154,7 @@ class Article { * Show relevant lines from the deletion log */ function showLogExtract( $out ) { - $out->addHtml( '

' . htmlspecialchars( LogPage::logName( 'delete' ) ) . '

' ); + $out->addHtml( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) ); $logViewer = new LogViewer( new LogReader( new FauxRequest( @@ -2142,25 +2167,24 @@ class Article { /** * Perform a deletion and output success or failure messages */ - function doDelete( $reason ) { + function doDelete( $reason, $suppress = false ) { global $wgOut, $wgUser; wfDebug( __METHOD__."\n" ); if (wfRunHooks('ArticleDelete', array(&$this, &$wgUser, &$reason))) { - if ( $this->doDeleteArticle( $reason ) ) { - $deleted = wfEscapeWikiText( $this->mTitle->getPrefixedText() ); + if ( $this->doDeleteArticle( $reason, $suppress ) ) { + $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 { - $wgOut->showFatalError( wfMsg( 'cannotdelete' ) ); + $wgOut->showFatalError( wfMsg( 'cannotdelete' ).'
'.wfMsg('cannotdelete-merge') ); } } } @@ -2170,7 +2194,7 @@ class Article { * Deletes the article with database consistency, writes logs, purges caches * Returns success */ - function doDeleteArticle( $reason ) { + function doDeleteArticle( $reason, $suppress = false ) { global $wgUseSquid, $wgDeferredUpdateList; global $wgUseTrackbacks; @@ -2188,6 +2212,18 @@ class Article { $u = new SiteStatsUpdate( 0, 1, -(int)$this->isCountable( $this->getContent() ), -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'; + } + // 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 @@ -2211,8 +2247,9 @@ class Article { 'ar_text_id' => 'rev_text_id', 'ar_text' => '\'\'', // Be explicit to appease 'ar_flags' => '\'\'', // MySQL's "strict mode"... - 'ar_len' => 'rev_len', + 'ar_len' => 'rev_len', 'ar_page_id' => 'page_id', + 'ar_deleted' => $bitfield ), array( 'page_id' => $id, 'page_id = rev_page' @@ -2222,12 +2259,20 @@ class Article { # Delete restrictions for it $dbw->delete( 'page_restrictions', array ( 'pr_page' => $id ), __METHOD__ ); + # 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 ); + # Now that it's safely backed up, delete it $dbw->delete( 'page', array( 'page_id' => $id ), __METHOD__); # If using cascading deletes, we can skip some explicit deletes if ( !$dbw->cascadingDeletes() ) { - $dbw->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ ); if ($wgUseTrackbacks) @@ -2253,8 +2298,9 @@ class Article { # Clear caches Article::onArticleDelete( $this->mTitle ); - # Log the deletion - $log = new LogPage( 'delete' ); + # Log the deletion, if the page was suppressed, log it at Oversight instead + $logtype = $suppress ? 'oversight' : 'delete'; + $log = new LogPage( $logtype ); $log->addEntry( 'delete', $this->mTitle, $reason ); # Clear the cached article id so the interface doesn't act like we exist @@ -2266,75 +2312,95 @@ 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; - # Just in case it's being called from elsewhere - - if( $wgUser->isAllowed( 'rollback' ) && $this->mTitle->userCan( 'edit' ) ) { - 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() ) { - return self::RATE_LIMITED; + $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 $user = intval( $current->getUser() ); $user_text = $dbw->addQuotes( $current->getUserText() ); $s = $dbw->selectRow( 'revision', - array( 'rev_id', 'rev_timestamp' ), - array( - 'rev_page' => $current->getPage(), + 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', + 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')); + } 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(); @@ -2357,10 +2423,17 @@ 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; @@ -2368,7 +2441,7 @@ class Article { if ($wgUser->isAllowed('minoredit')) $flags |= EDIT_MINOR; - if( $bot ) + if( $bot && ($wgUser->isAllowed('markbotedits') || $wgUser->isAllowed('bot')) ) $flags |= EDIT_FORCE_BOT; $this->doEdit( $target->getText(), $summary, $flags ); @@ -2379,7 +2452,7 @@ class Article { 'current' => $current, 'target' => $target, ); - return self::SUCCESS; + return array(); } /** @@ -2387,19 +2460,8 @@ class Article { */ function rollback() { global $wgUser, $wgOut, $wgRequest, $wgUseRCPatrol; - $details = null; - # Skip the permissions-checking in doRollback() itself, by checking permissions here. - - $perm_errors = array_merge( $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser ), - $this->mTitle->getUserPermissionsErrors( 'rollback', $wgUser ) ); - - if (count($perm_errors)) { - $wgOut->showPermissionsErrorPage( $perm_errors ); - return; - } - $result = $this->doRollback( $wgRequest->getVal( 'from' ), $wgRequest->getText( 'summary' ), @@ -2408,61 +2470,57 @@ 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: + if( in_array( array( 'blocked' ), $result ) ) { + $wgOut->blockedPage(); + return; + } + 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']; - $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() ) ) ); + $wgOut->addWikiMsgArray( 'editcomment', array( $wgUser->getSkin()->formatComment( $current->getComment() ) ), array( 'replaceafter' ) ); } - break; - case self::ONLY_AUTHOR: - $wgOut->setPageTitle( wfMsg( 'rollbackfailed' ) ); - $wgOut->addHtml( wfMsg( 'cantrollback' ) ); - break; - case self::RATE_LIMITED: - $wgOut->rateLimited(); - 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}`" ); + } + 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; + } + } + $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 ); } @@ -2524,7 +2582,7 @@ 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__ ); @@ -2539,8 +2597,10 @@ class Article { } # Save it to the parser cache - $parserCache =& ParserCache::singleton(); - $parserCache->save( $editInfo->output, $this, $wgUser ); + if ( $wgEnableParserCache ) { + $parserCache =& ParserCache::singleton(); + $parserCache->save( $editInfo->output, $this, $wgUser ); + } # Update the links tables $u = new LinksUpdate( $this->mTitle, $editInfo->output ); @@ -2638,7 +2698,7 @@ class Article { $sk = $wgUser->getSkin(); $lnk = $current ? wfMsg( 'currentrevisionlink' ) - : $lnk = $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'currentrevisionlink' ) ); + : $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'currentrevisionlink' ) ); $curdiff = $current ? wfMsg( 'diff' ) : $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'diff' ), 'diff=cur&oldid='.$oldid ); @@ -2656,8 +2716,29 @@ class Article { ? wfMsg( 'diff' ) : $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'diff' ), 'diff=next&oldid='.$oldid ); - $userlinks = $sk->userLink( $revision->getUser(), $revision->getUserText() ) - . $sk->userToolLinks( $revision->getUser(), $revision->getUserText() ); + $cdel=''; + if( $wgUser->isAllowed( 'deleterevision' ) ) { + $revdel = SpecialPage::getTitleFor( 'Revisiondelete' ); + if( $revision->isCurrent() ) { + // We don't handle top deleted edits too well + $cdel = wfMsgHtml('rev-delundel'); + } else if( !$revision->userCan( Revision::DELETED_RESTRICTED ) ) { + // If revision was hidden from sysops + $cdel = wfMsgHtml('rev-delundel'); + } else { + $cdel = $sk->makeKnownLinkObj( $revdel, + wfMsgHtml('rev-delundel'), + 'target=' . urlencode( $this->mTitle->getPrefixedDbkey() ) . + '&oldid=' . urlencode( $oldid ) ); + // Bolden oversighted content + if( $revision->isDeleted( Revision::DELETED_RESTRICTED ) ) + $cdel = "$cdel"; + } + $cdel = "($cdel) "; + } + # Show user links if allowed to see them. Normally they + # are hidden regardless, but since we can already see the text here... + $userlinks = $sk->revUserTools( $revision, false ); $m = wfMsg( 'revision-info-current' ); $infomsg = $current && !wfEmptyMsg( 'revision-info-current', $m ) && $m != '-' @@ -2665,7 +2746,8 @@ class Article { : 'revision-info'; $r = "\n\t\t\t\t
" . wfMsg( $infomsg, $td, $userlinks ) . "
\n" . - "\n\t\t\t\t
" . wfMsg( 'revision-nav', $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff ) . "
\n\t\t\t"; + + "\n\t\t\t\t
" . $cdel . wfMsg( 'revision-nav', $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff ) . "
\n\t\t\t"; $wgOut->setSubtitle( $r ); } @@ -2915,6 +2997,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(); + } } /** @@ -2924,9 +3010,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(); @@ -2939,6 +3027,15 @@ class Article { /**#@-*/ + /** + * Overriden by ImagePage class, only present here to avoid a fatal error + * Called for ?action=revert + */ + public function revert(){ + global $wgOut; + $wgOut->showErrorPage( 'nosuchaction', 'nosuchactiontext' ); + } + /** * Info about this page * Called for ?action=info when $wgAllowPageInfo is on. @@ -3065,6 +3162,36 @@ class Article { return $result; } + /** + * Returns a list of hidden categories this page is a member of. + * Uses the page_props and categorylinks tables. + * + * @return array Array of Title objects + */ + function getHiddenCategories() { + $result = array(); + $id = $this->mTitle->getArticleID(); + if( $id == 0 ) { + return array(); + } + + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( array( 'categorylinks', 'page_props', 'page' ), + array( 'cl_to' ), + array( 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat', + 'page_namespace' => NS_CATEGORY, 'page_title=cl_to'), + 'Article:getHiddenCategories' ); + if ( false !== $res ) { + if ( $dbr->numRows( $res ) ) { + while ( $row = $dbr->fetchObject( $res ) ) { + $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to ); + } + } + } + $dbr->freeResult( $res ); + return $result; + } + /** * Return an auto-generated summary if the text provided is a redirect. * @@ -3151,7 +3278,7 @@ 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); @@ -3160,7 +3287,7 @@ class Article { $popts, true, true, $this->getRevIdFetched() ); $popts->setTidy(false); $popts->enableLimitReport( false ); - if ( $cache && $this && $parserOutput->getCacheTime() != -1 ) { + if ( $wgEnableParserCache && $cache && $this && $parserOutput->getCacheTime() != -1 ) { $parserCache =& ParserCache::singleton(); $parserCache->save( $parserOutput, $this, $wgUser ); } @@ -3221,4 +3348,56 @@ class Article { $wgOut->addParserOutput( $parserOutput ); } + /** + * Update all the appropriate counts in the category table, given that + * we've added the categories $added and deleted the categories $deleted. + * + * @param $added array The names of categories that were added + * @param $deleted array The names of categories that were deleted + * @return null + */ + public function updateCategoryCounts( $added, $deleted ) { + $ns = $this->mTitle->getNamespace(); + $dbw = wfGetDB( DB_MASTER ); + + # First make sure the rows exist. If one of the "deleted" ones didn't + # exist, we might legitimately not create it, but it's simpler to just + # create it and then give it a negative value, since the value is bogus + # anyway. + # + # Sometimes I wish we had INSERT ... ON DUPLICATE KEY UPDATE. + $insertCats = array_merge( $added, $deleted ); + $insertRows = array(); + foreach( $insertCats as $cat ) { + $insertRows[] = array( 'cat_title' => $cat ); + } + $dbw->insert( 'category', $insertRows, __METHOD__, 'IGNORE' ); + + $addFields = array( 'cat_pages = cat_pages + 1' ); + $removeFields = array( 'cat_pages = cat_pages - 1' ); + if( $ns == NS_CATEGORY ) { + $addFields[] = 'cat_subcats = cat_subcats + 1'; + $removeFields[] = 'cat_subcats = cat_subcats - 1'; + } elseif( $ns == NS_IMAGE ) { + $addFields[] = 'cat_files = cat_files + 1'; + $removeFields[] = 'cat_files = cat_files - 1'; + } + + if ( $added ) { + $dbw->update( + 'category', + $addFields, + array( 'cat_title' => $added ), + __METHOD__ + ); + } + if ( $deleted ) { + $dbw->update( + 'category', + $removeFields, + array( 'cat_title' => $deleted ), + __METHOD__ + ); + } + } }