* (bug 12574) Allow bots to specify whether an edit should be marked as a bot
[lhc/web/wiklou.git] / includes / Article.php
index 660220b..bca4461 100644 (file)
@@ -47,6 +47,7 @@ class Article {
        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
@@ -135,6 +136,7 @@ class Article {
                $this->mRevIdFetched = 0;
                $this->mRedirectUrl = false;
                $this->mLatest = false;
+               $this->mPreparedEdit = false;
        }
 
        /**
@@ -868,7 +870,7 @@ class Article {
 
                # If we have been passed an &rcid= parameter, we want to give the user a
                # chance to mark this new article as patrolled.
-               if (!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',
@@ -1296,7 +1298,7 @@ class Article {
         * @return bool success
         */
        function doEdit( $text, $summary, $flags = 0 ) {
-               global $wgUser, $wgDBtransactions;
+               global $wgUser, $wgDBtransactions, $wgRequest;
 
                wfProfileIn( __METHOD__ );
                $good = true;
@@ -1321,7 +1323,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 = ( $wgUser->isAllowed( 'bot' ) ? $wgRequest->getBool( 'bot' , true ) : 0 ) || ( $flags & EDIT_FORCE_BOT );
 
                $oldtext = $this->getContent();
                $oldsize = strlen( $oldtext );
@@ -1330,7 +1332,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 );
@@ -1346,8 +1349,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;
@@ -1412,9 +1417,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 {
@@ -1530,12 +1534,10 @@ class Article {
                        return;
                }
 
-               if ( $rc->mAttribs['rc_type'] == RC_NEW && !$wgUseNPPatrol ) {
-                       $wgOut->errorpage( 'nppatroldisabled', 'nppatroldisabledtext' );
-                       return;
-               }
-               
                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;
                }
@@ -1708,7 +1710,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;
                }
 
@@ -1839,23 +1841,110 @@ 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
                $permission_errors = $this->mTitle->getUserPermissionsErrors( 'delete', $wgUser );
 
-               if (count($permission_errors)>0)
-               {
+               if (count($permission_errors)>0) {
                        $wgOut->showPermissionsErrorPage( $permission_errors );
                        return;
                }
@@ -1881,88 +1970,16 @@ 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 === '' ) {
-                               if( !$blanked ) {
-                                       if( $authorOfAll === false ) {
-                                               $reason = wfMsgForContent( 'excontent', '$1' );
-                                       } else {
-                                               $reason = wfMsgForContent( 'excontentauthor', '$1', $authorOfAll );
-                                       }
-                               } else {
-                                       $reason = wfMsgForContent( 'exbeforeblank', '$1' );
-                               }
-
-                               # comment field=255, find the max length of the content from page
-                               # Max content length is max comment length, minus length of the actual
-                               # comment (except for the $1), and minus the possible ... chars
-                               $maxLength = 255 - ( strlen( $reason ) - 2 ) - 3;
-                               if( $maxLength < 0 ) {
-                                       $maxLength = 0;
-                               }
-
-                               # let's strip out newlines
-                               $text = preg_replace( "/[\n\r]/", '', $text );
-
-                               # Truncate to max length
-                               global $wgContLang;
-                               $text = $wgContLang->truncate( $text, $maxLength, '...' );
-
-                               # Remove possible unfinished links
-                               $text = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $text );
-
-                               # Add to the reason field
-                               $reason = str_replace( '$1', $text, $reason );
-                       }
-               }
-
+               
                return $this->confirmDelete( '', $reason );
        }
 
