Refactored parser output handling slightly, and added a hook function, to allow exten...
[lhc/web/wiklou.git] / includes / Article.php
index 6a1c0e3..368210f 100644 (file)
@@ -8,10 +8,6 @@
  * Need the CacheManager to be loaded
  */
 require_once( 'CacheManager.php' );
-require_once( 'Revision.php' );
-
-$wgArticleCurContentFields = false;
-$wgArticleOldContentFields = false;
 
 /**
  * Class representing a MediaWiki article and history.
@@ -23,26 +19,34 @@ $wgArticleOldContentFields = false;
  * @package MediaWiki
  */
 class Article {
-       /**#@+
-        * @access private
-        */
-       var $mContent, $mContentLoaded;
-       var $mUser, $mTimestamp, $mUserText;
-       var $mCounter, $mComment, $mGoodAdjustment, $mTotalAdjustment;
-       var $mMinorEdit, $mRedirectedFrom;
-       var $mTouched, $mFileCache, $mTitle;
-       var $mId, $mTable;
-       var $mForUpdate;
-       var $mOldId;
-       var $mRevIdFetched;
-       var $mRevision;
-       var $mRedirectUrl;
-       /**#@-*/
+       /**@{{
+        * @private
+        */
+       var $mComment;                  //!<
+       var $mContent;                  //!<
+       var $mContentLoaded;    //!<
+       var $mCounter;                  //!<
+       var $mForUpdate;                //!<
+       var $mGoodAdjustment;   //!<
+       var $mLatest;                   //!<
+       var $mMinorEdit;                //!<
+       var $mOldId;                    //!<
+       var $mRedirectedFrom;   //!<
+       var $mRedirectUrl;              //!<
+       var $mRevIdFetched;             //!<
+       var $mRevision;                 //!<
+       var $mTimestamp;                //!<
+       var $mTitle;                    //!<
+       var $mTotalAdjustment;  //!<
+       var $mTouched;                  //!<
+       var $mUser;                             //!<
+       var $mUserText;                 //!<
+       /**@}}*/
 
        /**
         * Constructor and clear the article
-        * @param Title &$title
-        * @param integer $oldId Revision ID, null to fetch from request, zero for current
+        * @param $title Reference to a Title object.
+        * @param $oldId Integer revision ID, null to fetch from request, zero for current
         */
        function Article( &$title, $oldId = null ) {
                $this->mTitle =& $title;
@@ -53,7 +57,7 @@ class Article {
        /**
         * Tell the page view functions that this view was redirected
         * from another page on the wiki.
-        * @param Title $from
+        * @param $from Title object.
         */
        function setRedirectedFrom( $from ) {
                $this->mRedirectedFrom = $from;
@@ -91,11 +95,8 @@ class Article {
                                        } else {
                                                return $rt->getFullURL();
                                        }
-                               } elseif( $rt->exists() ) {
-                                       // Internal redirects can be handled relatively gracefully.
-                                       // We may have to change to another Article subclass, though.
-                                       return $rt;
                                }
+                               return $rt;
                        }
                }
                
@@ -112,7 +113,7 @@ class Article {
 
        /**
          * Clear the object
-         * @access private
+         * @private
          */
        function clear() {
                $this->mDataLoaded    = false;
@@ -121,21 +122,22 @@ class Article {
                $this->mCurID = $this->mUser = $this->mCounter = -1; # Not loaded
                $this->mRedirectedFrom = null; # Title object if set
                $this->mUserText =
-               $this->mTimestamp = $this->mComment = $this->mFileCache = '';
+               $this->mTimestamp = $this->mComment = '';
                $this->mGoodAdjustment = $this->mTotalAdjustment = 0;
                $this->mTouched = '19700101000000';
                $this->mForUpdate = false;
                $this->mIsRedirect = false;
                $this->mRevIdFetched = 0;
                $this->mRedirectUrl = false;
+               $this->mLatest = false;
        }
 
        /**
         * Note that getContent/loadContent do not follow redirects anymore.
         * If you need to fetch redirectable content easily, try
         * the shortcut in Article::followContent()
-        *
-        * @fixme There are still side-effects in this!
+        * FIXME
+        * @todo There are still side-effects in this!
         *        In general, you should use the Revision class, not Article,
         *        to fetch text for purposes other than page views.
         *
@@ -144,27 +146,10 @@ class Article {
        function getContent() {
                global $wgRequest, $wgUser, $wgOut;
 
-               # Get variables from query string :P
-               $action = $wgRequest->getText( 'action', 'view' );
-               $section = $wgRequest->getText( 'section' );
-               $preload = $wgRequest->getText( 'preload' );
-
-               $fname =  'Article::getContent';
-               wfProfileIn( $fname );
+               wfProfileIn( __METHOD__ );
 
                if ( 0 == $this->getID() ) {
-                       if ( 'edit' == $action ) {
-                               wfProfileOut( $fname );
-
-                               # If requested, preload some text.
-                               $text=$this->getPreloadedText($preload);
-
-                               # We used to put MediaWiki:Newarticletext here if
-                               # $text was empty at this point.
-                               # This is now shown above the edit box instead.
-                               return $text;
-                       }
-                       wfProfileOut( $fname );
+                       wfProfileOut( __METHOD__ );
                        $wgOut->setRobotpolicy( 'noindex,nofollow' );
 
                        if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
@@ -176,133 +161,26 @@ class Article {
                        return "<div class='noarticletext'>$ret</div>";
                } else {
                        $this->loadContent();
-                       # check if we're displaying a [[User talk:x.x.x.x]] anonymous talk page
-                       if ( $this->mTitle->getNamespace() == NS_USER_TALK &&
-                         $wgUser->isIP($this->mTitle->getText()) &&
-                         $action=='view'
-                       ) {
-                               wfProfileOut( $fname );
-                               return $this->mContent . "\n" .wfMsg('anontalkpagetext');
-                       } else {
-                               if($action=='edit') {
-                                       if($section!='') {
-                                               if($section=='new') {
-                                                       wfProfileOut( $fname );
-                                                       $text=$this->getPreloadedText($preload);
-                                                       return $text;
-                                               }
-
-                                               # strip NOWIKI etc. to avoid confusion (true-parameter causes HTML
-                                               # comments to be stripped as well)
-                                               $rv=$this->getSection($this->mContent,$section);
-                                               wfProfileOut( $fname );
-                                               return $rv;
-                                       }
-                               }
-                               wfProfileOut( $fname );
-                               return $this->mContent;
-                       }
-               }
-       }
-
-       /**
-        * Get the contents of a page from its title and remove includeonly tags
-        *
-        * TODO FIXME: This is only here because of the inputbox extension and
-        * should be moved there
-        *
-        * @deprecated
-        *
-        * @param string The title of the page
-        * @return string The contents of the page
-        */
-       function getPreloadedText($preload) {
-               if ( $preload === '' )
-                       return '';
-               else {
-                       $preloadTitle = Title::newFromText( $preload );
-                       if ( isset( $preloadTitle ) && $preloadTitle->userCanRead() ) {
-                               $rev=Revision::newFromTitle($preloadTitle);
-                               if ( is_object( $rev ) ) {
-                                       $text = $rev->getText();
-                                       // TODO FIXME: AAAAAAAAAAA, this shouldn't be implementing
-                                       // its own mini-parser! -ævar
-                                       $text = preg_replace( '~</?includeonly>~', '', $text );
-                                       return $text;
-                               } else
-                                       return '';
-                       }
+                       wfProfileOut( __METHOD__ );
+                       return $this->mContent;
                }
        }
 
        /**
         * This function returns the text of a section, specified by a number ($section).
-        * A section is text under a heading like == Heading == or <h1>Heading</h1>, or
+        * A section is text under a heading like == Heading == or \<h1\>Heading\</h1\>, or
         * the first section before any such heading (section 0).
         *
         * If a section contains subsections, these are also returned.
         *
-        * @param string $text text to look in
-        * @param integer $section section number
+        * @param $text String: text to look in
+        * @param $section Integer: section number
         * @return string text of the requested section
+        * @deprecated
         */
        function getSection($text,$section) {
-
-               # strip NOWIKI etc. to avoid confusion (true-parameter causes HTML
-               # comments to be stripped as well)
-               $striparray=array();
-               $parser=new Parser();
-               $parser->mOutputType=OT_WIKI;
-               $parser->mOptions = new ParserOptions();
-               $striptext=$parser->strip($text, $striparray, true);
-
-               # now that we can be sure that no pseudo-sections are in the source,
-               # split it up by section
-               $secs =
-                 preg_split(
-                 '/(^=+.+?=+|^<h[1-6].*?>.*?<\/h[1-6].*?>)(?!\S)/mi',
-                 $striptext, -1,
-                 PREG_SPLIT_DELIM_CAPTURE);
-               if($section==0) {
-                       $rv=$secs[0];
-               } else {
-                       $headline=$secs[$section*2-1];
-                       preg_match( '/^(=+).+?=+|^<h([1-6]).*?>.*?<\/h[1-6].*?>(?!\S)/mi',$headline,$matches);
-                       $hlevel=$matches[1];
-
-                       # translate wiki heading into level
-                       if(strpos($hlevel,'=')!==false) {
-                               $hlevel=strlen($hlevel);
-                       }
-
-                       $rv=$headline. $secs[$section*2];
-                       $count=$section+1;
-
-                       $break=false;
-                       while(!empty($secs[$count*2-1]) && !$break) {
-
-                               $subheadline=$secs[$count*2-1];
-                               preg_match( '/^(=+).+?=+|^<h([1-6]).*?>.*?<\/h[1-6].*?>(?!\S)/mi',$subheadline,$matches);
-                               $subhlevel=$matches[1];
-                               if(strpos($subhlevel,'=')!==false) {
-                                       $subhlevel=strlen($subhlevel);
-                               }
-                               if($subhlevel > $hlevel) {
-                                       $rv.=$subheadline.$secs[$count*2];
-                               }
-                               if($subhlevel <= $hlevel) {
-                                       $break=true;
-                               }
-                               $count++;
-
-                       }
-               }
-               # reinsert stripped tags
-               $rv=$parser->unstrip($rv,$striparray);
-               $rv=$parser->unstripNoWiki($rv,$striparray);
-               $rv=trim($rv);
-               return $rv;
-
+               global $wgParser;
+               return $wgParser->getSection( $text, $section );
        }
 
        /**
@@ -342,8 +220,10 @@ class Article {
                                        # TODO
                                }
                        }
-                       $lastid = $oldid;
+                       # unused:
+                       # $lastid = $oldid;
                }
+
                if ( !$oldid ) {
                        $oldid = 0;
                }
@@ -354,15 +234,11 @@ class Article {
         * Load the revision (including text) into this object
         */
        function loadContent() {
-               global $wgOut, $wgRequest;
-
                if ( $this->mContentLoaded ) return;
 
                # Query variables :P
                $oldid = $this->getOldID();
 
-               $fname = 'Article::loadContent';
-
                # Pre-fill content with error message so that if something
                # fails we'll have something telling us what we intended.
 
@@ -377,7 +253,7 @@ class Article {
         * Fetch a page record with the given conditions
         * @param Database $dbr
         * @param array    $conditions
-        * @access private
+        * @private
         */
        function pageData( &$dbr, $conditions ) {
                $fields = array(
@@ -424,16 +300,32 @@ class Article {
         * some source.
         *
         * @param object $data
-        * @access private
+        * @private
         */
-       function loadPageData( $data ) {
-               $this->mTitle->loadRestrictions( $data->page_restrictions );
-               $this->mTitle->mRestrictionsLoaded = true;
+       function loadPageData( $data = 'fromdb' ) {
+               if ( $data === 'fromdb' ) {
+                       $dbr =& $this->getDB();
+                       $data = $this->pageDataFromId( $dbr, $this->getId() );
+               }
+                       
+               $lc =& LinkCache::singleton();
+               if ( $data ) {
+                       $lc->addGoodLinkObj( $data->page_id, $this->mTitle );
 
-               $this->mCounter     = $data->page_counter;
-               $this->mTouched     = wfTimestamp( TS_MW, $data->page_touched );
-               $this->mIsRedirect  = $data->page_is_redirect;
-               $this->mLatest      = $data->page_latest;
+                       $this->mTitle->mArticleID = $data->page_id;
+                       $this->mTitle->loadRestrictions( $data->page_restrictions );
+                       $this->mTitle->mRestrictionsLoaded = true;
+
+                       $this->mCounter     = $data->page_counter;
+                       $this->mTouched     = wfTimestamp( TS_MW, $data->page_touched );
+                       $this->mIsRedirect  = $data->page_is_redirect;
+                       $this->mLatest      = $data->page_latest;
+               } else {
+                       if ( is_object( $this->mTitle ) ) {
+                               $lc->addBadLinkObj( $this->mTitle );
+                       }
+                       $this->mTitle->mArticleID = 0;
+               }
 
                $this->mDataLoaded  = true;
        }
@@ -450,7 +342,6 @@ class Article {
                }
 
                $dbr =& $this->getDB();
-               $fname = 'Article::fetchContent';
 
                # Pre-fill content with error message so that if something
                # fails we'll have something telling us what we intended.
@@ -463,12 +354,12 @@ class Article {
                if( $oldid ) {
                        $revision = Revision::newFromId( $oldid );
                        if( is_null( $revision ) ) {
-                               wfDebug( "$fname failed to retrieve specified revision, id $oldid\n" );
+                               wfDebug( __METHOD__." failed to retrieve specified revision, id $oldid\n" );
                                return false;
                        }
                        $data = $this->pageDataFromId( $dbr, $revision->getPage() );
                        if( !$data ) {
-                               wfDebug( "$fname failed to get page data linked to revision id $oldid\n" );
+                               wfDebug( __METHOD__." failed to get page data linked to revision id $oldid\n" );
                                return false;
                        }
                        $this->mTitle = Title::makeTitle( $data->page_namespace, $data->page_title );
@@ -477,19 +368,22 @@ class Article {
                        if( !$this->mDataLoaded ) {
                                $data = $this->pageDataFromTitle( $dbr, $this->mTitle );
                                if( !$data ) {
-                                       wfDebug( "$fname failed to find page data for title " . $this->mTitle->getPrefixedText() . "\n" );
+                                       wfDebug( __METHOD__." failed to find page data for title " . $this->mTitle->getPrefixedText() . "\n" );
                                        return false;
                                }
                                $this->loadPageData( $data );
                        }
                        $revision = Revision::newFromId( $this->mLatest );
                        if( is_null( $revision ) ) {
-                               wfDebug( "$fname failed to retrieve current page, rev_id $data->page_latest\n" );
+                               wfDebug( __METHOD__." failed to retrieve current page, rev_id {$data->page_latest}\n" );
                                return false;
                        }
                }
 
-               $this->mContent   = $revision->getText();
+               // 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->mUser      = $revision->getUser();
                $this->mUserText  = $revision->getUserText();
@@ -508,7 +402,7 @@ class Article {
        /**
         * Read/write accessor to select FOR UPDATE
         *
-        * @param mixed $x
+        * @param $x Mixed: FIXME
         */
        function forUpdate( $x = NULL ) {
                return wfSetVar( $this->mForUpdate, $x );
@@ -527,9 +421,9 @@ class Article {
        /**
         * Get options for all SELECT statements
         *
-        * @param array $options an optional options array which'll be appended to
+        * @param $options Array: an optional options array which'll be appended to
         *                       the default
-        * @return array Options
+        * @return Array: options
         */
        function getSelectOptions( $options = '' ) {
                if ( $this->mForUpdate ) {
@@ -566,9 +460,13 @@ class Article {
        function getCount() {
                if ( -1 == $this->mCounter ) {
                        $id = $this->getID();
-                       $dbr =& wfGetDB( DB_SLAVE );
-                       $this->mCounter = $dbr->selectField( 'page', 'page_counter', array( 'page_id' => $id ),
-                               'Article::getCount', $this->getSelectOptions() );
+                       if ( $id == 0 ) {
+                               $this->mCounter = 0;
+                       } else {
+                               $dbr =& wfGetDB( DB_SLAVE );
+                               $this->mCounter = $dbr->selectField( 'page', 'page_counter', array( 'page_id' => $id ),
+                                       'Article::getCount', $this->getSelectOptions() );
+                       }
                }
                return $this->mCounter;
        }
@@ -577,15 +475,15 @@ class Article {
         * Determine whether a page  would be suitable for being counted as an
         * article in the site_stats table based on the title & its content
         *
-        * @param string $text Text to analyze
+        * @param $text String: text to analyze
         * @return bool
         */
        function isCountable( $text ) {
-               global $wgUseCommaCount;
+               global $wgUseCommaCount, $wgContentNamespaces;
 
                $token = $wgUseCommaCount ? ',' : '[[';
                return
-                       $this->mTitle->getNamespace() == NS_MAIN
+                       array_search( $this->mTitle->getNamespace(), $wgContentNamespaces ) !== false
                        && ! $this->isRedirect( $text )
                        && in_string( $token, $text );
        }
@@ -593,7 +491,7 @@ class Article {
        /**
         * Tests if the article text represents a redirect
         *
-        * @param string $text
+        * @param $text String: FIXME
         * @return bool
         */
        function isRedirect( $text = false ) {
@@ -620,11 +518,9 @@ class Article {
        /**
         * Loads everything except the text
         * This isn't necessary for all uses, so it's only done if needed.
-        * @access private
+        * @private
         */
        function loadLastEdit() {
-               global $wgOut;
-
                if ( -1 != $this->mUser )
                        return;
 
@@ -644,7 +540,10 @@ class Article {
        }
 
        function getTimestamp() {
-               $this->loadLastEdit();
+               // Check if the field has been filled by ParserCache::get()
+               if ( !$this->mTimestamp ) {
+                       $this->loadLastEdit();
+               }
                return wfTimestamp(TS_MW, $this->mTimestamp);
        }
 
@@ -673,9 +572,12 @@ class Article {
                return $this->mRevIdFetched;
        }
 
+       /**
+        * @todo Document, fixme $offset never used.
+        * @param $limit Integer: default 0.
+        * @param $offset Integer: default 0.
+        */
        function getContributors($limit = 0, $offset = 0) {
-               $fname = 'Article::getContributors';
-
                # XXX: this is expensive; cache this info somewhere.
 
                $title = $this->mTitle;
@@ -698,7 +600,7 @@ class Article {
                if ($limit > 0) { $sql .= ' LIMIT '.$limit; }
                $sql .= ' '. $this->getSelectOptions();
 
-               $res = $dbr->query($sql, $fname);
+               $res = $dbr->query($sql, __METHOD__);
 
                while ( $line = $dbr->fetchObject( $res ) ) {
                        $contribs[] = array($line->rev_user, $line->rev_user_text, $line->user_real_name);
@@ -713,21 +615,23 @@ class Article {
         * the given title.
        */
        function view() {
-               global $wgUser, $wgOut, $wgRequest, $wgOnlySysopsCanPatrol, $wgContLang;
+               global $wgUser, $wgOut, $wgRequest, $wgContLang;
                global $wgEnableParserCache, $wgStylePath, $wgUseRCPatrol, $wgParser;
-               global $wgUseTrackbacks;
+               global $wgUseTrackbacks, $wgNamespaceRobotPolicies;
                $sk = $wgUser->getSkin();
 
-               $fname = 'Article::view';
-               wfProfileIn( $fname );
+               wfProfileIn( __METHOD__ );
+
                $parserCache =& ParserCache::singleton();
+               $ns = $this->mTitle->getNamespace(); # shortcut
+               
                # Get variables from query string
                $oldid = $this->getOldID();
 
                # getOldID may want us to redirect somewhere else
                if ( $this->mRedirectUrl ) {
                        $wgOut->redirect( $this->mRedirectUrl );
-                       wfProfileOut( $fname );
+                       wfProfileOut( __METHOD__ );
                        return;
                }
 
@@ -736,7 +640,13 @@ class Article {
                $rdfrom = $wgRequest->getVal( 'rdfrom' );
 
                $wgOut->setArticleFlag( true );
-               $wgOut->setRobotpolicy( 'index,follow' );
+               if ( isset( $wgNamespaceRobotPolicies[$ns] ) ) {
+                       $policy = $wgNamespaceRobotPolicies[$ns];
+               } else {
+                       $policy = 'index,follow';
+               }
+               $wgOut->setRobotpolicy( $policy );
+
                # If we got diff and oldid in the query, we want to see a
                # diff page instead of the article.
 
@@ -753,24 +663,25 @@ class Article {
                                # Run view updates for current revision only
                                $this->viewUpdates();
                        }
-                       wfProfileOut( $fname );
+                       wfProfileOut( __METHOD__ );
                        return;
                }
-
+               
                if ( empty( $oldid ) && $this->checkTouched() ) {
                        $wgOut->setETag($parserCache->getETag($this, $wgUser));
 
                        if( $wgOut->checkLastModified( $this->mTouched ) ){
-                               wfProfileOut( $fname );
+                               wfProfileOut( __METHOD__ );
                                return;
                        } else if ( $this->tryFileCache() ) {
                                # tell wgOut that output is taken care of
                                $wgOut->disable();
                                $this->viewUpdates();
-                               wfProfileOut( $fname );
+                               wfProfileOut( __METHOD__ );
                                return;
                        }
                }
+
                # Should the parser cache be used?
                $pcache = $wgEnableParserCache &&
                        intval( $wgUser->getOption( 'stubthreshold' ) ) == 0 &&
@@ -808,6 +719,7 @@ class Article {
                $outputDone = false;
                if ( $pcache ) {
                        if ( $wgOut->tryParserCache( $this, $wgUser ) ) {
+                               wfRunHooks( 'ArticleViewHeader', array( &$this ) );
                                $outputDone = true;
                        }
                }
@@ -818,8 +730,10 @@ class Article {
                                $t = $this->mTitle->getPrefixedText();
                                if( $oldid ) {
                                        $t .= ',oldid='.$oldid;
+                                       $text = wfMsg( 'missingarticle', $t );
+                               } else {
+                                       $text = wfMsg( 'noarticletext', $t );
                                }
-                               $text = wfMsg( 'missingarticle', $t );
                        }
 
                        # Another whitelist check in case oldid is altering the title
@@ -832,8 +746,23 @@ class Article {
                        # We're looking at an old revision
 
                        if ( !empty( $oldid ) ) {
-                               $this->setOldSubtitle( isset($this->mOldId) ? $this->mOldId : $oldid );
-                               $wgOut->setRobotpolicy( 'noindex,follow' );
+                               $wgOut->setRobotpolicy( 'noindex,nofollow' );
+                               if( is_null( $this->mRevision ) ) {
+                                       // FIXME: This would be a nice place to load the 'no such page' text.
+                               } else {
+                                       $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->setPageTitle( $this->mTitle->getPrefixedText() );
+                                                       return;
+                                               } else {
+                                                       $wgOut->addWikiText( wfMsg( 'rev-deleted-text-view' ) );
+                                                       // and we are allowed to see...
+                                               }
+                                       }
+                               }
+
                        }
                }
                if( !$outputDone ) {
@@ -846,7 +775,7 @@ class Article {
                        # wrap user css and user js in pre and don't parse
                        # XXX: use $this->mTitle->usCssJsSubpage() when php is fixed/ a workaround is found
                        if (
-                               $this->mTitle->getNamespace() == NS_USER &&
+                               $ns == NS_USER &&
                                preg_match('/\\/[\\w]+\\.(css|js)$/', $this->mTitle->getDBkey())
                        ) {
                                $wgOut->addWikiText( wfMsg('clearyourcache'));
@@ -855,10 +784,12 @@ class Article {
                                # Display redirect
                                $imageDir = $wgContLang->isRTL() ? 'rtl' : 'ltr';
                                $imageUrl = $wgStylePath.'/common/images/redirect' . $imageDir . '.png';
-                               if( !$wasRedirected ) {
+                               # Don't overwrite the subtitle if this was an old revision
+                               if( !$wasRedirected && $this->isCurrent() ) {
                                        $wgOut->setSubtitle( wfMsgHtml( 'redirectpagesub' ) );
                                }
                                $targetUrl = $rt->escapeLocalURL();
+                               # fixme unused $titleText :
                                $titleText = htmlspecialchars( $rt->getPrefixedText() );
                                $link = $sk->makeLinkObj( $rt );
 
@@ -872,16 +803,15 @@ class Article {
                                $wgOut->addPrimaryWikiText( $text, $this );
                        } else {
                                # Display content, don't attempt to save to parser cache
-
                                # Don't show section-edit links on old revisions... this way lies madness.
                                if( !$this->isCurrent() ) {
-                                       $oldEditSectionSetting = $wgOut->mParserOptions->setEditSection( false );
+                                       $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection( false );
                                }
                                # Display content and don't save to parser cache
                                $wgOut->addPrimaryWikiText( $text, $this, false );
 
                                if( !$this->isCurrent() ) {
-                                       $wgOut->mParserOptions->setEditSection( $oldEditSectionSetting );
+                                       $wgOut->parserOptions()->setEditSection( $oldEditSectionSetting );
                                }
                        }
                }
@@ -891,14 +821,15 @@ class Article {
                        $wgOut->setPageTitle( $this->mTitle->getPrefixedText() );
                }
 
+               # 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') );
+               }
+
                # 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->isLoggedIn()
-                       && ( $wgUser->isAllowed('patrol') || !$wgOnlySysopsCanPatrol ) )
-               {
+               if ( $wgUseRCPatrol && !is_null( $rcid ) && $rcid != 0 && $wgUser->isAllowed( 'patrol' ) ) {
                        $wgOut->addHTML(
                                "<div class='patrollink'>" .
                                        wfMsg ( 'markaspatrolledlink',
@@ -913,7 +844,7 @@ class Article {
                        $this->addTrackbacks();
 
                $this->viewUpdates();
-               wfProfileOut( $fname );
+               wfProfileOut( __METHOD__ );
        }
 
        function addTrackbacks() {
@@ -932,7 +863,7 @@ class Article {
                $tbtext = "";
                while ($o = $dbr->fetchObject($tbs)) {
                        $rmvtxt = "";
-                       if ($wgUser->isSysop()) {
+                       if ($wgUser->isAllowed( 'trackback' )) {
                                $delurl = $this->mTitle->getFullURL("action=deletetrackback&tbid="
                                                . $o->tb_id . "&token=" . $wgUser->editToken());
                                $rmvtxt = wfMsg('trackbackremove', $delurl);
@@ -956,7 +887,7 @@ class Article {
                }
 
                if ((!$wgUser->isAllowed('delete'))) {
-                       $wgOut->sysopRequired();
+                       $wgOut->permissionRequired( 'delete' );
                        return;
                }
 
@@ -978,23 +909,16 @@ class Article {
                $this->view();
        }
 
+       /**
+        * Handle action=purge
+        */
        function purge() {
-               global $wgUser, $wgRequest, $wgOut, $wgUseSquid;
-
-               if ( $wgUser->isLoggedIn() || $wgRequest->wasPosted() || ! wfRunHooks( 'ArticlePurge', array( &$this ) ) ) {
-                       // Invalidate the cache
-                       $this->mTitle->invalidateCache();
-
-                       if ( $wgUseSquid ) {
-                               // Commit the transaction before the purge is sent
-                               $dbw = wfGetDB( DB_MASTER );
-                               $dbw->immediateCommit();
+               global $wgUser, $wgRequest, $wgOut;
 
-                               // Send purge
-                               $update = SquidUpdate::newSimplePurge( $this->mTitle );
-                               $update->doUpdate();
+               if ( $wgUser->isLoggedIn() || $wgRequest->wasPosted() ) {
+                       if( wfRunHooks( 'ArticlePurge', array( &$this ) ) ) {
+                               $this->doPurge();
                        }
-                       $this->view();
                } else {
                        $msg = $wgOut->parse( wfMsg( 'confirm_purge' ) );
                        $action = $this->mTitle->escapeLocalURL( 'action=purge' );
@@ -1009,6 +933,26 @@ class Article {
                        $wgOut->addHTML( $msg );
                }
        }
+       
+       /**
+        * Perform the actions of a page purging
+        */
+       function doPurge() {
+               global $wgUseSquid;
+               // Invalidate the cache
+               $this->mTitle->invalidateCache();
+
+               if ( $wgUseSquid ) {
+                       // Commit the transaction before the purge is sent
+                       $dbw = wfGetDB( DB_MASTER );
+                       $dbw->immediateCommit();
+
+                       // Send purge
+                       $update = SquidUpdate::newSimplePurge( $this->mTitle );
+                       $update->doUpdate();
+               }
+               $this->view();
+       }
 
        /**
         * Insert a new empty page record for this article.
@@ -1020,11 +964,10 @@ class Article {
         * @param Database $dbw
         * @param string   $restrictions
         * @return int     The newly created page_id key
-        * @access private
+        * @private
         */
        function insertOn( &$dbw, $restrictions = '' ) {
-               $fname = 'Article::insertOn';
-               wfProfileIn( $fname );
+               wfProfileIn( __METHOD__ );
 
                $page_id = $dbw->nextSequenceValue( 'page_page_id_seq' );
                $dbw->insert( 'page', array(
@@ -1039,12 +982,12 @@ class Article {
                        'page_touched'      => $dbw->timestamp(),
                        'page_latest'       => 0, # Fill this in shortly...
                        'page_len'          => 0, # Fill this in shortly...
-               ), $fname );
+               ), __METHOD__ );
                $newid = $dbw->insertId();
 
                $this->mTitle->resetArticleId( $newid );
 
-               wfProfileOut( $fname );
+               wfProfileOut( __METHOD__ );
                return $newid;
        }
 
@@ -1059,11 +1002,10 @@ class Article {
         *                          Giving 0 indicates the new page flag should
         *                          be set on.
         * @return bool true on success, false on failure
-        * @access private
+        * @private
         */
        function updateRevisionOn( &$dbw, $revision, $lastRevision = null ) {
-               $fname = 'Article::updateToRevision';
-               wfProfileIn( $fname );
+               wfProfileIn( __METHOD__ );
 
                $conditions = array( 'page_id' => $this->getId() );
                if( !is_null( $lastRevision ) ) {
@@ -1081,9 +1023,9 @@ class Article {
                                'page_len'         => strlen( $text ),
                        ),
                        $conditions,
-                       $fname );
+                       __METHOD__ );
 
-               wfProfileOut( $fname );
+               wfProfileOut( __METHOD__ );
                return ( $dbw->affectedRows() != 0 );
        }
 
@@ -1095,8 +1037,7 @@ class Article {
         * @param Revision $revision
         */
        function updateIfNewerOn( &$dbw, $revision ) {
-               $fname = 'Article::updateIfNewerOn';
-               wfProfileIn( $fname );
+               wfProfileIn( __METHOD__ );
 
                $row = $dbw->selectRow(
                        array( 'revision', 'page' ),
@@ -1104,10 +1045,10 @@ class Article {
                        array(
                                'page_id' => $this->getId(),
                                'page_latest=rev_id' ),
-                       $fname );
+                       __METHOD__ );
                if( $row ) {
                        if( wfTimestamp(TS_MW, $row->rev_timestamp) >= $revision->getTimestamp() ) {
-                               wfProfileOut( $fname );
+                               wfProfileOut( __METHOD__ );
                                return false;
                        }
                        $prev = $row->rev_id;
@@ -1117,115 +1058,19 @@ class Article {
                }
 
                $ret = $this->updateRevisionOn( $dbw, $revision, $prev );
-               wfProfileOut( $fname );
+               wfProfileOut( __METHOD__ );
                return $ret;
        }
 
-       /**
-        * Theoretically we could defer these whole insert and update
-        * functions for after display, but that's taking a big leap
-        * of faith, and we want to be able to report database
-        * errors at some point.
-        * @access private
-        */
-       function insertNewArticle( $text, $summary, $isminor, $watchthis, $suppressRC=false, $comment=false ) {
-               global $wgOut, $wgUser, $wgUseSquid;
-
-               $fname = 'Article::insertNewArticle';
-               wfProfileIn( $fname );
-
-               if( !wfRunHooks( 'ArticleSave', array( &$this, &$wgUser, &$text,
-                       &$summary, &$isminor, &$watchthis, NULL ) ) ) {
-                       wfDebug( "$fname: ArticleSave hook aborted save!\n" );
-                       wfProfileOut( $fname );
-                       return false;
-               }
-
-               $this->mGoodAdjustment = (int)$this->isCountable( $text );
-               $this->mTotalAdjustment = 1;
-
-               $ns = $this->mTitle->getNamespace();
-               $ttl = $this->mTitle->getDBkey();
-
-               # If this is a comment, add the summary as headline
-               if($comment && $summary!="") {
-                       $text="== {$summary} ==\n\n".$text;
-               }
-               $text = $this->preSaveTransform( $text );
-
-               /* Silently ignore minoredit if not allowed */
-               $isminor = $isminor && $wgUser->isAllowed('minoredit');
-               $now = wfTimestampNow();
-
-               $dbw =& wfGetDB( DB_MASTER );
-
-               # Add the page record; stake our claim on this title!
-               $newid = $this->insertOn( $dbw );
-
-               # Save the revision text...
-               $revision = new Revision( array(
-                       'page'       => $newid,
-                       'comment'    => $summary,
-                       'minor_edit' => $isminor,
-                       'text'       => $text
-                       ) );
-               $revisionId = $revision->insertOn( $dbw );
-
-               $this->mTitle->resetArticleID( $newid );
-
-               # Update the page record with revision data
-               $this->updateRevisionOn( $dbw, $revision, 0 );
-
-               Article::onArticleCreate( $this->mTitle );
-               if(!$suppressRC) {
-                       require_once( 'RecentChange.php' );
-                       RecentChange::notifyNew( $now, $this->mTitle, $isminor, $wgUser, $summary, 'default',
-                         '', strlen( $text ), $revisionId );
-               }
-
-               if ($watchthis) {
-                       if(!$this->mTitle->userIsWatching()) $this->watch();
-               } else {
-                       if ( $this->mTitle->userIsWatching() ) {
-                               $this->unwatch();
-                       }
-               }
-
-               # The talk page isn't in the regular link tables, so we need to update manually:
-               $talkns = $ns ^ 1; # talk -> normal; normal -> talk
-               $dbw->update( 'page',
-                       array( 'page_touched' => $dbw->timestamp($now) ),
-                       array( 'page_namespace' => $talkns,
-                              'page_title' => $ttl ),
-                       $fname );
-
-               # standard deferred updates
-               $this->editUpdates( $text, $summary, $isminor, $now, $revisionId );
-
-               $oldid = 0; # new article
-               $this->showArticle( $text, wfMsg( 'newarticle' ), false, $isminor, $now, $summary, $oldid );
-
-               wfRunHooks( 'ArticleInsertComplete', array( &$this, &$wgUser, $text,
-                       $summary, $isminor,
-                       $watchthis, NULL ) );
-               wfRunHooks( 'ArticleSaveComplete', array( &$this, &$wgUser, $text,
-                       $summary, $isminor,
-                       $watchthis, NULL ) );
-               wfProfileOut( $fname );
-       }
-
-       function getTextOfLastEditWithSectionReplacedOrAdded($section, $text, $summary = '', $edittime = NULL) {
-               $this->replaceSection( $section, $text, $summary, $edittime );
-       }
-
        /**
         * @return string Complete article text, or null if error
         */
        function replaceSection($section, $text, $summary = '', $edittime = NULL) {
-               $fname = 'Article::replaceSection';
-               wfProfileIn( $fname );
-
-               if ($section != '') {
+               wfProfileIn( __METHOD__ );
+               
+               if( $section == '' ) {
+                       // Whole-page edit; let the text through unmolested.
+               } else {
                        if( is_null( $edittime ) ) {
                                $rev = Revision::newFromTitle( $this->mTitle );
                        } else {
@@ -1243,251 +1088,316 @@ class Article {
                                if($summary) $subject="== {$summary} ==\n\n";
                                $text=$oldtext."\n\n".$subject.$text;
                        } else {
+                               global $wgParser;
+                               $text = $wgParser->replaceSection( $oldtext, $section, $text );
+                       }
+               }
 
-                               # strip NOWIKI etc. to avoid confusion (true-parameter causes HTML
-                               # comments to be stripped as well)
-                               $striparray=array();
-                               $parser=new Parser();
-                               $parser->mOutputType=OT_WIKI;
-                               $parser->mOptions = new ParserOptions();
-                               $oldtext=$parser->strip($oldtext, $striparray, true);
-
-                               # now that we can be sure that no pseudo-sections are in the source,
-                               # split it up
-                               # Unfortunately we can't simply do a preg_replace because that might
-                               # replace the wrong section, so we have to use the section counter instead
-                               $secs=preg_split('/(^=+.+?=+|^<h[1-6].*?>.*?<\/h[1-6].*?>)(?!\S)/mi',
-                                 $oldtext,-1,PREG_SPLIT_DELIM_CAPTURE);
-                               $secs[$section*2]=$text."\n\n"; // replace with edited
-
-                               # section 0 is top (intro) section
-                               if($section!=0) {
-
-                                       # headline of old section - we need to go through this section
-                                       # to determine if there are any subsections that now need to
-                                       # be erased, as the mother section has been replaced with
-                                       # the text of all subsections.
-                                       $headline=$secs[$section*2-1];
-                                       preg_match( '/^(=+).+?=+|^<h([1-6]).*?>.*?<\/h[1-6].*?>(?!\S)/mi',$headline,$matches);
-                                       $hlevel=$matches[1];
-
-                                       # determine headline level for wikimarkup headings
-                                       if(strpos($hlevel,'=')!==false) {
-                                               $hlevel=strlen($hlevel);
-                                       }
+               wfProfileOut( __METHOD__ );
+               return $text;
+       }
 
-                                       $secs[$section*2-1]=''; // erase old headline
-                                       $count=$section+1;
-                                       $break=false;
-                                       while(!empty($secs[$count*2-1]) && !$break) {
-
-                                               $subheadline=$secs[$count*2-1];
-                                               preg_match(
-                                                '/^(=+).+?=+|^<h([1-6]).*?>.*?<\/h[1-6].*?>(?!\S)/mi',$subheadline,$matches);
-                                               $subhlevel=$matches[1];
-                                               if(strpos($subhlevel,'=')!==false) {
-                                                       $subhlevel=strlen($subhlevel);
-                                               }
-                                               if($subhlevel > $hlevel) {
-                                                       // erase old subsections
-                                                       $secs[$count*2-1]='';
-                                                       $secs[$count*2]='';
-                                               }
-                                               if($subhlevel <= $hlevel) {
-                                                       $break=true;
-                                               }
-                                               $count++;
+       /**
+        * @deprecated use Article::doEdit()
+        */
+       function insertNewArticle( $text, $summary, $isminor, $watchthis, $suppressRC=false, $comment=false ) {
+               $flags = EDIT_NEW | EDIT_DEFER_UPDATES |
+                       ( $isminor ? EDIT_MINOR : 0 ) |
+                       ( $suppressRC ? EDIT_SUPPRESS_RC : 0 );
 
-                                       }
+               # If this is a comment, add the summary as headline
+               if ( $comment && $summary != "" ) {
+                       $text = "== {$summary} ==\n\n".$text;
+               }
+               
+               $this->doEdit( $text, $summary, $flags );
+
+               $dbw =& wfGetDB( DB_MASTER );
+               if ($watchthis) {
+                       if (!$this->mTitle->userIsWatching()) {
+                               $dbw->begin();
+                               $this->doWatch();
+                               $dbw->commit();
+                       }
+               } else {
+                       if ( $this->mTitle->userIsWatching() ) {
+                               $dbw->begin();
+                               $this->doUnwatch();
+                               $dbw->commit();
+                       }
+               }
+               $this->doRedirect( $this->isRedirect( $text ) );
+       }
+
+       /**
+        * @deprecated use Article::doEdit()
+        */
+       function updateArticle( $text, $summary, $minor, $watchthis, $forceBot = false, $sectionanchor = '' ) {
+               $flags = EDIT_UPDATE | EDIT_DEFER_UPDATES |
+                       ( $minor ? EDIT_MINOR : 0 ) |
+                       ( $forceBot ? EDIT_FORCE_BOT : 0 );
 
+               $good = $this->doEdit( $text, $summary, $flags );
+               if ( $good ) {
+                       $dbw =& wfGetDB( DB_MASTER );
+                       if ($watchthis) {
+                               if (!$this->mTitle->userIsWatching()) {
+                                       $dbw->begin();
+                                       $this->doWatch();
+                                       $dbw->commit();
+                               }
+                       } else {
+                               if ( $this->mTitle->userIsWatching() ) {
+                                       $dbw->begin();
+                                       $this->doUnwatch();
+                                       $dbw->commit();
                                }
-                               $text=join('',$secs);
-                               # reinsert the stuff that we stripped out earlier
-                               $text=$parser->unstrip($text,$striparray);
-                               $text=$parser->unstripNoWiki($text,$striparray);
                        }
 
+                       $this->doRedirect( $this->isRedirect( $text ), $sectionanchor );
                }
-               wfProfileOut( $fname );
-               return $text;
+               return $good;
        }
 
        /**
-        * Change an existing article. Puts the previous version back into the old table, updates RC
-        * and all necessary caches, mostly via the deferred update array.
+        * Article::doEdit()
         *
-        * It is possible to call this function from a command-line script, but note that you should
-        * first set $wgUser, and clean up $wgDeferredUpdates after each edit.
+        * Change an existing article or create a new article. Updates RC and all necessary caches, 
+        * optionally via the deferred update array.
+        *
+        * $wgUser must be set before calling this function.
+        *
+        * @param string $text New text
+        * @param string $summary Edit summary
+        * @param integer $flags bitfield:
+        *      EDIT_NEW
+        *          Article is known or assumed to be non-existent, create a new one
+        *      EDIT_UPDATE
+        *          Article is known or assumed to be pre-existing, update it
+        *      EDIT_MINOR
+        *          Mark this edit minor, if the user is allowed to do so
+        *      EDIT_SUPPRESS_RC
+        *          Do not log the change in recentchanges
+        *      EDIT_FORCE_BOT
+        *          Mark the edit a "bot" edit regardless of user rights
+        *      EDIT_DEFER_UPDATES
+        *          Defer some of the updates until the end of index.php
+        * 
+        * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the article will be detected. 
+        * If EDIT_UPDATE is specified and the article doesn't exist, the function will return false. If 
+        * EDIT_NEW is specified and the article does exist, a duplicate key error will cause an exception
+        * to be thrown from the Database. These two conditions are also possible with auto-detection due
+        * to MediaWiki's performance-optimised locking strategy.
+        *
+        * @return bool success
         */
-       function updateArticle( $text, $summary, $minor, $watchthis, $forceBot = false, $sectionanchor = '' ) {
-               global $wgOut, $wgUser, $wgDBtransactions, $wgMwRedir, $wgUseSquid;
-               global $wgPostCommitUpdateList, $wgUseFileCache;
+       function doEdit( $text, $summary, $flags = 0 ) {
+               global $wgUser, $wgDBtransactions;
 
-               $fname = 'Article::updateArticle';
-               wfProfileIn( $fname );
+               wfProfileIn( __METHOD__ );
                $good = true;
 
+               if ( !($flags & EDIT_NEW) && !($flags & EDIT_UPDATE) ) {
+                       $aid = $this->mTitle->getArticleID( GAID_FOR_UPDATE );
+                       if ( $aid ) {
+                               $flags |= EDIT_UPDATE;
+                       } else {
+                               $flags |= EDIT_NEW;
+                       }
+               }
+
                if( !wfRunHooks( 'ArticleSave', array( &$this, &$wgUser, &$text,
-                       &$summary, &$minor,
-                       &$watchthis, &$sectionanchor ) ) ) {
-                       wfDebug( "$fname: ArticleSave hook aborted save!\n" );
-                       wfProfileOut( $fname );
+                       &$summary, $flags & EDIT_MINOR,
+                       null, null, &$flags ) ) ) 
+               {
+                       wfDebug( __METHOD__ . ": ArticleSave hook aborted save!\n" );
+                       wfProfileOut( __METHOD__ );
                        return false;
                }
 
-               $isminor = $minor && $wgUser->isAllowed('minoredit');
-               $redir = (int)$this->isRedirect( $text );
+               # Silently ignore EDIT_MINOR if not allowed
+               $isminor = ( $flags & EDIT_MINOR ) && $wgUser->isAllowed('minoredit');
+               $bot = $wgUser->isAllowed( 'bot' ) || ( $flags & EDIT_FORCE_BOT );
 
                $text = $this->preSaveTransform( $text );
+
                $dbw =& wfGetDB( DB_MASTER );
                $now = wfTimestampNow();
+               
+               if ( $flags & EDIT_UPDATE ) {
+                       # Update article, but only if changed.
+
+                       # Make sure the revision is either completely inserted or not inserted at all
+                       if( !$wgDBtransactions ) {
+                               $userAbort = ignore_user_abort( true );
+                       }
 
-               # Update article, but only if changed.
+                       $oldtext = $this->getContent();
+                       $oldsize = strlen( $oldtext );
+                       $newsize = strlen( $text );
+                       $lastRevision = 0;
+                       $revisionId = 0;
 
-               # It's important that we either rollback or complete, otherwise an attacker could
-               # overwrite cur entries by sending precisely timed user aborts. Random bored users
-               # could conceivably have the same effect, especially if cur is locked for long periods.
-               if( !$wgDBtransactions ) {
-                       $userAbort = ignore_user_abort( true );
-               }
+                       if ( 0 != strcmp( $text, $oldtext ) ) {
+                               $this->mGoodAdjustment = (int)$this->isCountable( $text )
+                                 - (int)$this->isCountable( $oldtext );
+                               $this->mTotalAdjustment = 0;
+
+                               $lastRevision = $dbw->selectField(
+                                       'page', 'page_latest', array( 'page_id' => $this->getId() ) );
+
+                               if ( !$lastRevision ) {
+                                       # Article gone missing
+                                       wfDebug( __METHOD__.": EDIT_UPDATE specified but article doesn't exist\n" );
+                                       wfProfileOut( __METHOD__ );
+                                       return false;
+                               }
+                               
+                               $revision = new Revision( array(
+                                       'page'       => $this->getId(),
+                                       'comment'    => $summary,
+                                       'minor_edit' => $isminor,
+                                       'text'       => $text
+                                       ) );
+
+                               $dbw->begin();
+                               $revisionId = $revision->insertOn( $dbw );
+
+                               # Update page
+                               $ok = $this->updateRevisionOn( $dbw, $revision, $lastRevision );
+
+                               if( !$ok ) {
+                                       /* Belated edit conflict! Run away!! */
+                                       $good = false;
+                                       $dbw->rollback();
+                               } else {
+                                       # Update recentchanges
+                                       if( !( $flags & EDIT_SUPPRESS_RC ) ) {
+                                               $rcid = RecentChange::notifyEdit( $now, $this->mTitle, $isminor, $wgUser, $summary,
+                                                       $lastRevision, $this->getTimestamp(), $bot, '', $oldsize, $newsize,
+                                                       $revisionId );
+                                                       
+                                               # Mark as patrolled if the user can do so and has it set in their options
+                                               if( $wgUser->isAllowed( 'patrol' ) && $wgUser->getOption( 'autopatrol' ) ) {
+                                                       RecentChange::markPatrolled( $rcid );
+                                               }
+                                       }
+                                       $dbw->commit();
+                               }
+                       } else {
+                               // Keep the same revision ID, but do some updates on it
+                               $revisionId = $this->getRevIdFetched();
+                               // Update page_touched, this is usually implicit in the page update
+                               // Other cache updates are done in onArticleEdit()
+                               $this->mTitle->invalidateCache();
+                       }
 
-               $oldtext = $this->getContent();
-               $oldsize = strlen( $oldtext );
-               $newsize = strlen( $text );
-               $lastRevision = 0;
-               $revisionId = 0;
+                       if( !$wgDBtransactions ) {
+                               ignore_user_abort( $userAbort );
+                       }
+
+                       if ( $good ) {
+                               # 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 {
+                       # Create new article
+                       
+                       # Set statistics members
+                       # We work out if it's countable after PST to avoid counter drift 
+                       # when articles are created with {{subst:}}
+                       $this->mGoodAdjustment = (int)$this->isCountable( $text );
+                       $this->mTotalAdjustment = 1;
 
-               if ( 0 != strcmp( $text, $oldtext ) ) {
-                       $this->mGoodAdjustment = (int)$this->isCountable( $text )
-                         - (int)$this->isCountable( $oldtext );
-                       $this->mTotalAdjustment = 0;
-                       $now = wfTimestampNow();
+                       $dbw->begin();
 
-                       $lastRevision = $dbw->selectField(
-                               'page', 'page_latest', array( 'page_id' => $this->getId() ) );
+                       # Add the page record; stake our claim on this title!
+                       # This will fail with a database query exception if the article already exists
+                       $newid = $this->insertOn( $dbw );
 
+                       # Save the revision text...
                        $revision = new Revision( array(
-                               'page'       => $this->getId(),
+                               'page'       => $newid,
                                'comment'    => $summary,
                                'minor_edit' => $isminor,
                                'text'       => $text
                                ) );
-
-                       $dbw->immediateCommit();
-                       $dbw->begin();
                        $revisionId = $revision->insertOn( $dbw );
 
-                       # Update page
-                       $ok = $this->updateRevisionOn( $dbw, $revision, $lastRevision );
+                       $this->mTitle->resetArticleID( $newid );
 
-                       if( !$ok ) {
-                               /* Belated edit conflict! Run away!! */
-                               $good = false;
-                               $dbw->rollback();
-                       } else {
-                               # Update recentchanges and purge cache and whatnot
-                               require_once( 'RecentChange.php' );
-                               $bot = (int)($wgUser->isBot() || $forceBot);
-                               RecentChange::notifyEdit( $now, $this->mTitle, $isminor, $wgUser, $summary,
-                                       $lastRevision, $this->getTimestamp(), $bot, '', $oldsize, $newsize,
-                                       $revisionId );
-                               $dbw->commit();
+                       # Update the page record with revision data
+                       $this->updateRevisionOn( $dbw, $revision, 0 );
 
-                               // Update caches outside the main transaction
-                               Article::onArticleEdit( $this->mTitle );
-                       }
-               } else {
-                       // Keep the same revision ID, but do some updates on it
-                       $revisionId = $this->getRevIdFetched();
-               }
-
-               if( !$wgDBtransactions ) {
-                       ignore_user_abort( $userAbort );
-               }
-
-               if ( $good ) {
-                       if ($watchthis) {
-                               if (!$this->mTitle->userIsWatching()) {
-                                       $dbw->immediateCommit();
-                                       $dbw->begin();
-                                       $this->watch();
-                                       $dbw->commit();
-                               }
-                       } else {
-                               if ( $this->mTitle->userIsWatching() ) {
-                                       $dbw->immediateCommit();
-                                       $dbw->begin();
-                                       $this->unwatch();
-                                       $dbw->commit();
+                       if( !( $flags & EDIT_SUPPRESS_RC ) ) {
+                               $rcid = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $wgUser, $summary, $bot,
+                                 '', strlen( $text ), $revisionId );
+                               # Mark as patrolled if the user can and has the option set
+                               if( $wgUser->isAllowed( 'patrol' ) && $wgUser->getOption( 'autopatrol' ) ) {
+                                       RecentChange::markPatrolled( $rcid );
                                }
                        }
-                       # standard deferred updates
-                       $this->editUpdates( $text, $summary, $minor, $now, $revisionId );
-
+                       $dbw->commit();
 
-                       $urls = array();
-                       # Invalidate caches of all articles using this article as a template
-
-                       # Template namespace
-                       # Purge all articles linking here
-                       $titles = $this->mTitle->getTemplateLinksTo();
-                       Title::touchArray( $titles );
-                       if ( $wgUseSquid ) {
-                                       foreach ( $titles as $title ) {
-                                               $urls[] = $title->getInternalURL();
-                                       }
-                       }
+                       # Update links, etc.
+                       $this->editUpdates( $text, $summary, $isminor, $now, $revisionId, true );
 
-                       # Squid updates
-                       if ( $wgUseSquid ) {
-                               $urls = array_merge( $urls, $this->mTitle->getSquidURLs() );
-                               $u = new SquidUpdate( $urls );
-                               array_push( $wgPostCommitUpdateList, $u );
-                       }
+                       # Clear caches
+                       Article::onArticleCreate( $this->mTitle );
 
-                       # File cache
-                       if ( $wgUseFileCache ) {
-                               $cm = new CacheManager($this->mTitle);
-                               @unlink($cm->fileCacheName());
-                       }
+                       wfRunHooks( 'ArticleInsertComplete', array( &$this, &$wgUser, $text,
+                               $summary, $flags & EDIT_MINOR,
+                               null, null, &$flags ) );
+               }
 
-                       $this->showArticle( $text, wfMsg( 'updated' ), $sectionanchor, $isminor, $now, $summary, $lastRevision );
+               if ( $good && !( $flags & EDIT_DEFER_UPDATES ) ) {
+                       wfDoUpdates();
                }
+
                wfRunHooks( 'ArticleSaveComplete',
                        array( &$this, &$wgUser, $text,
-                       $summary, $minor,
-                       $watchthis, $sectionanchor ) );
-               wfProfileOut( $fname );
+                       $summary, $flags & EDIT_MINOR,
+                       null, null, &$flags ) );
+               
+               wfProfileOut( __METHOD__ );
                return $good;
        }
 
        /**
-        * After we've either updated or inserted the article, update
-        * the link tables and redirect to the new page.
+        * @deprecated wrapper for doRedirect
         */
        function showArticle( $text, $subtitle , $sectionanchor = '', $me2, $now, $summary, $oldid ) {
-               global $wgOut, $wgUser;
-               global $wgUseEnotif;
-
-               $fname = 'Article::showArticle';
-               wfProfileIn( $fname );
-
-               # Output the redirect
-               if( $this->isRedirect( $text ) )
-                       $r = 'redirect=no';
-               else
-                       $r = '';
-               $wgOut->redirect( $this->mTitle->getFullURL( $r ).$sectionanchor );
-
-               wfProfileOut( $fname );
+               $this->doRedirect( $this->isRedirect( $text ), $sectionanchor );
        }
 
+       /**
+        * Output a redirect back to the article.
+        * This is typically used after an edit.
+        *
+        * @param boolean $noRedir Add redirect=no
+        * @param string $sectionAnchor section to redirect to, including "#"
+        */
+       function doRedirect( $noRedir = false, $sectionAnchor = '' ) {
+               global $wgOut;
+               if ( $noRedir ) {
+                       $query = 'redirect=no';
+               } else {
+                       $query = '';
+               }
+               $wgOut->redirect( $this->mTitle->getFullURL( $query ) . $sectionAnchor );
+       }
+               
        /**
         * Mark this particular edit as patrolled
         */
        function markpatrolled() {
-               global $wgOut, $wgRequest, $wgOnlySysopsCanPatrol, $wgUseRCPatrol, $wgUser;
-               $wgOut->setRobotpolicy( 'noindex,follow' );
+               global $wgOut, $wgRequest, $wgUseRCPatrol, $wgUser;
+               $wgOut->setRobotpolicy( 'noindex,nofollow' );
 
                # Check RC patrol config. option
                if( !$wgUseRCPatrol ) {
@@ -1496,37 +1406,29 @@ class Article {
                }
                
                # Check permissions
-               if( $wgUser->isLoggedIn() ) {
-                       if( !$wgUser->isAllowed( 'patrol' ) ) {
-                               $wgOut->permissionRequired( 'patrol' );
-                               return;
-                       }
-               } else {
-                       $wgOut->loginToUse();
+               if( !$wgUser->isAllowed( 'patrol' ) ) {
+                       $wgOut->permissionRequired( 'patrol' );
                        return;
                }
                
                $rcid = $wgRequest->getVal( 'rcid' );
-               if ( !is_null ( $rcid ) )
-               {
-                       if( wfRunHooks( 'MarkPatrolled', array( &$rcid, &$wgUser, $wgOnlySysopsCanPatrol ) ) ) {
-                               require_once( 'RecentChange.php' );
+               if ( !is_null ( $rcid ) ) {
+                       if( wfRunHooks( 'MarkPatrolled', array( &$rcid, &$wgUser, false ) ) ) {
                                RecentChange::markPatrolled( $rcid );
-                               wfRunHooks( 'MarkPatrolledComplete', array( &$rcid, &$wgUser, $wgOnlySysopsCanPatrol ) );
+                               wfRunHooks( 'MarkPatrolledComplete', array( &$rcid, &$wgUser, false ) );
                                $wgOut->setPagetitle( wfMsg( 'markedaspatrolled' ) );
                                $wgOut->addWikiText( wfMsg( 'markedaspatrolledtext' ) );
                        }
                        $rcTitle = Title::makeTitle( NS_SPECIAL, 'Recentchanges' );
                        $wgOut->returnToMain( false, $rcTitle->getPrefixedText() );
                }
-               else
-               {
-                       $wgOut->errorpage( 'markedaspatrollederror', 'markedaspatrollederrortext' );
+               else {
+                       $wgOut->showErrorPage( 'markedaspatrollederror', 'markedaspatrollederrortext' );
                }
        }
 
        /**
-        * Add this page to $wgUser's watchlist
+        * User-interface handler for the "watch" action
         */
 
        function watch() {
@@ -1534,66 +1436,93 @@ class Article {
                global $wgUser, $wgOut;
 
                if ( $wgUser->isAnon() ) {
-                       $wgOut->errorpage( 'watchnologin', 'watchnologintext' );
+                       $wgOut->showErrorPage( 'watchnologin', 'watchnologintext' );
                        return;
                }
                if ( wfReadOnly() ) {
                        $wgOut->readOnlyPage();
                        return;
                }
-
-               if (wfRunHooks('WatchArticle', array(&$wgUser, &$this))) {
-
-                       $wgUser->addWatch( $this->mTitle );
-                       $wgUser->saveSettings();
-
-                       wfRunHooks('WatchArticleComplete', array(&$wgUser, &$this));
-
+               
+               if( $this->doWatch() ) {
                        $wgOut->setPagetitle( wfMsg( 'addedwatch' ) );
-                       $wgOut->setRobotpolicy( 'noindex,follow' );
+                       $wgOut->setRobotpolicy( 'noindex,nofollow' );
 
-                       $link = $this->mTitle->getPrefixedText();
+                       $link = wfEscapeWikiText( $this->mTitle->getPrefixedText() );
                        $text = wfMsg( 'addedwatchtext', $link );
                        $wgOut->addWikiText( $text );
                }
 
                $wgOut->returnToMain( true, $this->mTitle->getPrefixedText() );
        }
-
+       
        /**
-        * Stop watching a page
+        * Add this page to $wgUser's watchlist
+        * @return bool true on successful watch operation
         */
+       function doWatch() {
+               global $wgUser;
+               if( $wgUser->isAnon() ) {
+                       return false;
+               }
+               
+               if (wfRunHooks('WatchArticle', array(&$wgUser, &$this))) {
+                       $wgUser->addWatch( $this->mTitle );
+                       $wgUser->saveSettings();
 
+                       return wfRunHooks('WatchArticleComplete', array(&$wgUser, &$this));
+               }
+               
+               return false;
+       }
+
+       /**
+        * User interface handler for the "unwatch" action.
+        */
        function unwatch() {
 
                global $wgUser, $wgOut;
 
                if ( $wgUser->isAnon() ) {
-                       $wgOut->errorpage( 'watchnologin', 'watchnologintext' );
+                       $wgOut->showErrorPage( 'watchnologin', 'watchnologintext' );
                        return;
                }
                if ( wfReadOnly() ) {
                        $wgOut->readOnlyPage();
                        return;
                }
-
-               if (wfRunHooks('UnwatchArticle', array(&$wgUser, &$this))) {
-
-                       $wgUser->removeWatch( $this->mTitle );
-                       $wgUser->saveSettings();
-
-                       wfRunHooks('UnwatchArticleComplete', array(&$wgUser, &$this));
-
+               
+               if( $this->doUnwatch() ) {
                        $wgOut->setPagetitle( wfMsg( 'removedwatch' ) );
-                       $wgOut->setRobotpolicy( 'noindex,follow' );
+                       $wgOut->setRobotpolicy( 'noindex,nofollow' );
 
-                       $link = $this->mTitle->getPrefixedText();
+                       $link = wfEscapeWikiText( $this->mTitle->getPrefixedText() );
                        $text = wfMsg( 'removedwatchtext', $link );
                        $wgOut->addWikiText( $text );
                }
 
                $wgOut->returnToMain( true, $this->mTitle->getPrefixedText() );
        }
+       
+       /**
+        * Stop watching a page
+        * @return bool true on successful unwatch
+        */
+       function doUnwatch() {
+               global $wgUser;
+               if( $wgUser->isAnon() ) {
+                       return false;
+               }
+
+               if (wfRunHooks('UnwatchArticle', array(&$wgUser, &$this))) {
+                       $wgUser->removeWatch( $this->mTitle );
+                       $wgUser->saveSettings();
+
+                       return wfRunHooks('UnwatchArticleComplete', array(&$wgUser, &$this));
+               }
+               
+               return false;
+       }
 
        /**
         * action=protect handler
@@ -1619,47 +1548,63 @@ class Article {
         * @return bool true on success
         */
        function updateRestrictions( $limit = array(), $reason = '' ) {
-               global $wgUser, $wgOut, $wgRequest;
-
-               if ( !$wgUser->isAllowed( 'protect' ) ) {
-                       return false;
-               }
-
-               if( wfReadOnly() ) {
-                       return false;
-               }
-
+               global $wgUser, $wgRestrictionTypes, $wgContLang;
+               
                $id = $this->mTitle->getArticleID();
-               if ( 0 == $id ) {
+               if( !$wgUser->isAllowed( 'protect' ) || wfReadOnly() || $id == 0 ) {
                        return false;
                }
 
-               $flat = Article::flattenRestrictions( $limit );
-               $protecting = ($flat != '');
-
-               if( wfRunHooks( 'ArticleProtect', array( &$this, &$wgUser,
-                       $limit, $reason ) ) ) {
-
-                       $dbw =& wfGetDB( DB_MASTER );
-                       $dbw->update( 'page',
-                               array( /* SET */
-                                       'page_touched' => $dbw->timestamp(),
-                                       'page_restrictions' => $flat
-                               ), array( /* WHERE */
-                                       'page_id' => $id
-                               ), 'Article::protect'
-                       );
+               # FIXME: Same limitations as described in ProtectionForm.php (line 37);
+               # we expect a single selection, but the schema allows otherwise.
+               $current = array();
+               foreach( $wgRestrictionTypes as $action )
+                       $current[$action] = implode( '', $this->mTitle->getRestrictions( $action ) );
 
-                       wfRunHooks( 'ArticleProtectComplete', array( &$this, &$wgUser,
-                               $limit, $reason ) );
+               $current = Article::flattenRestrictions( $current );
+               $updated = Article::flattenRestrictions( $limit );
+               
+               $changed = ( $current != $updated );
+               $protect = ( $updated != '' );
+               
+               # If nothing's changed, do nothing
+               if( $changed ) {
+                       if( wfRunHooks( 'ArticleProtect', array( &$this, &$wgUser, $limit, $reason ) ) ) {
 
-                       $log = new LogPage( 'protect' );
-                       if( $protecting ) {
-                               $log->addEntry( 'protect', $this->mTitle, trim( $reason . " [$flat]" ) );
-                       } else {
-                               $log->addEntry( 'unprotect', $this->mTitle, $reason );
-                       }
-               }
+                               $dbw =& wfGetDB( DB_MASTER );
+                               
+                               # Prepare a null revision to be added to the history
+                               $comment = $wgContLang->ucfirst( wfMsgForContent( $protect ? 'protectedarticle' : 'unprotectedarticle', $this->mTitle->getPrefixedText() ) );
+                               if( $reason )
+                                       $comment .= ": $reason";
+                               if( $protect )
+                                       $comment .= " [$updated]";
+                               $nullRevision = Revision::newNullRevision( $dbw, $id, $comment, true );
+                               $nullRevId = $nullRevision->insertOn( $dbw );
+                       
+                               # Update page record
+                               $dbw->update( 'page',
+                                       array( /* SET */
+                                               'page_touched' => $dbw->timestamp(),
+                                               'page_restrictions' => $updated,
+                                               'page_latest' => $nullRevId
+                                       ), array( /* WHERE */
+                                               'page_id' => $id
+                                       ), 'Article::protect'
+                               );
+                               wfRunHooks( 'ArticleProtectComplete', array( &$this, &$wgUser, $limit, $reason ) );
+       
+                               # Update the protection log
+                               $log = new LogPage( 'protect' );
+                               if( $protect ) {
+                                       $log->addEntry( 'protect', $this->mTitle, trim( $reason . " [$updated]" ) );
+                               } else {
+                                       $log->addEntry( 'unprotect', $this->mTitle, $reason );
+                               }
+                               
+                       } # End hook
+               } # End "changed" check
+               
                return true;
        }
 
@@ -1668,13 +1613,14 @@ class Article {
         * suitable for insertion into the page_restrictions field.
         * @param array $limit
         * @return string
-        * @access private
+        * @private
         */
        function flattenRestrictions( $limit ) {
                if( !is_array( $limit ) ) {
-                       wfDebugDieBacktrace( 'Article::flattenRestrictions given non-array restriction set' );
+                       throw new MWException( 'Article::flattenRestrictions given non-array restriction set' );
                }
                $bits = array();
+               ksort( $limit );
                foreach( $limit as $action => $restrictions ) {
                        if( $restrictions != '' ) {
                                $bits[] = "$action=$restrictions";
@@ -1688,7 +1634,6 @@ class Article {
         */
        function delete() {
                global $wgUser, $wgOut, $wgRequest;
-               $fname = 'Article::delete';
                $confirm = $wgRequest->wasPosted() &&
                        $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) );
                $reason = $wgRequest->getText( 'wpReason' );
@@ -1697,7 +1642,7 @@ class Article {
 
                # Check permissions
                if( $wgUser->isAllowed( 'delete' ) ) {
-                       if( $wgUser->isBlocked() ) {
+                       if( $wgUser->isBlocked( !$confirm ) ) {
                                $wgOut->blockedPage();
                                return;
                        }
@@ -1716,9 +1661,9 @@ class Article {
                # Better double-check that it hasn't been deleted yet!
                $dbw =& wfGetDB( DB_MASTER );
                $conds = $this->mTitle->pageCond();
-               $latest = $dbw->selectField( 'page', 'page_latest', $conds, $fname );
+               $latest = $dbw->selectField( 'page', 'page_latest', $conds, __METHOD__ );
                if ( $latest === false ) {
-                       $wgOut->fatalError( wfMsg( 'cannotdelete' ) );
+                       $wgOut->showFatalError( wfMsg( 'cannotdelete' ) );
                        return;
                }
 
@@ -1734,8 +1679,7 @@ class Article {
                
                if( count( $authors ) > 1 && !$confirm ) {
                        $skin=$wgUser->getSkin();
-                       $wgOut->addHTML('<b>'.wfMsg('historywarning'));
-                       $wgOut->addHTML( $skin->historyLink() .'</b>');
+                       $wgOut->addHTML( '<strong>' . wfMsg( 'historywarning' ) . ' ' . $skin->historyLink() . '</strong>' );
                }
 
                # If a single user is responsible for all revisions, find out who they are
@@ -1801,14 +1745,13 @@ class Article {
        }
 
        /**
-        * Get the last N authors 
+        * Get the last N authors
         * @param int $num Number of revisions to get
         * @param string $revLatest The latest rev_id, selected from the master (optional)
         * @return array Array of authors, duplicates not removed
         */
        function getLastNAuthors( $num, $revLatest = 0 ) {
-               $fname = 'Article::getLastNAuthors';
-               wfProfileIn( $fname );
+               wfProfileIn( __METHOD__ );
 
                // First try the slave
                // If that doesn't have the latest revision, try the master
@@ -1821,13 +1764,13 @@ class Article {
                                        'page_namespace' => $this->mTitle->getNamespace(),
                                        'page_title' => $this->mTitle->getDBkey(),
                                        'rev_page = page_id'
-                               ), $fname, $this->getSelectOptions( array( 
+                               ), __METHOD__, $this->getSelectOptions( array(
                                        'ORDER BY' => 'rev_timestamp DESC',
                                        'LIMIT' => $num
                                ) )
                        );
                        if ( !$res ) {
-                               wfProfileOut( $fname );
+                               wfProfileOut( __METHOD__ );
                                return array();
                        }
                        $row = $db->fetchObject( $res );
@@ -1843,7 +1786,7 @@ class Article {
                while ( $row = $db->fetchObject( $res ) ) {
                        $authors[] = $row->rev_user_text;
                }
-               wfProfileOut( $fname );
+               wfProfileOut( __METHOD__ );
                return $authors;
        }
        
@@ -1895,13 +1838,12 @@ class Article {
         * Perform a deletion and output success or failure messages
         */
        function doDelete( $reason ) {
-               global $wgOut, $wgUser, $wgContLang;
-               $fname = 'Article::doDelete';
-               wfDebug( $fname."\n" );
+               global $wgOut, $wgUser;
+               wfDebug( __METHOD__."\n" );
 
                if (wfRunHooks('ArticleDelete', array(&$this, &$wgUser, &$reason))) {
                        if ( $this->doDeleteArticle( $reason ) ) {
-                               $deleted = $this->mTitle->getPrefixedText();
+                               $deleted = wfEscapeWikiText( $this->mTitle->getPrefixedText() );
 
                                $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) );
                                $wgOut->setRobotpolicy( 'noindex,nofollow' );
@@ -1913,7 +1855,7 @@ class Article {
                                $wgOut->returnToMain( false );
                                wfRunHooks('ArticleDeleteComplete', array(&$this, &$wgUser, $reason));
                        } else {
-                               $wgOut->fatalError( wfMsg( 'cannotdelete' ) );
+                               $wgOut->showFatalError( wfMsg( 'cannotdelete' ) );
                        }
                }
        }
@@ -1924,11 +1866,10 @@ class Article {
         * Returns success
         */
        function doDeleteArticle( $reason ) {
-               global $wgUser, $wgUseSquid, $wgDeferredUpdateList;
+               global $wgUseSquid, $wgDeferredUpdateList;
                global $wgPostCommitUpdateList, $wgUseTrackbacks;
 
-               $fname = 'Article::doDeleteArticle';
-               wfDebug( $fname."\n" );
+               wfDebug( __METHOD__."\n" );
 
                $dbw =& wfGetDB( DB_MASTER );
                $ns = $this->mTitle->getNamespace();
@@ -1942,24 +1883,6 @@ class Article {
                $u = new SiteStatsUpdate( 0, 1, -(int)$this->isCountable( $this->getContent() ), -1 );
                array_push( $wgDeferredUpdateList, $u );
 
-               $linksTo = $this->mTitle->getLinksTo();
-
-               # Squid purging
-               if ( $wgUseSquid ) {
-                       $urls = array(
-                               $this->mTitle->getInternalURL(),
-                               $this->mTitle->getInternalURL( 'history' )
-                       );
-
-                       $u = SquidUpdate::newFromTitles( $linksTo, $urls );
-                       array_push( $wgPostCommitUpdateList, $u );
-
-               }
-
-               # Client and file cache invalidation
-               Title::touchArray( $linksTo );
-
-
                // 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
@@ -1984,28 +1907,38 @@ class Article {
                        ), array(
                                'page_id' => $id,
                                'page_id = rev_page'
-                       ), $fname
+                       ), __METHOD__
                );
 
                # Now that it's safely backed up, delete it
-               $dbw->delete( 'revision', array( 'rev_page' => $id ), $fname );
-               $dbw->delete( 'page', array( 'page_id' => $id ), $fname);
+               $dbw->delete( 'page', array( 'page_id' => $id ), __METHOD__);
 
-               if ($wgUseTrackbacks)
-                       $dbw->delete( 'trackbacks', array( 'tb_page' => $id ), $fname );
+               # If using cascading deletes, we can skip some explicit deletes
+               if ( !$dbw->cascadingDeletes() ) {
 
-               # Clean up recentchanges entries...
-               $dbw->delete( 'recentchanges', array( 'rc_namespace' => $ns, 'rc_title' => $t ), $fname );
+                       $dbw->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ );
 
-               # Finally, clean up the link tables
-               $t = $this->mTitle->getPrefixedDBkey();
+                       if ($wgUseTrackbacks)
+                               $dbw->delete( 'trackbacks', array( 'tb_page' => $id ), __METHOD__ );
 
-               Article::onArticleDelete( $this->mTitle );
+                       # Delete outgoing links
+                       $dbw->delete( 'pagelinks', array( 'pl_from' => $id ) );
+                       $dbw->delete( 'imagelinks', array( 'il_from' => $id ) );
+                       $dbw->delete( 'categorylinks', array( 'cl_from' => $id ) );
+                       $dbw->delete( 'templatelinks', array( 'tl_from' => $id ) );
+                       $dbw->delete( 'externallinks', array( 'el_from' => $id ) );
+                       $dbw->delete( 'langlinks', array( 'll_from' => $id ) );
+               }
 
-               # Delete outgoing links
-               $dbw->delete( 'pagelinks', array( 'pl_from' => $id ) );
-               $dbw->delete( 'imagelinks', array( 'il_from' => $id ) );
-               $dbw->delete( 'categorylinks', array( 'cl_from' => $id ) );
+               # If using cleanup triggers, we can skip some manual deletes
+               if ( !$dbw->cleanupTriggers() ) {
+
+                       # Clean up recentchanges entries...
+                       $dbw->delete( 'recentchanges', array( 'rc_namespace' => $ns, 'rc_title' => $t ), __METHOD__ );
+               }
+
+               # Clear caches
+               Article::onArticleDelete( $this->mTitle );
 
                # Log the deletion
                $log = new LogPage( 'delete' );
@@ -2022,7 +1955,6 @@ class Article {
         */
        function rollback() {
                global $wgUser, $wgOut, $wgRequest, $wgUseRCPatrol;
-               $fname = 'Article::rollback';
 
                if( $wgUser->isAllowed( 'rollback' ) ) {
                        if( $wgUser->isBlocked() ) {
@@ -2054,12 +1986,10 @@ class Article {
                $tt = $this->mTitle->getDBKey();
                $n = $this->mTitle->getNamespace();
 
-               # Get the last editor, lock table exclusively
-               $dbw->begin();
+               # Get the last editor
                $current = Revision::newFromTitle( $this->mTitle );
                if( is_null( $current ) ) {
                        # Something wrong... no page?
-                       $dbw->rollback();
                        $wgOut->addHTML( wfMsg( 'notanarticle' ) );
                        return;
                }
@@ -2087,14 +2017,13 @@ class Article {
                        array(
                                'rev_page' => $current->getPage(),
                                "rev_user <> {$user} OR rev_user_text <> {$user_text}"
-                       ), $fname,
+                       ), __METHOD__,
                        array(
                                'USE INDEX' => 'page_timestamp',
                                'ORDER BY'  => 'rev_timestamp DESC' )
                        );
                if( $s === false ) {
                        # Something wrong
-                       $dbw->rollback();
                        $wgOut->setPageTitle(wfMsg('rollbackfailed'));
                        $wgOut->addHTML( wfMsg( 'cantrollback' ) );
                        return;
@@ -2116,7 +2045,7 @@ class Article {
                                        'rc_cur_id'    => $current->getPage(),
                                        'rc_user_text' => $current->getUserText(),
                                        "rc_timestamp > '{$s->rev_timestamp}'",
-                               ), $fname
+                               ), __METHOD__
                        );
                }
 
@@ -2131,16 +2060,14 @@ class Article {
                $wgOut->addHTML( '<h2>' . htmlspecialchars( $newComment ) . "</h2>\n<hr />\n" );
 
                $this->updateArticle( $target->getText(), $newComment, 1, $this->mTitle->userIsWatching(), $bot );
-               Article::onArticleEdit( $this->mTitle );
 
-               $dbw->commit();
                $wgOut->returnToMain( false );
        }
 
 
        /**
         * Do standard deferred updates after page view
-        * @access private
+        * @private
         */
        function viewUpdates() {
                global $wgDeferredUpdateList;
@@ -2161,18 +2088,25 @@ class Article {
 
        /**
         * Do standard deferred updates after page edit.
+        * Update links tables, site stats, search index and message cache.
         * Every 1000th edit, prune the recent changes table.
-        * @access private
-        * @param string $text
+        * 
+        * @private
+        * @param $text New text of the article
+        * @param $summary Edit summary
+        * @param $minoredit Minor edit
+        * @param $timestamp_of_pagechange Timestamp associated with the page change
+        * @param $newid rev_id value of the new revision
+        * @param $changed Whether or not the content actually changed
         */
-       function editUpdates( $text, $summary, $minoredit, $timestamp_of_pagechange, $newid) {
+       function editUpdates( $text, $summary, $minoredit, $timestamp_of_pagechange, $newid, $changed = true ) {
                global $wgDeferredUpdateList, $wgMessageCache, $wgUser, $wgParser;
 
-               $fname = 'Article::editUpdates';
-               wfProfileIn( $fname );
+               wfProfileIn( __METHOD__ );
 
                # Parse the text
                $options = new ParserOptions;
+               $options->setTidy(true);
                $poutput = $wgParser->parse( $text, $this->mTitle, $options, true, true, $newid );
 
                # Save it to the parser cache
@@ -2202,7 +2136,7 @@ class Article {
                $shortTitle = $this->mTitle->getDBkey();
 
                if ( 0 == $id ) {
-                       wfProfileOut( $fname );
+                       wfProfileOut( __METHOD__ );
                        return;
                }
 
@@ -2212,16 +2146,19 @@ class Article {
                array_push( $wgDeferredUpdateList, $u );
 
                # If this is another user's talk page, update newtalk
-
-               if ($this->mTitle->getNamespace() == NS_USER_TALK && $shortTitle != $wgUser->getName()) {
-                       $other = User::newFromName( $shortTitle );
-                       if( is_null( $other ) && User::isIP( $shortTitle ) ) {
-                               // An anonymous user
-                               $other = new User();
-                               $other->setName( $shortTitle );
-                       }
-                       if( $other ) {
-                               $other->setNewtalk( true );
+               # Don't do this if $changed = false otherwise some idiot can null-edit a
+               # load of user talk pages and piss people off
+               if( $this->mTitle->getNamespace() == NS_USER_TALK && $shortTitle != $wgUser->getTitleKey() && $changed ) {
+                       if (wfRunHooks('ArticleEditUpdateNewTalk', array(&$this)) ) {
+                               $other = User::newFromName( $shortTitle );
+                               if( is_null( $other ) && User::isIP( $shortTitle ) ) {
+                                       // An anonymous user
+                                       $other = new User();
+                                       $other->setName( $shortTitle );
+                               }
+                               if( $other ) {
+                                       $other->setNewtalk( true );
+                               }
                        }
                }
 
@@ -2229,28 +2166,51 @@ class Article {
                        $wgMessageCache->replace( $shortTitle, $text );
                }
 
-               wfProfileOut( $fname );
+               wfProfileOut( __METHOD__ );
        }
 
        /**
-        * @todo document this function
-        * @access private
+        * Generate the navigation links when browsing through an article revisions
+        * It shows the information as:
+        *   Revision as of \<date\>; view current revision
+        *   \<- Previous version | Next Version -\>
+        *
+        * @private
         * @param string $oldid         Revision ID of this article revision
         */
        function setOldSubtitle( $oldid=0 ) {
                global $wgLang, $wgOut, $wgUser;
 
+               if ( !wfRunHooks( 'DisplayOldSubtitle', array(&$this, &$oldid) ) ) {
+                               return; 
+               }       
+
+               $revision = Revision::newFromId( $oldid );
+
                $current = ( $oldid == $this->mLatest );
                $td = $wgLang->timeanddate( $this->mTimestamp, true );
                $sk = $wgUser->getSkin();
                $lnk = $current
                        ? wfMsg( 'currentrevisionlink' )
                        : $lnk = $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'currentrevisionlink' ) );
-               $prevlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'previousrevision' ), 'direction=prev&oldid='.$oldid );
+               $prev = $this->mTitle->getPreviousRevisionID( $oldid ) ;
+               $prevlink = $prev
+                       ? $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'previousrevision' ), 'direction=prev&oldid='.$oldid )
+                       : wfMsg( 'previousrevision' );
+               $prevdiff = $prev
+                       ? $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'diff' ), 'diff=prev&oldid='.$oldid )
+                       : wfMsg( 'diff' );
                $nextlink = $current
                        ? wfMsg( 'nextrevision' )
                        : $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'nextrevision' ), 'direction=next&oldid='.$oldid );
-               $r = wfMsg( 'revisionasofwithlink', $td, $lnk, $prevlink, $nextlink );
+               $nextdiff = $current
+                       ? 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() );
+               
+               $r = wfMsg( 'old-revision-navigation', $td, $lnk, $prevlink, $nextlink, $userlinks, $prevdiff, $nextdiff );
                $wgOut->setSubtitle( $r );
        }
 
@@ -2275,7 +2235,7 @@ class Article {
        function tryFileCache() {
                static $called = false;
                if( $called ) {
-                       wfDebug( " tryFileCache() -- called twice!?\n" );
+                       wfDebug( "Article::tryFileCache(): called twice!?\n" );
                        return;
                }
                $called = true;
@@ -2283,16 +2243,15 @@ class Article {
                        $touched = $this->mTouched;
                        $cache = new CacheManager( $this->mTitle );
                        if($cache->isFileCacheGood( $touched )) {
-                               global $wgOut;
-                               wfDebug( " tryFileCache() - about to load\n" );
+                               wfDebug( "Article::tryFileCache(): about to load file\n" );
                                $cache->loadFromFileCache();
                                return true;
                        } else {
-                               wfDebug( " tryFileCache() - starting buffer\n" );
+                               wfDebug( "Article::tryFileCache(): starting buffer\n" );
                                ob_start( array(&$cache, 'saveToFileCache' ) );
                        }
                } else {
-                       wfDebug( " tryFileCache() - not cacheable\n" );
+                       wfDebug( "Article::tryFileCache(): not cacheable\n" );
                }
        }
 
@@ -2323,13 +2282,8 @@ class Article {
         *
         */
        function checkTouched() {
-               $fname = 'Article::checkTouched';
                if( !$this->mDataLoaded ) {
-                       $dbr =& $this->getDB();
-                       $data = $this->pageDataFromId( $dbr, $this->getId() );
-                       if( $data ) {
-                               $this->loadPageData( $data );
-                       }
+                       $this->loadPageData();
                }
                return !$this->mIsRedirect;
        }
@@ -2340,15 +2294,21 @@ class Article {
        function getTouched() {
                # Ensure that page data has been loaded
                if( !$this->mDataLoaded ) {
-                       $dbr =& $this->getDB();
-                       $data = $this->pageDataFromId( $dbr, $this->getId() );
-                       if( $data ) {
-                               $this->loadPageData( $data );
-                       }
+                       $this->loadPageData();
                }
                return $this->mTouched;
        }
 
+       /**
+        * Get the page_latest field
+        */
+       function getLatest() {
+               if ( !$this->mDataLoaded ) {
+                       $this->loadPageData();
+               }
+               return $this->mLatest;
+       }
+
        /**
         * Edit an article without doing all that other stuff
         * The article must already exist; link tables etc
@@ -2359,8 +2319,7 @@ class Article {
         * @param bool $minor whereas it's a minor modification
         */
        function quickEdit( $text, $comment = '', $minor = 0 ) {
-               $fname = 'Article::quickEdit';
-               wfProfileIn( $fname );
+               wfProfileIn( __METHOD__ );
 
                $dbw =& wfGetDB( DB_MASTER );
                $dbw->begin();
@@ -2370,11 +2329,12 @@ class Article {
                        'comment'    => $comment,
                        'minor_edit' => $minor ? 1 : 0,
                        ) );
+               # fixme : $revisionId never used
                $revisionId = $revision->insertOn( $dbw );
                $this->updateRevisionOn( $dbw, $revision );
                $dbw->commit();
 
-               wfProfileOut( $fname );
+               wfProfileOut( __METHOD__ );
        }
 
        /**
@@ -2385,7 +2345,7 @@ class Article {
         */
        function incViewCount( $id ) {
                $id = intval( $id );
-               global $wgHitcounterUpdateFreq;
+               global $wgHitcounterUpdateFreq, $wgDBtype;
 
                $dbw =& wfGetDB( DB_MASTER );
                $pageTable = $dbw->tableName( 'page' );
@@ -2416,12 +2376,15 @@ class Article {
                        wfProfileIn( 'Article::incViewCount-collect' );
                        $old_user_abort = ignore_user_abort( true );
 
-                       $dbw->query("LOCK TABLES $hitcounterTable WRITE");
-                       $dbw->query("CREATE TEMPORARY TABLE $acchitsTable TYPE=HEAP ".
+                       if ($wgDBtype == 'mysql')
+                               $dbw->query("LOCK TABLES $hitcounterTable WRITE");
+                       $tabletype = $wgDBtype == 'mysql' ? "ENGINE=HEAP " : '';
+                       $dbw->query("CREATE TEMPORARY TABLE $acchitsTable $tabletype".
                                "SELECT hc_id,COUNT(*) AS hc_n FROM $hitcounterTable ".
                                'GROUP BY hc_id');
                        $dbw->query("DELETE FROM $hitcounterTable");
-                       $dbw->query('UNLOCK TABLES');
+                       if ($wgDBtype == 'mysql')
+                               $dbw->query('UNLOCK TABLES');
                        $dbw->query("UPDATE $pageTable,$acchitsTable SET page_counter=page_counter + hc_n ".
                                'WHERE page_id = hc_id');
                        $dbw->query("DROP TABLE $acchitsTable");
@@ -2444,27 +2407,31 @@ class Article {
         * @param $title_obj a title object
         */
 
-       function onArticleCreate($title_obj) {
-               global $wgUseSquid, $wgPostCommitUpdateList;
-
-               $title_obj->touchLinks();
-               $titles = $title_obj->getLinksTo();
-
-               # Purge squid
-               if ( $wgUseSquid ) {
-                       $urls = $title_obj->getSquidURLs();
-                       foreach ( $titles as $linkTitle ) {
-                               $urls[] = $linkTitle->getInternalURL();
-                       }
-                       $u = new SquidUpdate( $urls );
-                       array_push( $wgPostCommitUpdateList, $u );
+       static function onArticleCreate($title) {
+               # The talk page isn't in the regular link tables, so we need to update manually:
+               if ( $title->isTalkPage() ) {
+                       $other = $title->getSubjectPage();
+               } else {
+                       $other = $title->getTalkPage();
                }
+               $other->invalidateCache();
+               $other->purgeSquid();
+
+               $title->touchLinks();
+               $title->purgeSquid();
        }
 
-       function onArticleDelete( $title ) {
-               global $wgMessageCache;
+       static function onArticleDelete( $title ) {
+               global $wgUseFileCache, $wgMessageCache;
 
                $title->touchLinks();
+               $title->purgeSquid();
+               
+               # File cache
+               if ( $wgUseFileCache ) {
+                       $cm = new CacheManager( $title );
+                       @unlink( $cm->fileCacheName() );
+               }
 
                if( $title->getNamespace() == NS_MEDIAWIKI) {
                        $wgMessageCache->replace( $title->getDBkey(), false );
@@ -2474,31 +2441,19 @@ class Article {
        /**
         * Purge caches on page update etc
         */
-       function onArticleEdit( $title ) {
-               global $wgUseSquid, $wgPostCommitUpdateList, $wgUseFileCache;
+       static function onArticleEdit( $title ) {
+               global $wgDeferredUpdateList, $wgUseFileCache;
 
                $urls = array();
 
-               // Template namespace? Purge all articles linking here.
-               // FIXME: When a templatelinks table arrives, use it for all includes.
-               if ( $title->getNamespace() == NS_TEMPLATE) {
-                       $titles = $title->getLinksTo();
-                       Title::touchArray( $titles );
-                       if ( $wgUseSquid ) {
-                               foreach ( $titles as $link ) {
-                                       $urls[] = $link->getInternalURL();
-                               }
-                       }
-               }
+               // Invalidate caches of articles which include this page
+               $update = new HTMLCacheUpdate( $title, 'templatelinks' );
+               $wgDeferredUpdateList[] = $update;
 
-               # Squid updates
-               if ( $wgUseSquid ) {
-                       $urls = array_merge( $urls, $title->getSquidURLs() );
-                       $u = new SquidUpdate( $urls );
-                       array_push( $wgPostCommitUpdateList, $u );
-               }
+               # Purge squid for this page only
+               $title->purgeSquid();
 
-               # File cache
+               # Clear file cache
                if ( $wgUseFileCache ) {
                        $cm = new CacheManager( $title );
                        @unlink( $cm->fileCacheName() );
@@ -2511,14 +2466,13 @@ class Article {
         * Info about this page
         * Called for ?action=info when $wgAllowPageInfo is on.
         *
-        * @access public
+        * @public
         */
        function info() {
                global $wgLang, $wgOut, $wgAllowPageInfo, $wgUser;
-               $fname = 'Article::info';
 
                if ( !$wgAllowPageInfo ) {
-                       $wgOut->errorpage( 'nosuchaction', 'nosuchactiontext' );
+                       $wgOut->showErrorPage( 'nosuchaction', 'nosuchactiontext' );
                        return;
                }
 
@@ -2544,7 +2498,7 @@ class Article {
                                'watchlist',
                                'COUNT(*)',
                                $wl_clause,
-                               $fname,
+                               __METHOD__,
                                $this->getSelectOptions() );
 
                        $pageInfo = $this->pageCountInfo( $page );
@@ -2570,7 +2524,7 @@ class Article {
         *
         * @param Title $title
         * @return array
-        * @access private
+        * @private
         */
        function pageCountInfo( $title ) {
                $id = $title->getArticleId();
@@ -2581,20 +2535,19 @@ class Article {
                $dbr =& wfGetDB( DB_SLAVE );
 
                $rev_clause = array( 'rev_page' => $id );
-               $fname = 'Article::pageCountInfo';
 
                $edits = $dbr->selectField(
                        'revision',
                        'COUNT(rev_page)',
                        $rev_clause,
-                       $fname,
+                       __METHOD__,
                        $this->getSelectOptions() );
 
                $authors = $dbr->selectField(
                        'revision',
                        'COUNT(DISTINCT rev_user_text)',
                        $rev_clause,
-                       $fname,
+                       __METHOD__,
                        $this->getSelectOptions() );
 
                return array( 'edits' => $edits, 'authors' => $authors );
@@ -2609,6 +2562,9 @@ class Article {
        function getUsedTemplates() {
                $result = array();
                $id = $this->mTitle->getArticleID();
+               if( $id == 0 ) {
+                       return array();
+               }
 
                $dbr =& wfGetDB( DB_SLAVE );
                $res = $dbr->select( array( 'templatelinks' ),