merged master
[lhc/web/wiklou.git] / includes / Revision.php
index 6861605..aa0d831 100644 (file)
@@ -23,7 +23,7 @@
 /**
  * @todo document
  */
-class Revision {
+class Revision implements IDBAccessObject {
        protected $mId;
        protected $mPage;
        protected $mUserText;
@@ -51,7 +51,7 @@ class Revision {
        const DELETED_RESTRICTED = 8;
        // Convenience field
        const SUPPRESSED_USER = 12;
-       // Audience options for Revision::getText()
+       // Audience options for accessors
        const FOR_PUBLIC = 1;
        const FOR_THIS_USER = 2;
        const RAW = 3;
@@ -60,11 +60,17 @@ class Revision {
         * Load a page revision from a given revision ID number.
         * Returns null if no such revision can be found.
         *
+        * $flags include:
+        *      IDBAccessObject::LATEST_READ  : Select the data from the master
+        *      IDBAccessObject::LOCKING_READ : Select & lock the data from the master
+        *      IDBAccessObject::AVOID_MASTER : Avoid master queries; data may be stale
+        *
         * @param $id Integer
+        * @param $flags Integer (optional)
         * @return Revision or null
         */
-       public static function newFromId( $id ) {
-               return Revision::newFromConds( array( 'rev_id' => intval( $id ) ) );
+       public static function newFromId( $id, $flags = 0 ) {
+               return self::newFromConds( array( 'rev_id' => intval( $id ) ), $flags );
        }
 
        /**
@@ -72,11 +78,17 @@ class Revision {
         * that's attached to a given title. If not attached
         * to that title, will return null.
         *
+        * $flags include:
+        *      IDBAccessObject::LATEST_READ  : Select the data from the master
+        *      IDBAccessObject::LOCKING_READ : Select & lock the data from the master
+        *      IDBAccessObject::AVOID_MASTER : Avoid master queries; data may be stale
+        *
         * @param $title Title
         * @param $id Integer (optional)
+        * @param $flags Integer Bitfield (optional)
         * @return Revision or null
         */
-       public static function newFromTitle( $title, $id = 0 ) {
+       public static function newFromTitle( $title, $id = 0, $flags = 0 ) {
                $conds = array(
                        'page_namespace' => $title->getNamespace(),
                        'page_title'     => $title->getDBkey()
@@ -84,7 +96,7 @@ class Revision {
                if ( $id ) {
                        // Use the specified ID
                        $conds['rev_id'] = $id;
-               } elseif ( wfGetLB()->getServerCount() > 1 ) {
+               } elseif ( !( $flags & self::AVOID_MASTER ) && wfGetLB()->getServerCount() > 1 ) {
                        // Get the latest revision ID from the master
                        $dbw = wfGetDB( DB_MASTER );
                        $latest = $dbw->selectField( 'page', 'page_latest', $conds, __METHOD__ );
@@ -96,7 +108,7 @@ class Revision {
                        // Use a join to get the latest revision
                        $conds[] = 'rev_id=page_latest';
                }
-               return Revision::newFromConds( $conds );
+               return self::newFromConds( $conds, $flags );
        }
 
        /**
@@ -104,15 +116,21 @@ class Revision {
         * that's attached to a given page ID.
         * Returns null if no such revision can be found.
         *
+        * $flags include:
+        *      IDBAccessObject::LATEST_READ  : Select the data from the master
+        *      IDBAccessObject::LOCKING_READ : Select & lock the data from the master
+        *      IDBAccessObject::AVOID_MASTER : Avoid master queries; data may be stale
+        *
         * @param $revId Integer
         * @param $pageId Integer (optional)
+        * @param $flags Integer Bitfield (optional)
         * @return Revision or null
         */
-       public static function newFromPageId( $pageId, $revId = 0 ) {
+       public static function newFromPageId( $pageId, $revId = 0, $flags = 0 ) {
                $conds = array( 'page_id' => $pageId );
                if ( $revId ) {
                        $conds['rev_id'] = $revId;
-               } elseif ( wfGetLB()->getServerCount() > 1 ) {
+               } elseif ( !( $flags & self::AVOID_MASTER ) && wfGetLB()->getServerCount() > 1 ) {
                        // Get the latest revision ID from the master
                        $dbw = wfGetDB( DB_MASTER );
                        $latest = $dbw->selectField( 'page', 'page_latest', $conds, __METHOD__ );
@@ -123,7 +141,7 @@ class Revision {
                } else {
                        $conds[] = 'rev_id = page_latest';
                }
-               return Revision::newFromConds( $conds );
+               return self::newFromConds( $conds, $flags );
        }
 
        /**
@@ -189,7 +207,7 @@ class Revision {
         * @return Revision or null
         */
        public static function loadFromId( $db, $id ) {
-               return Revision::loadFromConds( $db, array( 'rev_id' => intval( $id ) ) );
+               return self::loadFromConds( $db, array( 'rev_id' => intval( $id ) ) );
        }
 
        /**
@@ -209,7 +227,7 @@ class Revision {
                } else {
                        $conds[] = 'rev_id=page_latest';
                }
-               return Revision::loadFromConds( $db, $conds );
+               return self::loadFromConds( $db, $conds );
        }
 
        /**
@@ -228,7 +246,7 @@ class Revision {
                } else {
                        $matchId = 'page_latest';
                }
-               return Revision::loadFromConds( $db,
+               return self::loadFromConds( $db,
                        array( "rev_id=$matchId",
                                   'page_namespace' => $title->getNamespace(),
                                   'page_title'     => $title->getDBkey() )
@@ -246,7 +264,7 @@ class Revision {
         * @return Revision or null
         */
        public static function loadFromTimestamp( $db, $title, $timestamp ) {
-               return Revision::loadFromConds( $db,
+               return self::loadFromConds( $db,
                        array( 'rev_timestamp'  => $db->timestamp( $timestamp ),
                                   'page_namespace' => $title->getNamespace(),
                                   'page_title'     => $title->getDBkey() )
@@ -257,14 +275,17 @@ class Revision {
         * Given a set of conditions, fetch a revision.
         *
         * @param $conditions Array
+        * @param $flags integer (optional)
         * @return Revision or null
         */
-       public static function newFromConds( $conditions ) {
-               $db = wfGetDB( DB_SLAVE );
-               $rev = Revision::loadFromConds( $db, $conditions );
-               if( is_null( $rev ) && wfGetLB()->getServerCount() > 1 ) {
-                       $dbw = wfGetDB( DB_MASTER );
-                       $rev = Revision::loadFromConds( $dbw, $conditions );
+       private static function newFromConds( $conditions, $flags = 0 ) {
+               $db = wfGetDB( ( $flags & self::LATEST_READ ) ? DB_MASTER : DB_SLAVE );
+               $rev = self::loadFromConds( $db, $conditions, $flags );
+               if ( is_null( $rev ) && wfGetLB()->getServerCount() > 1 ) {
+                       if ( !( $flags & self::LATEST_READ ) && !( $flags & self::AVOID_MASTER ) ) {
+                               $dbw = wfGetDB( DB_MASTER );
+                               $rev = self::loadFromConds( $dbw, $conditions, $flags );
+                       }
                }
                return $rev;
        }
@@ -275,10 +296,11 @@ class Revision {
         *
         * @param $db DatabaseBase
         * @param $conditions Array
+        * @param $flags integer (optional)
         * @return Revision or null
         */
-       private static function loadFromConds( $db, $conditions ) {
-               $res = Revision::fetchFromConds( $db, $conditions );
+       private static function loadFromConds( $db, $conditions, $flags = 0 ) {
+               $res = self::fetchFromConds( $db, $conditions, $flags );
                if( $res ) {
                        $row = $res->fetchObject();
                        if( $row ) {
@@ -299,7 +321,7 @@ class Revision {
         * @return ResultWrapper
         */
        public static function fetchRevision( $title ) {
-               return Revision::fetchFromConds(
+               return self::fetchFromConds(
                        wfGetDB( DB_SLAVE ),
                        array( 'rev_id=page_latest',
                                   'page_namespace' => $title->getNamespace(),
@@ -314,20 +336,25 @@ class Revision {
         *
         * @param $db DatabaseBase
         * @param $conditions Array
+        * @param $flags integer (optional)
         * @return ResultWrapper
         */
-       private static function fetchFromConds( $db, $conditions ) {
+       private static function fetchFromConds( $db, $conditions, $flags = 0 ) {
                $fields = array_merge(
                        self::selectFields(),
                        self::selectPageFields(),
                        self::selectUserFields()
                );
+               $options = array( 'LIMIT' => 1 );
+               if ( $flags & self::FOR_UPDATE ) {
+                       $options[] = 'FOR UPDATE';
+               }
                return $db->select(
                        array( 'revision', 'page', 'user' ),
                        $fields,
                        $conditions,
                        __METHOD__,
-                       array( 'LIMIT' => 1 ),
+                       $options,
                        array( 'page' => self::pageJoinCond(), 'user' => self::userJoinCond() )
                );
        }
@@ -418,6 +445,29 @@ class Revision {
                return array( 'user_name' );
        }
 
+       /**
+        * Do a batched query to get the parent revision lengths
+        * @param $db DatabaseBase
+        * @param $revIds Array
+        * @return array
+        */
+       public static function getParentLengths( $db, array $revIds ) {
+               $revLens = array();
+               if ( !$revIds ) {
+                       return $revLens; // empty
+               }
+               wfProfileIn( __METHOD__ );
+               $res = $db->select( 'revision',
+                       array( 'rev_id', 'rev_len' ),
+                       array( 'rev_id' => $revIds ),
+                       __METHOD__ );
+               foreach ( $res as $row ) {
+                       $revLens[$row->rev_id] = $row->rev_len;
+               }
+               wfProfileOut( __METHOD__ );
+               return $revLens;
+       }
+
        /**
         * Constructor
         *
@@ -464,13 +514,13 @@ class Revision {
                        if( !isset( $row->rev_content_model ) || is_null( $row->rev_content_model ) ) {
                                $this->mContentModel = null; # determine on demand if needed
                        } else {
-                               $this->mContentModel = intval( $row->rev_content_model );
+                               $this->mContentModel = strval( $row->rev_content_model );
                        }
 
                        if( !isset( $row->rev_content_format ) || is_null( $row->rev_content_format ) ) {
                                $this->mContentFormat = null; # determine on demand if needed
                        } else {
-                               $this->mContentFormat = intval( $row->rev_content_format );
+                               $this->mContentFormat = strval( $row->rev_content_format );
                        }
 
                        // Lazy extraction...
@@ -519,15 +569,17 @@ class Revision {
                        $this->mParentId  = isset( $row['parent_id']  ) ? intval( $row['parent_id']  ) : null;
                        $this->mSha1      = isset( $row['sha1']  )      ? strval( $row['sha1']  )      : null;
 
-                       $this->mContentModel = isset( $row['content_model']  )  ? intval( $row['content_model'] )  : null;
-                       $this->mContentFormat    = isset( $row['content_format']  ) ? intval( $row['content_format'] ) : null;
+                       $this->mContentModel = isset( $row['content_model']  )  ? strval( $row['content_model'] )  : null;
+                       $this->mContentFormat    = isset( $row['content_format']  ) ? strval( $row['content_format'] ) : null;
 
                        // Enforce spacing trimming on supplied text
                        $this->mComment   = isset( $row['comment']    ) ?  trim( strval( $row['comment'] ) ) : null;
                        $this->mText      = isset( $row['text']       ) ? rtrim( strval( $row['text']    ) ) : null;
                        $this->mTextRow   = null;
 
-                       # if we have a content object, override mText and mContentModel
+                       $this->mTitle     = isset( $row['title']      ) ? $row['title'] : null;
+
+                       // if we have a Content object, override mText and mContentModel
                        if ( !empty( $row['content'] ) ) {
                                $handler = $this->getContentHandler();
                                $this->mContent = $row['content'];
@@ -541,10 +593,17 @@ class Revision {
                                $this->mContent = $handler->unserializeContent( $this->mText );
                        }
 
-                       $this->mTitle     = null; # Load on demand if needed
-                       $this->mCurrent   = false; # XXX: really? we are about to create a revision. it will usually then be the current one.
+                       // if we have a Title object, override mPage. Useful for testing and convenience.
+                       if ( isset( $row['title'] ) ) {
+                               $this->mTitle     = $row['title'];
+                               $this->mPage      = $this->mTitle->getArticleID();
+                       } else {
+                               $this->mTitle     = null; // Load on demand if needed
+                       }
 
-                       # If we still have no length, see it we have the text to figure it out
+                       $this->mCurrent   = false; // @todo: XXX: really? we are about to create a revision. it will usually then be the current one.
+
+                       // If we still have no length, see it we have the text to figure it out
                        if ( !$this->mSize ) {
                                if ( !is_null( $this->mContent ) ) {
                                        $this->mSize = $this->mContent->getSize();
@@ -554,13 +613,14 @@ class Revision {
                                }
                        }
 
-                       # Same for sha1
+                       // Same for sha1
                        if ( $this->mSha1 === null ) {
                                $this->mSha1 = is_null( $this->mText ) ? null : self::base36Sha1( $this->mText );
                        }
 
-                       $this->getContentModel(); # force lazy init
-                       $this->getContentFormat();    # force lazy init
+                       // force lazy init
+                       $this->getContentModel();
+                       $this->getContentFormat();
                } else {
                        throw new MWException( 'Revision constructor passed invalid row format.' );
                }
@@ -646,7 +706,10 @@ class Revision {
                        }
                }
 
-               //@todo: as a last resort, perhaps load from page table, if $this->mPage is given?!
+               if ( !$this->mTitle && !is_null( $this->mPage ) && $this->mPage > 0 ) {
+                       $this->mTitle = Title::newFromID( $this->mPage );
+               }
+
                return $this->mTitle;
        }
 
@@ -926,7 +989,7 @@ class Revision {
         * used to determine the content model to use. If no title is know, CONTENT_MODEL_WIKITEXT
         * is used as a last resort.
         *
-        * @return int the content model id associated with this revision, see the CONTENT_MODEL_XXX constants.
+        * @return String the content model id associated with this revision, see the CONTENT_MODEL_XXX constants.
         **/
        public function getContentModel() {
                if ( !$this->mContentModel ) {
@@ -945,7 +1008,7 @@ class Revision {
         * If no content format was stored in the database, the default format for this
         * revision's content model is returned.
         *
-        * @return int the content format id associated with this revision, see the CONTENT_FORMAT_XXX constants.
+        * @return String the content format id associated with this revision, see the CONTENT_FORMAT_XXX constants.
         **/
        public function getContentFormat() {
                if ( !$this->mContentFormat ) {
@@ -971,10 +1034,7 @@ class Revision {
                        $format = $this->getContentFormat();
 
                        if ( !$this->mContentHandler->isSupportedFormat( $format ) ) {
-                               $formatName = ContentHandler::getContentFormatMimeType( $format );
-                               $modelName = ContentHandler::getContentModelName( $model );
-
-                               throw new MWException( "Oops, the content format #$format ($formatName) is not supported for this content model, #$model ($modelName)" );
+                               throw new MWException( "Oops, the content format $format is not supported for this content model, $model" );
                        }
                }
 
@@ -1004,7 +1064,7 @@ class Revision {
                if( $this->getTitle() ) {
                        $prev = $this->getTitle()->getPreviousRevisionID( $this->getId() );
                        if( $prev ) {
-                               return Revision::newFromTitle( $this->getTitle(), $prev );
+                               return self::newFromTitle( $this->getTitle(), $prev );
                        }
                }
                return null;
@@ -1019,7 +1079,7 @@ class Revision {
                if( $this->getTitle() ) {
                        $next = $this->getTitle()->getNextRevisionID( $this->getId() );
                        if ( $next ) {
-                               return Revision::newFromTitle( $this->getTitle(), $next );
+                               return self::newFromTitle( $this->getTitle(), $next );
                        }
                }
                return null;
@@ -1149,7 +1209,7 @@ class Revision {
                                $text = gzdeflate( $text );
                                $flags[] = 'gzip';
                        } else {
-                               wfDebug( "Revision::compressRevisionText() -- no zlib support, not compressing\n" );
+                               wfDebug( __METHOD__ . " -- no zlib support, not compressing\n" );
                        }
                }
                return implode( ',', $flags );
@@ -1167,8 +1227,10 @@ class Revision {
 
                wfProfileIn( __METHOD__ );
 
+               $this->checkContentModel();
+
                $data = $this->mText;
-               $flags = Revision::compressRevisionText( $data );
+               $flags = self::compressRevisionText( $data );
 
                # Write to external storage if required
                if( $wgDefaultExternalStore ) {
@@ -1202,7 +1264,6 @@ class Revision {
                $rev_id = isset( $this->mId )
                        ? $this->mId
                        : $dbw->nextSequenceValue( 'revision_rev_id_seq' );
-
                $row = array(
                        'rev_id'         => $rev_id,
                        'rev_page'       => $this->mPage,
@@ -1223,11 +1284,18 @@ class Revision {
                );
 
                if ( $wgContentHandlerUseDB ) {
-                       $row[ 'rev_content_model' ] = $this->getContentModel();
-                       $row[ 'rev_content_format' ] = $this->getContentFormat();
-               }
+                       //NOTE: Store null for the default model and format, to save space.
+                       //XXX: Makes the DB sensitive to changed defaults. Make this behaviour optional? Only in miser mode?
 
-               $this->checkContentModel();
+                       $model = $this->getContentModel();
+                       $format = $this->getContentFormat();
+
+                       $defaultModel = ContentHandler::getDefaultModelFor( $this->getTitle() );
+                       $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
+
+                       $row[ 'rev_content_model' ] = ( $model === $defaultModel ) ? null : $model;
+                       $row[ 'rev_content_format' ] = ( $format === $defaultFormat ) ? null : $format;
+               }
 
                $dbw->insert( 'revision', $row, __METHOD__ );
 
@@ -1242,7 +1310,7 @@ class Revision {
        protected function checkContentModel() {
                global $wgContentHandlerUseDB;
 
-               $title = $this->getTitle(); //note: returns null for revisions that have not yet been inserted.
+               $title = $this->getTitle(); //note: may return null for revisions that have not yet been inserted.
 
                $model = $this->getContentModel();
                $format = $this->getContentFormat();
@@ -1250,10 +1318,8 @@ class Revision {
 
                if ( !$handler->isSupportedFormat( $format ) ) {
                        $t = $title->getPrefixedDBkey();
-                       $modelName = ContentHandler::getContentModelName( $model );
-                       $formatName = ContentHandler::getContentFormatMimeType( $format );
 
-                       throw new MWException( "Can't use format #$format ($formatName) with content model #$model ($modelName) on $t" );
+                       throw new MWException( "Can't use format $format with content model $model on $t" );
                }
 
                if ( !$wgContentHandlerUseDB && $title ) {
@@ -1264,19 +1330,15 @@ class Revision {
                        $defaultFormat = $defaultHandler->getDefaultFormat();
 
                        if ( $this->getContentModel() != $defaultModel ) {
-                               $defaultModelName = ContentHandler::getContentModelName( $defaultModel );
-                               $modelName = ContentHandler::getContentModelName( $model );
                                $t = $title->getPrefixedDBkey();
 
-                               throw new MWException( "Can't save non-default content model with \$wgContentHandlerUseDB disabled: model is #$model ($modelName), default for $t is #$defaultModel ($defaultModelName)" );
+                               throw new MWException( "Can't save non-default content model with \$wgContentHandlerUseDB disabled: model is $model , default for $t is $defaultModel" );
                        }
 
                        if ( $this->getContentFormat() != $defaultFormat ) {
-                               $defaultFormatName = ContentHandler::getContentFormatMimeType( $defaultFormat );
-                               $formatName = ContentHandler::getContentFormatMimeType( $format );
                                $t = $title->getPrefixedDBkey();
 
-                               throw new MWException( "Can't use non-default content format with \$wgContentHandlerUseDB disabled: format is #$format ($formatName), default for $t is #$defaultFormat ($defaultFormatName)" );
+                               throw new MWException( "Can't use non-default content format with \$wgContentHandlerUseDB disabled: format is $format, default for $t is $defaultFormat" );
                        }
                }
 
@@ -1284,9 +1346,8 @@ class Revision {
 
                if ( !$content->isValid() ) {
                        $t = $title->getPrefixedDBkey();
-                       $modelName = ContentHandler::getContentModelName( $model );
 
-                       throw new MWException( "Content of $t is not valid! Content model is #$model ($modelName)" );
+                       throw new MWException( "Content of $t is not valid! Content model is $model" );
                }
        }
 
@@ -1517,7 +1578,7 @@ class Revision {
        static function countByTitle( $db, $title ) {
                $id = $title->getArticleID();
                if( $id ) {
-                       return Revision::countByPageId( $db, $id );
+                       return self::countByPageId( $db, $id );
                }
                return 0;
        }