@@ -2028,19 +2045,64 @@ class Article {
                $formaction = $this->mTitle->escapeLocalURL( 'action=delete' . $par );
 
                $confirm = htmlspecialchars( wfMsg( 'deletepage' ) );
-               $delcom = htmlspecialchars( wfMsg( 'deletecomment' ) );
+               $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 = "<option value=\"other\">$mDeletereasonotherlist</option>";
+                       $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 label=\"$value\">";
+                                       $optgroup = "</optgroup>";
+                               } elseif ( substr( $value, 0, 2) == '**' ) {
+                                       // groupmember
+                                       $selected = "";
+                                       $value = trim( substr( $value, 2 ) );
+                                       if ( $this->DeleteReasonList === $value)
+                                               $selected = ' selected="selected"';
+                                       $deleteReasonList .= "<option value=\"$value\"$selected>$value</option>";
+                               } else {
+                                       // groupless delete reason
+                                       $selected = "";
+                                       if ( $this->DeleteReasonList === $value)
+                                               $selected = ' selected="selected"';
+                                       $deleteReasonList .= "$optgroup<option value=\"$value\"$selected>$value</option>";
+                                       $optgroup = "";
+                               }
+                       }
+                       $deleteReasonList .= $optgroup;
+               }
                $wgOut->addHTML( "
 <form id='deleteconfirm' method='post' action=\"{$formaction}\">
        <table border='0'>
-               <tr>
+               <tr id=\"wpDeleteReasonListRow\" name=\"wpDeleteReasonListRow\">
                        <td align='right'>
-                               <label for='wpReason'>{$delcom}:</label>
+                               $delcom:
+                       </td>
+                       <td align='left'>
+                               <select tabindex='1' id='wpDeleteReasonList' name=\"wpDeleteReasonList\">
+                                       $deleteReasonList
+                               </select>
+                       </td>
+               </tr>
+               <tr id=\"wpDeleteReasonRow\" name=\"wpDeleteReasonRow\">
+                       <td>
+                               $mDeletereasonother
                        </td>
                        <td align='left'>
-                               <input type='text' maxlength='255' size='60' name='wpReason' id='wpReason' value=\"" . htmlspecialchars( $reason ) . "\" tabindex=\"1\" />
+                               <input type='text' maxlength='255' size='60' name='wpReason' id='wpReason' value=\"" . htmlspecialchars( $reason ) . "\" tabindex=\"2\" />
                        </td>
                </tr>
                <tr>
@@ -2218,7 +2280,7 @@ class Article {
         * @return self::SUCCESS on succes, self::* on failure
         */
        public function doRollback( $fromP, $summary, $token, $bot, &$resultDetails ) {
-               global $wgUser, $wgUseRCPatrol;
+               global $wgUser, $wgUseRCPatrol, $wgRequest;
                $resultDetails = null;
 
                # Just in case it's being called from elsewhere         
@@ -2238,6 +2300,10 @@ class Article {
                if( !$wgUser->matchEditToken( $token, array( $this->mTitle->getPrefixedText(), $fromP ) ) )
                        return self::BAD_TOKEN;
 
+               if ( $wgUser->pingLimiter('rollback') || $wgUser->pingLimiter() ) {
+                       return self::RATE_LIMITED;
+               }
+
                $dbw = wfGetDB( DB_MASTER );
 
                # Get the last editor
@@ -2272,7 +2338,7 @@ class Article {
                }
        
                $set = array();
-               if ( $bot ) {
+               if ( $bot && $wgUser->isAllowed('markbotedits') ) {
                        # Mark all reverted edits as bot
                        $set['rc_bot'] = 1;
                }
@@ -2297,8 +2363,12 @@ class Article {
                        $summary = wfMsgForContent( 'revertpage', $target->getUserText(), $from );
 
                # Save
-               $flags = EDIT_UPDATE | EDIT_MINOR;
-               if( $bot )
+               $flags = EDIT_UPDATE;
+
+               if ($wgUser->isAllowed('minoredit'))
+                       $flags |= EDIT_MINOR;
+
+               if( $bot && $wgRequest->getBool( 'bot' , true ) )
                        $flags |= EDIT_FORCE_BOT;
                $this->doEdit( $target->getText(), $summary, $flags );
 
@@ -2374,6 +2444,9 @@ class Article {
                                $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'];
@@ -2414,6 +2487,29 @@ class Article {
                $wgUser->clearNotification( $this->mTitle );
        }
 
+       /**
+        * 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.
@@ -2433,16 +2529,21 @@ class Article {
                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 );
+               $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 ) ) ) {
@@ -2796,6 +2897,7 @@ class Article {
 
                $title->touchLinks();
                $title->purgeSquid();
+               $title->deleteTitleProtection();
        }
 
        static function onArticleDelete( $title ) {
@@ -3053,9 +3155,11 @@ class Article {
 
                $popts = $wgOut->parserOptions();
                $popts->setTidy(true);
+               $popts->enableLimitReport();
                $parserOutput = $wgParser->parse( $text, $this->mTitle,
                        $popts, true, true, $this->getRevIdFetched() );
                $popts->setTidy(false);
+               $popts->enableLimitReport( false );
                if ( $cache && $this && $parserOutput->getCacheTime() != -1 ) {
                        $parserCache =& ParserCache::singleton();
                        $parserCache->save( $parserOutput, $this, $wgUser );