FU r102073:
[lhc/web/wiklou.git] / includes / filerepo / File.php
index d79a166..4dffbfe 100644 (file)
@@ -1,4 +1,10 @@
 <?php
+/**
+ * Base code for files.
+ *
+ * @file
+ * @ingroup FileRepo
+ */
 
 /**
  * Implements some public methods and some protected utility functions which
@@ -24,7 +30,14 @@ abstract class File {
        const DELETED_COMMENT = 2;
        const DELETED_USER = 4;
        const DELETED_RESTRICTED = 8;
-       const RENDER_NOW = 1;
+
+       /** Force rendering in the current process */
+       const RENDER_NOW   = 1;
+       /**
+        * Force rendering even if thumbnail already exist and using RENDER_NOW
+        * I.e. you have to pass both flags: File::RENDER_NOW | File::RENDER_FORCE 
+        */
+       const RENDER_FORCE = 2;
 
        const DELETE_SOURCE = 1;
 
@@ -46,16 +59,81 @@ abstract class File {
        /**
         * The following member variables are not lazy-initialised
         */
-       var $repo, $title, $lastError, $redirected, $redirectedTitle;
 
        /**
-        * Call this constructor from child classes
+        * @var FileRepo|false
+        */
+       var $repo;
+
+       /**
+        * @var Title|false
+        */
+       var $title;
+
+       var $lastError, $redirected, $redirectedTitle;
+
+       /**
+        * @var MediaHandler
+        */
+       protected $handler;
+
+       protected $url, $extension, $name, $path, $hashPath, $pageCount, $transformScript;
+
+       /**
+        * @var bool
+        */
+       protected $canRender, $isSafeFile;
+
+       /**
+        * @var string Required Repository class type
+        */
+       protected $repoClass = 'FileRepo';
+
+       /**
+        * Call this constructor from child classes.
+        * 
+        * Both $title and $repo are optional, though some functions
+        * may return false or throw exceptions if they are not set.
+        * Most subclasses will want to call assertRepoDefined() here.
+        *
+        * @param $title Title|string|false
+        * @param $repo FileRepo|false
         */
        function __construct( $title, $repo ) {
+               if ( $title !== false ) { // subclasses may not use MW titles
+                       $title = self::normalizeTitle( $title, 'exception' );
+               }
                $this->title = $title;
                $this->repo = $repo;
        }
 
+       /**
+        * Given a string or Title object return either a
+        * valid Title object with namespace NS_FILE or null
+        * @param $title Title|string
+        * @param $exception string|false Use 'exception' to throw an error on bad titles
+        * @return Title|null
+        */
+       static function normalizeTitle( $title, $exception = false ) {
+               $ret = $title;
+               if ( $ret instanceof Title ) {
+                       # Normalize NS_MEDIA -> NS_FILE
+                       if ( $ret->getNamespace() == NS_MEDIA ) {
+                               $ret = Title::makeTitleSafe( NS_FILE, $ret->getDBkey() );
+                       # Sanity check the title namespace
+                       } elseif ( $ret->getNamespace() !== NS_FILE ) {
+                               $ret = null;
+                       }
+               } else {
+                       # Convert strings to Title objects
+                       $ret = Title::makeTitleSafe( NS_FILE, (string)$ret );
+               }
+               if ( !$ret && $exception !== false ) {
+                       throw new MWException( "`$title` is not a valid file title." );
+               }
+               return $ret;
+       }
+
        function __get( $name ) {
                $function = array( $this, 'get' . ucfirst( $name ) );
                if ( !is_callable( $function ) ) {
@@ -95,6 +173,8 @@ abstract class File {
         *
         * @param $old File Old file
         * @param $new string New name
+        *
+        * @return bool|null
         */
        static function checkExtensionCompatibility( File $old, $new ) {
                $oldMime = $old->getMimeType();
@@ -116,10 +196,10 @@ abstract class File {
         * Split an internet media type into its two components; if not
         * a two-part name, set the minor type to 'unknown'.
         *
-        * @param $mime "text/html" etc
+        * @param string $mime "text/html" etc
         * @return array ("text", "html") etc
         */
-       static function splitMime( $mime ) {
+       public static function splitMime( $mime ) {
                if( strpos( $mime, '/' ) !== false ) {
                        return explode( '/', $mime, 2 );
                } else {
@@ -129,9 +209,12 @@ abstract class File {
 
        /**
         * Return the name of this file
+        *
+        * @return string
         */
        public function getName() {
                if ( !isset( $this->name ) ) {
+                       $this->assertRepoDefined();
                        $this->name = $this->repo->getNameFromTitle( $this->title );
                }
                return $this->name;
@@ -139,6 +222,8 @@ abstract class File {
 
        /**
         * Get the file extension, e.g. "svg"
+        *
+        * @return string
         */
        function getExtension() {
                if ( !isset( $this->extension ) ) {
@@ -151,23 +236,30 @@ abstract class File {
 
        /**
         * Return the associated title object
+        * @return Title|false
         */
        public function getTitle() { return $this->title; }
-       
+
        /**
         * Return the title used to find this file
+        *
+        * @return Title
         */
        public function getOriginalTitle() {
-               if ( $this->redirected )
+               if ( $this->redirected ) {
                        return $this->getRedirectedTitle();
+               }
                return $this->title;
        }
 
        /**
         * Return the URL of the file
+        *
+        * @return string
         */
        public function getUrl() {
                if ( !isset( $this->url ) ) {
+                       $this->assertRepoDefined();
                        $this->url = $this->repo->getZoneUrl( 'public' ) . '/' . $this->getUrlRel();
                }
                return $this->url;
@@ -177,18 +269,28 @@ abstract class File {
         * Return a fully-qualified URL to the file.
         * Upload URL paths _may or may not_ be fully qualified, so
         * we check. Local paths are assumed to belong on $wgServer.
-        * @return string
+        *
+        * @return String
         */
        public function getFullUrl() {
-               return wfExpandUrl( $this->getUrl() );
+               return wfExpandUrl( $this->getUrl(), PROTO_RELATIVE );
        }
 
+       /**
+        * @return string
+        */
+       public function getCanonicalUrl() {
+               return wfExpandUrl( $this->getUrl(), PROTO_CANONICAL );
+       }
+
+       /**
+        * @return string
+        */
        function getViewURL() {
                if( $this->mustRender()) {
                        if( $this->canRender() ) {
                                return $this->createThumb( $this->getWidth() );
-                       }
-                       else {
+                       } else {
                                wfDebug(__METHOD__.': supposed to render '.$this->getName().' ('.$this->getMimeType()."), but can't!\n");
                                return $this->getURL(); #hm... return NULL?
                        }
@@ -205,30 +307,33 @@ abstract class File {
        * i.e. whether the files are all found in the same directory,
        * or in hashed paths like /images/3/3c.
        *
-       * May return false if the file is not locally accessible.
+       * Most callers don't check the return value, but ForeignAPIFile::getPath
+       * returns false.
+        *
+        * @return string|false
        */
        public function getPath() {
                if ( !isset( $this->path ) ) {
-                       $this->path = $this->repo->getZonePath('public') . '/' . $this->getRel();
+                       $this->assertRepoDefined();
+                       $this->path = $this->repo->getZonePath( 'public' ) . '/' . $this->getRel();
                }
                return $this->path;
        }
 
-       /**
-       * Alias for getPath()
-       */
-       public function getFullPath() {
-               return $this->getPath();
-       }
-
        /**
         * Return the width of the image. Returns false if the width is unknown
         * or undefined.
         *
         * STUB
         * Overridden by LocalFile, UnregisteredLocalFile
+        *
+        * @param $page int
+        *
+        * @return number
         */
-       public function getWidth( $page = 1 ) { return false; }
+       public function getWidth( $page = 1 ) {
+               return false;
+       }
 
        /**
         * Return the height of the image. Returns false if the height is unknown
@@ -236,19 +341,31 @@ abstract class File {
         *
         * STUB
         * Overridden by LocalFile, UnregisteredLocalFile
+        *
+        * @param $page int
+        *
+        * @return false|number
         */
-       public function getHeight( $page = 1 ) { return false; }
+       public function getHeight( $page = 1 ) {
+               return false;
+       }
 
        /**
         * Returns ID or name of user who uploaded the file
         * STUB
         *
         * @param $type string 'text' or 'id'
+        *
+        * @return string|int
         */
-       public function getUser( $type='text' ) { return null; }
+       public function getUser( $type = 'text' ) {
+               return null;
+       }
 
        /**
         * Get the duration of a media file in seconds
+        *
+        * @return number
         */
        public function getLength() {
                $handler = $this->getHandler();
@@ -259,33 +376,77 @@ abstract class File {
                }
        }
 
+       /**
+        * Return true if the file is vectorized
+        *
+        * @return bool
+        */
+       public function isVectorized() {
+               $handler = $this->getHandler();
+               if ( $handler ) {
+                       return $handler->isVectorized( $this );
+               } else {
+                       return false;
+               }
+       }
+
        /**
         * Get handler-specific metadata
         * Overridden by LocalFile, UnregisteredLocalFile
         * STUB
         */
-       public function getMetadata() { return false; }
+       public function getMetadata() {
+               return false;
+       }
+
+       /**
+       * get versioned metadata
+       *
+       * @param $metadata Mixed Array or String of (serialized) metadata
+       * @param $version integer version number.
+       * @return Array containing metadata, or what was passed to it on fail (unserializing if not array)
+       */
+       public function convertMetadataVersion($metadata, $version) {
+               $handler = $this->getHandler();
+               if ( !is_array( $metadata ) ) {
+                       //just to make the return type consistant
+                       $metadata = unserialize( $metadata );
+               }
+               if ( $handler ) {
+                       return $handler->convertMetadataVersion( $metadata, $version );
+               } else {
+                       return $metadata;
+               }
+       }
 
        /**
         * Return the bit depth of the file
         * Overridden by LocalFile
         * STUB
         */
-       public function getBitDepth() { return 0; }
+       public function getBitDepth() {
+               return 0;
+       }
 
        /**
         * Return the size of the image file, in bytes
         * Overridden by LocalFile, UnregisteredLocalFile
         * STUB
         */
-       public function getSize() { return false; }
+       public function getSize() {
+               return false;
+       }
 
        /**
         * Returns the mime type of the file.
         * Overridden by LocalFile, UnregisteredLocalFile
         * STUB
+        *
+        * @return string
         */
-       function getMimeType() { return 'unknown/unknown'; }
+       function getMimeType() {
+               return 'unknown/unknown';
+       }
 
        /**
         * Return the type of the media in the file.
@@ -304,6 +465,8 @@ abstract class File {
         * that can be converted to a format
         * supported by all browsers (namely GIF, PNG and JPEG),
         * or if it is an SVG image and SVG conversion is enabled.
+        *
+        * @return bool
         */
        function canRender() {
                if ( !isset( $this->canRender ) ) {
@@ -335,6 +498,8 @@ abstract class File {
 
        /**
         * Alias for canRender()
+        *
+        * @return bool
         */
        function allowInlineDisplay() {
                return $this->canRender();
@@ -350,6 +515,8 @@ abstract class File {
         *
         * Note that this function will always return true if allowInlineDisplay()
         * or isTrustedFile() is true for this file.
+        *
+        * @return bool
         */
        function isSafeFile() {
                if ( !isset( $this->isSafeFile ) ) {
@@ -358,41 +525,64 @@ abstract class File {
                return $this->isSafeFile;
        }
 
-       /** Accessor for __get() */
+       /**
+        * Accessor for __get()
+        *
+        * @return bool
+        */
        protected function getIsSafeFile() {
                return $this->isSafeFile();
        }
 
-       /** Uncached accessor */
+       /**
+        * Uncached accessor
+        *
+        * @return bool
+        */
        protected function _getIsSafeFile() {
-               if ($this->allowInlineDisplay()) return true;
-               if ($this->isTrustedFile()) return true;
+               if ( $this->allowInlineDisplay() ) {
+                       return true;
+               }
+               if ($this->isTrustedFile()) {
+                       return true;
+               }
 
                global $wgTrustedMediaFormats;
 
-               $type= $this->getMediaType();
-               $mime= $this->getMimeType();
+               $type = $this->getMediaType();
+               $mime = $this->getMimeType();
                #wfDebug("LocalFile::isSafeFile: type= $type, mime= $mime\n");
 
-               if (!$type || $type===MEDIATYPE_UNKNOWN) return false; #unknown type, not trusted
-               if ( in_array( $type, $wgTrustedMediaFormats) ) return true;
+               if ( !$type || $type === MEDIATYPE_UNKNOWN ) {
+                       return false; #unknown type, not trusted
+               }
+               if ( in_array( $type, $wgTrustedMediaFormats ) ) {
+                       return true;
+               }
 
-               if ($mime==="unknown/unknown") return false; #unknown type, not trusted
-               if ( in_array( $mime, $wgTrustedMediaFormats) ) return true;
+               if ( $mime === "unknown/unknown" ) {
+                       return false; #unknown type, not trusted
+               }
+               if ( in_array( $mime, $wgTrustedMediaFormats) ) {
+                       return true;
+               }
 
                return false;
        }
 
-       /** Returns true if the file is flagged as trusted. Files flagged that way
-       * can be linked to directly, even if that is not allowed for this type of
-       * file normally.
-       *
-       * This is a dummy function right now and always returns false. It could be
-       * implemented to extract a flag from the database. The trusted flag could be
-       * set on upload, if the user has sufficient privileges, to bypass script-
-       * and html-filters. It may even be coupled with cryptographics signatures
-       * or such.
-       */
+       /**
+        * Returns true if the file is flagged as trusted. Files flagged that way
+        * can be linked to directly, even if that is not allowed for this type of
+        * file normally.
+        *
+        * This is a dummy function right now and always returns false. It could be
+        * implemented to extract a flag from the database. The trusted flag could be
+        * set on upload, if the user has sufficient privileges, to bypass script-
+        * and html-filters. It may even be coupled with cryptographics signatures
+        * or such.
+        *
+        * @return bool
+        */
        function isTrustedFile() {
                #this could be implemented to check a flag in the databas,
                #look for signatures, etc
@@ -415,12 +605,14 @@ abstract class File {
         * It would be unsafe to include private images, making public thumbnails inadvertently
         *
         * @return boolean Whether file exists in the repository and is includable.
-        * @public
         */
-       function isVisible() {
+       public function isVisible() {
                return $this->exists();
        }
 
+       /**
+        * @return string
+        */
        function getTransformScript() {
                if ( !isset( $this->transformScript ) ) {
                        $this->transformScript = false;
@@ -436,36 +628,49 @@ abstract class File {
 
        /**
         * Get a ThumbnailImage which is the same size as the source
+        *
+        * @param $handlerParams array
+        *
+        * @return string
         */
-       function getUnscaledThumb( $page = false ) {
+       function getUnscaledThumb( $handlerParams = array() ) {
+               $hp =& $handlerParams;
+               $page = isset( $hp['page'] ) ? $hp['page'] : false;
                $width = $this->getWidth( $page );
                if ( !$width ) {
                        return $this->iconThumb();
                }
-               if ( $page ) {
-                       $params = array(
-                               'page' => $page,
-                               'width' => $this->getWidth( $page )
-                       );
-               } else {
-                       $params = array( 'width' => $this->getWidth() );
-               }
-               return $this->transform( $params );
+               $hp['width'] = $width;
+               return $this->transform( $hp );
        }
 
        /**
         * Return the file name of a thumbnail with the specified parameters
         *
-        * @param array $params Handler-specific parameters
+        * @param $params Array: handler-specific parameters
         * @private -ish
+        *
+        * @return string
         */
        function thumbName( $params ) {
+               return $this->generateThumbName( $this->getName(), $params );
+       }
+
+       /**
+        * Generate a thumbnail file name from a name and specified parameters
+        *
+        * @param string $name
+        * @param array $params Parameters which will be passed to MediaHandler::makeParamString
+        *
+        * @return string
+        */
+       function generateThumbName( $name, $params ) {
                if ( !$this->getHandler() ) {
                        return null;
                }
                $extension = $this->getExtension();
-               list( $thumbExt, $thumbMime ) = $this->handler->getThumbType( $extension, $this->getMimeType() );
-               $thumbName = $this->handler->makeParamString( $params ) . '-' . $this->getName();
+               list( $thumbExt, $thumbMime ) = $this->handler->getThumbType( $extension, $this->getMimeType(), $params );
+               $thumbName = $this->handler->makeParamString( $params ) . '-' . $name;
                if ( $thumbExt != $extension ) {
                        $thumbName .= ".$thumbExt";
                }
@@ -484,8 +689,10 @@ abstract class File {
         * specified, the generated image will be no bigger than width x height,
         * and will also have correct aspect ratio.
         *
-        * @param integer $width        maximum width of the generated thumbnail
-        * @param integer $height       maximum height of the image (optional)
+        * @param $width Integer: maximum width of the generated thumbnail
+        * @param $height Integer: maximum height of the image (optional)
+        *
+        * @return string
         */
        public function createThumb( $width, $height = -1 ) {
                $params = array( 'width' => $width );
@@ -498,38 +705,64 @@ abstract class File {
        }
 
        /**
-        * As createThumb, but returns a ThumbnailImage object. This can
-        * provide access to the actual file, the real size of the thumb,
-        * and can produce a convenient <img> tag for you.
-        *
-        * For non-image formats, this may return a filetype-specific icon.
-        *
-        * @param integer $width        maximum width of the generated thumbnail
-        * @param integer $height       maximum height of the image (optional)
-        * @param boolean $render       Deprecated
+        * Do the work of a transform (from an original into a thumb).
+        * Contains filesystem-specific functions.
         *
-        * @return ThumbnailImage or null on failure
+        * @param $thumbName string: the name of the thumbnail file.
+        * @param $thumbUrl string: the URL of the thumbnail file.
+        * @param $params Array: an associative array of handler-specific parameters.
+        *                Typical keys are width, height and page.
+        * @param $flags Integer: a bitfield, may contain self::RENDER_NOW to force rendering
         *
-        * @deprecated use transform()
+        * @return MediaTransformOutput | false
         */
-       public function getThumbnail( $width, $height=-1, $render = true ) {
-               $params = array( 'width' => $width );
-               if ( $height != -1 ) {
-                       $params['height'] = $height;
+       protected function maybeDoTransform( $thumbName, $thumbUrl, $params, $flags = 0 ) {
+               global $wgIgnoreImageErrors, $wgThumbnailEpoch;
+
+               $thumbPath = $this->getThumbPath( $thumbName );
+               if ( $this->repo && $this->repo->canTransformVia404() && !($flags & self::RENDER_NOW ) ) {
+                       wfDebug( __METHOD__ . " transformation deferred." );
+                       return $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params );
                }
-               return $this->transform( $params, 0 );
+
+               wfDebug( __METHOD__.": Doing stat for $thumbPath\n" );
+               $this->migrateThumbFile( $thumbName );
+               if ( file_exists( $thumbPath ) && !($flags & self::RENDER_FORCE) ) { 
+                       $thumbTime = filemtime( $thumbPath );
+                       if ( $thumbTime !== FALSE &&
+                            gmdate( 'YmdHis', $thumbTime ) >= $wgThumbnailEpoch ) { 
+
+                               return $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params );
+                       }
+               } elseif( $flags & self::RENDER_FORCE ) {
+                       wfDebug( __METHOD__ . " forcing rendering per flag File::RENDER_FORCE\n" ); 
+               }
+               $thumb = $this->handler->doTransform( $this, $thumbPath, $thumbUrl, $params );
+
+               // Ignore errors if requested
+               if ( !$thumb ) {
+                       $thumb = null;
+               } elseif ( $thumb->isError() ) {
+                       $this->lastError = $thumb->toText();
+                       if ( $wgIgnoreImageErrors && !($flags & self::RENDER_NOW) ) {
+                               $thumb = $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params );
+                       }
+               }
+
+               return $thumb;
        }
 
+
        /**
         * Transform a media file
         *
-        * @param array $params An associative array of handler-specific parameters. Typical
-        *                      keys are width, height and page.
-        * @param integer $flags A bitfield, may contain self::RENDER_NOW to force rendering
-        * @return MediaTransformOutput
+        * @param $params Array: an associative array of handler-specific parameters.
+        *                Typical keys are width, height and page.
+        * @param $flags Integer: a bitfield, may contain self::RENDER_NOW to force rendering
+        * @return MediaTransformOutput | false
         */
        function transform( $params, $flags = 0 ) {
-               global $wgUseSquid, $wgIgnoreImageErrors, $wgThumbnailEpoch, $wgServer;
+               global $wgUseSquid;
 
                wfProfileIn( __METHOD__ );
                do {
@@ -542,7 +775,7 @@ abstract class File {
                        // Get the descriptionUrl to embed it as comment into the thumbnail. Bug 19791.
                        $descriptionUrl =  $this->getDescriptionUrl();
                        if ( $descriptionUrl ) {
-                               $params['descriptionUrl'] = $wgServer . $descriptionUrl;
+                               $params['descriptionUrl'] = wfExpandUrl( $descriptionUrl, PROTO_CANONICAL );
                        }
 
                        $script = $this->getTransformScript();
@@ -557,39 +790,12 @@ abstract class File {
                        $normalisedParams = $params;
                        $this->handler->normaliseParams( $this, $normalisedParams );
                        $thumbName = $this->thumbName( $normalisedParams );
-                       $thumbPath = $this->getThumbPath( $thumbName );
                        $thumbUrl = $this->getThumbUrl( $thumbName );
 
-                       if ( $this->repo->canTransformVia404() && !($flags & self::RENDER_NOW ) ) {
-                               $thumb = $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params );
-                               break;
-                       }
+                       $thumb = $this->maybeDoTransform( $thumbName, $thumbUrl, $params, $flags );
 
-                       wfDebug( __METHOD__.": Doing stat for $thumbPath\n" );
-                       $this->migrateThumbFile( $thumbName );
-                       if ( file_exists( $thumbPath )) {
-                               $thumbTime = filemtime( $thumbPath );
-                               if ( $thumbTime !== FALSE &&
-                                    gmdate( 'YmdHis', $thumbTime ) >= $wgThumbnailEpoch ) { 
-       
-                                       $thumb = $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params );
-                                       break;
-                               }
-                       }
-                       $thumb = $this->handler->doTransform( $this, $thumbPath, $thumbUrl, $params );
-
-                       // Ignore errors if requested
-                       if ( !$thumb ) {
-                               $thumb = null;
-                       } elseif ( $thumb->isError() ) {
-                               $this->lastError = $thumb->toText();
-                               if ( $wgIgnoreImageErrors && !($flags & self::RENDER_NOW) ) {
-                                       $thumb = $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params );
-                               }
-                       }
-                       
-                       // Purge. Useful in the event of Core -> Squid connection failure or squid 
-                       // purge collisions from elsewhere during failure. Don't keep triggering for 
+                       // Purge. Useful in the event of Core -> Squid connection failure or squid
+                       // purge collisions from elsewhere during failure. Don't keep triggering for
                        // "thumbs" which have the main image URL though (bug 13776)
                        if ( $wgUseSquid && ( !$thumb || $thumb->isError() || $thumb->getUrl() != $this->getURL()) ) {
                                SquidUpdate::purge( array( $thumbUrl ) );
@@ -609,6 +815,7 @@ abstract class File {
 
        /**
         * Get a MediaHandler instance for this file
+        * @return MediaHandler
         */
        function getHandler() {
                if ( !isset( $this->handler ) ) {
@@ -648,7 +855,9 @@ abstract class File {
         * STUB
         * Overridden by LocalFile
         */
-       function getThumbnails() { return array(); }
+       function getThumbnails() {
+               return array();
+       }
 
        /**
         * Purge shared caches such as thumbnails and DB data caching
@@ -695,6 +904,8 @@ abstract class File {
         * @param $start timestamp Only revisions older than $start will be returned
         * @param $end timestamp Only revisions newer than $end will be returned
         * @param $inc bool Include the endpoints of the time range
+        *
+        * @return array
         */
        function getHistory($limit = null, $start = null, $end = null, $inc=true) {
                return array();
@@ -724,9 +935,12 @@ abstract class File {
         * Get the filename hash component of the directory including trailing slash,
         * e.g. f/fa/
         * If the repository is not hashed, returns an empty string.
+        *
+        * @return string
         */
        function getHashPath() {
                if ( !isset( $this->hashPath ) ) {
+                       $this->assertRepoDefined();
                        $this->hashPath = $this->repo->getHashPath( $this->getName() );
                }
                return $this->hashPath;
@@ -734,6 +948,8 @@ abstract class File {
 
        /**
         * Get the path of the file relative to the public zone root
+        *
+        * @return string
         */
        function getRel() {
                return $this->getHashPath() . $this->getName();
@@ -741,12 +957,20 @@ abstract class File {
 
        /**
         * Get urlencoded relative path of the file
+        *
+        * @return string
         */
        function getUrlRel() {
                return $this->getHashPath() . rawurlencode( $this->getName() );
        }
 
-       /** Get the relative path for an archive file */
+       /**
+        * Get the relative path for an archived file
+        *
+        * @param $suffix bool|string if not false, the name of an archived thumbnail file
+        *
+        * @return string
+        */
        function getArchiveRel( $suffix = false ) {
                $path = 'archive/' . $this->getHashPath();
                if ( $suffix === false ) {
@@ -757,23 +981,97 @@ abstract class File {
                return $path;
        }
 
-       /** Get the path of the archive directory, or a particular file if $suffix is specified */
+       /**
+        * Get the relative path for an archived file's thumbs directory
+        * or a specific thumb if the $suffix is given.
+        *
+        * @param $archiveName string the timestamped name of an archived image
+        * @param $suffix bool|string if not false, the name of a thumbnail file
+        *
+        * @return string
+        */
+       function getArchiveThumbRel( $archiveName, $suffix = false ) {
+               $path = 'archive/' . $this->getHashPath() . $archiveName . "/";
+               if ( $suffix === false ) {
+                       $path = substr( $path, 0, -1 );
+               } else {
+                       $path .= $suffix;
+               }
+               return $path;
+       }
+
+       /**
+        * Get the path of the archived file.
+        *
+        * @param $suffix bool|string if not false, the name of an archived file.
+        *
+        * @return string
+        */
        function getArchivePath( $suffix = false ) {
-               return $this->repo->getZonePath('public') . '/' . $this->getArchiveRel( $suffix );
+               $this->assertRepoDefined();
+               return $this->repo->getZonePath( 'public' ) . '/' . $this->getArchiveRel( $suffix );
+       }
+
+       /**
+        * Get the path of the archived file's thumbs, or a particular thumb if $suffix is specified
+        *
+        * @param $archiveName string the timestamped name of an archived image
+        * @param $suffix bool|string if not false, the name of a thumbnail file
+        *
+        * @return string
+        */
+       function getArchiveThumbPath( $archiveName, $suffix = false ) {
+               $this->assertRepoDefined();
+               return $this->repo->getZonePath( 'thumb' ) . '/' .
+                       $this->getArchiveThumbRel( $archiveName, $suffix );
        }
 
-       /** Get the path of the thumbnail directory, or a particular file if $suffix is specified */
+       /**
+        * Get the path of the thumbnail directory, or a particular file if $suffix is specified
+        *
+        * @param $suffix bool|string if not false, the name of a thumbnail file
+        *
+        * @return string
+        */
        function getThumbPath( $suffix = false ) {
-               $path = $this->repo->getZonePath('thumb') . '/' . $this->getRel();
+               $this->assertRepoDefined();
+               $path = $this->repo->getZonePath( 'thumb' ) . '/' . $this->getRel();
                if ( $suffix !== false ) {
                        $path .= '/' . $suffix;
                }
                return $path;
        }
 
-       /** Get the URL of the archive directory, or a particular file if $suffix is specified */
+       /**
+        * Get the URL of the archive directory, or a particular file if $suffix is specified
+        *
+        * @param $suffix bool|string if not false, the name of an archived file
+        *
+        * @return string
+        */
        function getArchiveUrl( $suffix = false ) {
-               $path = $this->repo->getZoneUrl('public') . '/archive/' . $this->getHashPath();
+               $this->assertRepoDefined();
+               $path = $this->repo->getZoneUrl( 'public' ) . '/archive/' . $this->getHashPath();
+               if ( $suffix === false ) {
+                       $path = substr( $path, 0, -1 );
+               } else {
+                       $path .= rawurlencode( $suffix );
+               }
+               return $path;
+       }
+
+       /**
+        * Get the URL of the archived file's thumbs, or a particular thumb if $suffix is specified
+        *
+        * @param $archiveName string the timestamped name of an archived image
+        * @param $suffix bool|string if not false, the name of a thumbnail file
+        *
+        * @return string
+        */
+       function getArchiveThumbUrl( $archiveName, $suffix = false ) {
+               $this->assertRepoDefined();
+               $path = $this->repo->getZoneUrl( 'thumb' ) . '/archive/' .
+                       $this->getHashPath() . rawurlencode( $archiveName ) . "/";
                if ( $suffix === false ) {
                        $path = substr( $path, 0, -1 );
                } else {
@@ -782,8 +1080,15 @@ abstract class File {
                return $path;
        }
 
-       /** Get the URL of the thumbnail directory, or a particular file if $suffix is specified */
+       /**
+        * Get the URL of the thumbnail directory, or a particular file if $suffix is specified
+        *
+        * @param $suffix bool|string if not false, the name of a thumbnail file
+        *
+        * @return path
+        */
        function getThumbUrl( $suffix = false ) {
+               $this->assertRepoDefined();
                $path = $this->repo->getZoneUrl('thumb') . '/' . $this->getUrlRel();
                if ( $suffix !== false ) {
                        $path .= '/' . rawurlencode( $suffix );
@@ -791,8 +1096,15 @@ abstract class File {
                return $path;
        }
 
-       /** Get the virtual URL for an archive file or directory */
+       /**
+        * Get the virtual URL for an archived file's thumbs, or a specific thumb.
+        *
+        * @param $suffix bool|string if not false, the name of a thumbnail file
+        *
+        * @return string
+        */
        function getArchiveVirtualUrl( $suffix = false ) {
+               $this->assertRepoDefined();
                $path = $this->repo->getVirtualUrl() . '/public/archive/' . $this->getHashPath();
                if ( $suffix === false ) {
                        $path = substr( $path, 0, -1 );
@@ -802,8 +1114,15 @@ abstract class File {
                return $path;
        }
 
-       /** Get the virtual URL for a thumbnail file or directory */
+       /**
+        * Get the virtual URL for a thumbnail file or directory
+        *
+        * @param $suffix bool|string if not false, the name of a thumbnail file
+        *
+        * @return string
+        */
        function getThumbVirtualUrl( $suffix = false ) {
+               $this->assertRepoDefined();
                $path = $this->repo->getVirtualUrl() . '/thumb/' . $this->getUrlRel();
                if ( $suffix !== false ) {
                        $path .= '/' . rawurlencode( $suffix );
@@ -811,8 +1130,15 @@ abstract class File {
                return $path;
        }
 
-       /** Get the virtual URL for the file itself */
+       /**
+        * Get the virtual URL for the file itself
+        *
+        * @param $suffix bool|string if not false, the name of a thumbnail file
+        *
+        * @return string
+        */
        function getVirtualUrl( $suffix = false ) {
+               $this->assertRepoDefined();
                $path = $this->repo->getVirtualUrl() . '/public/' . $this->getUrlRel();
                if ( $suffix !== false ) {
                        $path .= '/' . rawurlencode( $suffix );
@@ -824,9 +1150,13 @@ abstract class File {
         * @return bool
         */
        function isHashed() {
+               $this->assertRepoDefined();
                return $this->repo->isHashed();
        }
 
+       /**
+        * @throws MWException
+        */
        function readOnlyError() {
                throw new MWException( get_class($this) . ': write operations are not supported' );
        }
@@ -835,6 +1165,12 @@ abstract class File {
         * Record a file upload in the upload log and the image table
         * STUB
         * Overridden by LocalFile
+        * @param $oldver
+        * @param $desc
+        * @param $license string
+        * @param $copyStatus string
+        * @param $source string
+        * @param $watch bool
         */
        function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '', $watch = false ) {
                $this->readOnlyError();
@@ -842,19 +1178,18 @@ abstract class File {
 
        /**
         * Move or copy a file to its public location. If a file exists at the
-        * destination, move it to an archive. Returns the archive name on success
-        * or an empty string if it was a new file, and a wikitext-formatted
-        * WikiError object on failure.
+        * destination, move it to an archive. Returns a FileRepoStatus object with
+        * the archive name in the "value" member on success.
         *
         * The archive name should be passed through to recordUpload for database
         * registration.
         *
-        * @param string $sourcePath Local filesystem path to the source image
-        * @param integer $flags A bitwise combination of:
+        * @param $srcPath String: local filesystem path to the source image
+        * @param $flags Integer: a bitwise combination of:
         *     File::DELETE_SOURCE    Delete the source file, i.e. move
         *         rather than copy
-        * @return The archive name on success or an empty string if it was a new
-        *     file, and a wikitext-formatted WikiError object on failure.
+        * @return FileRepoStatus object. On success, the value member contains the
+        *     archive name, or an empty string if it was a new file.
         *
         * STUB
         * Overridden by LocalFile
@@ -864,45 +1199,8 @@ abstract class File {
        }
 
        /**
-        * Get an array of Title objects which are articles which use this file
-        * Also adds their IDs to the link cache
-        *
-        * This is mostly copied from Title::getLinksTo()
-        *
-        * @deprecated Use HTMLCacheUpdate, this function uses too much memory
+        * @return bool
         */
-       function getLinksTo( $options = array() ) {
-               wfProfileIn( __METHOD__ );
-
-               // Note: use local DB not repo DB, we want to know local links
-               if ( count( $options ) > 0 ) {
-                       $db = wfGetDB( DB_MASTER );
-               } else {
-                       $db = wfGetDB( DB_SLAVE );
-               }
-               $linkCache = LinkCache::singleton();
-
-               $encName = $db->addQuotes( $this->getName() );
-               $res = $db->select( array( 'page', 'imagelinks'), 
-                                                       array( 'page_namespace', 'page_title', 'page_id', 'page_len', 'page_is_redirect' ),
-                                                       array( 'page_id' => 'il_from', 'il_to' => $encName ),
-                                                       __METHOD__,
-                                                       $options );
-
-               $retVal = array();
-               if ( $db->numRows( $res ) ) {
-                       while ( $row = $db->fetchObject( $res ) ) {
-                               if ( $titleObj = Title::newFromRow( $row ) ) {
-                                       $linkCache->addGoodLinkObj( $row->page_id, $titleObj, $row->page_len, $row->page_is_redirect );
-                                       $retVal[] = $titleObj;
-                               }
-                       }
-               }
-               $db->freeResult( $res );
-               wfProfileOut( __METHOD__ );
-               return $retVal;
-       }
-
        function formatMetadata() {
                if ( !$this->getHandler() ) {
                        return false;
@@ -916,7 +1214,8 @@ abstract class File {
         * @return bool
         */
        function isLocal() {
-               return $this->getRepoName() == 'local';
+               $repo = $this->getRepo();
+               return $repo && $repo->isLocal();
        }
 
        /**
@@ -927,8 +1226,11 @@ abstract class File {
        function getRepoName() {
                return $this->repo ? $this->repo->getName() : 'unknown';
        }
-       /*
+
+       /**
         * Returns the repository
+        *
+        * @return FileRepo|false
         */
        function getRepo() {
                return $this->repo;
@@ -937,6 +1239,8 @@ abstract class File {
        /**
         * Returns true if the image is an old version
         * STUB
+        *
+        * @return bool
         */
        function isOld() {
                return false;
@@ -945,15 +1249,19 @@ abstract class File {
        /**
         * Is this file a "deleted" file in a private archive?
         * STUB
+        *
+        * @param $field
+        *
+        * @return bool
         */
        function isDeleted( $field ) {
                return false;
        }
-       
+
        /**
         * Return the deletion bitfield
         * STUB
-        */     
+        */
        function getVisibility() {
                return 0;
        }
@@ -992,8 +1300,8 @@ abstract class File {
         *
         * Cache purging is done; logging is caller's responsibility.
         *
-        * @param $reason
-        * @param $suppress, hide content from sysops?
+        * @param $reason String
+        * @param $suppress Boolean: hide content from sysops?
         * @return true on success, false on some kind of failure
         * STUB
         * Overridden by LocalFile
@@ -1008,21 +1316,21 @@ abstract class File {
         *
         * May throw database exceptions on error.
         *
-        * @param $versions set of record ids of deleted items to restore,
+        * @param $versions array set of record ids of deleted items to restore,
         *                    or empty to restore all revisions.
-        * @param $unsuppress, remove restrictions on content upon restoration?
-        * @return the number of file revisions restored if successful,
+        * @param $unsuppress bool remove restrictions on content upon restoration?
+        * @return int|false the number of file revisions restored if successful,
         *         or false on failure
         * STUB
         * Overridden by LocalFile
         */
-       function restore( $versions=array(), $unsuppress=false ) {
+       function restore( $versions = array(), $unsuppress = false ) {
                $this->readOnlyError();
        }
 
        /**
-        * Returns 'true' if this file is a type which supports multiple pages, 
-        * e.g. DJVU or PDF. Note that this may be true even if the file in 
+        * Returns 'true' if this file is a type which supports multiple pages,
+        * e.g. DJVU or PDF. Note that this may be true even if the file in
         * question only has a single page.
         *
         * @return Bool
@@ -1032,8 +1340,10 @@ abstract class File {
        }
 
        /**
-        * Returns the number of pages of a multipage document, or NULL for
+        * Returns the number of pages of a multipage document, or false for
         * documents which aren't multipage documents
+        *
+        * @return false|int
         */
        function pageCount() {
                if ( !isset( $this->pageCount ) ) {
@@ -1048,6 +1358,12 @@ abstract class File {
 
        /**
         * Calculate the height of a thumbnail using the source and destination width
+        *
+        * @param $srcWidth
+        * @param $srcHeight
+        * @param $dstWidth
+        *
+        * @return int
         */
        static function scaleHeight( $srcWidth, $srcHeight, $dstWidth ) {
                // Exact integer multiply followed by division
@@ -1059,11 +1375,11 @@ abstract class File {
        }
 
        /**
-        * Get an image size array like that returned by getimagesize(), or false if it
+        * Get an image size array like that returned by getImageSize(), or false if it
         * can't be determined.
         *
-        * @param string $fileName The filename
-        * @return array
+        * @param $fileName String: The filename
+        * @return Array
         */
        function getImageSize( $fileName ) {
                if ( !$this->getHandler() ) {
@@ -1075,24 +1391,32 @@ abstract class File {
        /**
         * Get the URL of the image description page. May return false if it is
         * unknown or not applicable.
+        *
+        * @return string
         */
        function getDescriptionUrl() {
-               return $this->repo->getDescriptionUrl( $this->getName() );
+               if ( $this->repo ) {
+                       return $this->repo->getDescriptionUrl( $this->getName() );
+               } else {
+                       return false;
+               }
        }
 
        /**
         * Get the HTML text of the description page, if available
+        *
+        * @return string
         */
        function getDescriptionText() {
                global $wgMemc, $wgLang;
-               if ( !$this->repo->fetchDescription ) {
+               if ( !$this->repo || !$this->repo->fetchDescription ) {
                        return false;
                }
                $renderUrl = $this->repo->getDescriptionRenderUrl( $this->getName(), $wgLang->getCode() );
                if ( $renderUrl ) {
                        if ( $this->repo->descriptionCacheExpiry > 0 ) {
                                wfDebug("Attempting to get the description from cache...");
-                               $key = $this->repo->getLocalCacheKey( 'RemoteFileDescription', 'url', $wgLang->getCode(), 
+                               $key = $this->repo->getLocalCacheKey( 'RemoteFileDescription', 'url', $wgLang->getCode(),
                                                                        $this->getName() );
                                $obj = $wgMemc->get($key);
                                if ($obj) {
@@ -1115,6 +1439,8 @@ abstract class File {
        /**
         * Get discription of file revision
         * STUB
+        *
+        * @return string
         */
        function getDescription() {
                return null;
@@ -1123,6 +1449,8 @@ abstract class File {
        /**
         * Get the 14-character timestamp of the file upload, or false if
         * it doesn't exist
+        *
+        * @return string
         */
        function getTimestamp() {
                $path = $this->getPath();
@@ -1134,6 +1462,8 @@ abstract class File {
 
        /**
         * Get the SHA-1 base 36 hash of the file
+        *
+        * @return string
         */
        function getSha1() {
                return self::sha1Base36( $this->getPath() );
@@ -1141,6 +1471,8 @@ abstract class File {
 
        /**
         * Get the deletion archive key, <sha1>.<ext>
+        *
+        * @return string
         */
        function getStorageKey() {
                $hash = $this->getSha1();
@@ -1149,26 +1481,29 @@ abstract class File {
                }
                $ext = $this->getExtension();
                $dotExt = $ext === '' ? '' : ".$ext";
-               return $hash . $dotExt;                         
+               return $hash . $dotExt;
        }
 
        /**
         * Determine if the current user is allowed to view a particular
         * field of this file, if it's marked as deleted.
         * STUB
-        * @param int $field
-        * @return bool
+        * @param $field Integer
+        * @param $user User object to check, or null to use $wgUser
+        * @return Boolean
         */
-       function userCan( $field ) {
+       function userCan( $field, User $user = null ) {
                return true;
        }
 
        /**
         * Get an associative array containing information about a file in the local filesystem.
         *
-        * @param string $path Absolute local filesystem path
-        * @param mixed $ext The file extension, or true to extract it from the filename.
-        *                   Set it to false to ignore the extension.
+        * @param $path String: absolute local filesystem path
+        * @param $ext Mixed: the file extension, or true to extract it from the filename.
+        *             Set it to false to ignore the extension.
+        *
+        * @return array
         */
        static function getPropsFromPath( $path, $ext = true ) {
                wfProfileIn( __METHOD__ );
@@ -1180,7 +1515,16 @@ abstract class File {
                if ( $info['fileExists'] ) {
                        $magic = MimeMagic::singleton();
 
-                       $info['mime'] = $magic->guessMimeType( $path, $ext );
+                       if ( $ext === true ) {
+                               $i = strrpos( $path, '.' );
+                               $ext = strtolower( $i ? substr( $path, $i + 1 ) : '' );
+                       }
+
+                       # mime type according to file contents
+                       $info['file-mime'] = $magic->guessMimeType( $path, false );
+                       # logical mime type
+                       $info['mime'] = $magic->improveTypeFromExtension( $info['file-mime'], $ext );
+
                        list( $info['major_mime'], $info['minor_mime'] ) = self::splitMime( $info['mime'] );
                        $info['media_type'] = $magic->getMediaType( $path, $info['mime'] );
 
@@ -1232,7 +1576,9 @@ abstract class File {
         * 160 log 2 / log 36 = 30.95, so the 160-bit hash fills 31 digits in base 36
         * fairly neatly.
         *
-        * Returns false on failure
+        * @param $path string
+        *
+        * @return false|string False on failure
         */
        static function sha1Base36( $path ) {
                wfSuppressWarnings();
@@ -1245,6 +1591,9 @@ abstract class File {
                }
        }
 
+       /**
+        * @return string
+        */
        function getLongDesc() {
                $handler = $this->getHandler();
                if ( $handler ) {
@@ -1254,6 +1603,9 @@ abstract class File {
                }
        }
 
+       /**
+        * @return string
+        */
        function getShortDesc() {
                $handler = $this->getHandler();
                if ( $handler ) {
@@ -1263,6 +1615,9 @@ abstract class File {
                }
        }
 
+       /**
+        * @return string
+        */
        function getDimensionsString() {
                $handler = $this->getHandler();
                if ( $handler ) {
@@ -1272,25 +1627,59 @@ abstract class File {
                }
        }
 
+       /**
+        * @return
+        */
        function getRedirected() {
                return $this->redirected;
        }
-       
+
+       /**
+        * @return Title
+        */
        function getRedirectedTitle() {
                if ( $this->redirected ) {
-                       if ( !$this->redirectTitle )
+                       if ( !$this->redirectTitle ) {
                                $this->redirectTitle = Title::makeTitle( NS_FILE, $this->redirected );
+                       }
                        return $this->redirectTitle;
                }
        }
 
+       /**
+        * @param  $from
+        * @return void
+        */
        function redirectedFrom( $from ) {
                $this->redirected = $from;
        }
 
+       /**
+        * @return bool
+        */
        function isMissing() {
                return false;
        }
+
+       /**
+        * Assert that $this->repo is set to a valid FileRepo instance
+        * @throws MWException
+        */
+       protected function assertRepoDefined() {
+               if ( !( $this->repo instanceof $this->repoClass ) ) {
+                       throw new MWException( "A {$this->repoClass} object is not set for this File.\n" );
+               }
+       }
+
+       /**
+        * Assert that $this->title is set to a Title
+        * @throws MWException
+        */
+       protected function assertTitleDefined() {
+               if ( !( $this->title instanceof Title ) ) {
+                       throw new MWException( "A Title object is not set for this File.\n" );
+               }
+       }
 }
 /**
  * Aliases for backwards compatibility with 1.6