Moved File classes to filerepo/file (as well as ArchivedFile)
authorAaron Schulz <aaron@users.mediawiki.org>
Wed, 16 Nov 2011 22:55:48 +0000 (22:55 +0000)
committerAaron Schulz <aaron@users.mediawiki.org>
Wed, 16 Nov 2011 22:55:48 +0000 (22:55 +0000)
15 files changed:
includes/AutoLoader.php
includes/filerepo/ArchivedFile.php [deleted file]
includes/filerepo/File.php [deleted file]
includes/filerepo/ForeignAPIFile.php [deleted file]
includes/filerepo/ForeignDBFile.php [deleted file]
includes/filerepo/LocalFile.php [deleted file]
includes/filerepo/OldLocalFile.php [deleted file]
includes/filerepo/UnregisteredLocalFile.php [deleted file]
includes/filerepo/file/ArchivedFile.php [new file with mode: 0644]
includes/filerepo/file/File.php [new file with mode: 0644]
includes/filerepo/file/ForeignAPIFile.php [new file with mode: 0644]
includes/filerepo/file/ForeignDBFile.php [new file with mode: 0644]
includes/filerepo/file/LocalFile.php [new file with mode: 0644]
includes/filerepo/file/OldLocalFile.php [new file with mode: 0644]
includes/filerepo/file/UnregisteredLocalFile.php [new file with mode: 0644]

index e0de7ec..6594831 100644 (file)
@@ -459,25 +459,27 @@ $wgAutoloadLocalClasses = array(
        'ExternalUser_vB' => 'includes/extauth/vB.php',
 
        # includes/filerepo
-       'ArchivedFile' => 'includes/filerepo/ArchivedFile.php',
-       'File' => 'includes/filerepo/File.php',
        'FileRepo' => 'includes/filerepo/FileRepo.php',
        'FileRepoStatus' => 'includes/filerepo/FileRepoStatus.php',
-       'ForeignAPIFile' => 'includes/filerepo/ForeignAPIFile.php',
        'ForeignAPIRepo' => 'includes/filerepo/ForeignAPIRepo.php',
-       'ForeignDBFile' => 'includes/filerepo/ForeignDBFile.php',
        'ForeignDBRepo' => 'includes/filerepo/ForeignDBRepo.php',
        'ForeignDBViaLBRepo' => 'includes/filerepo/ForeignDBViaLBRepo.php',
        'FSRepo' => 'includes/filerepo/FSRepo.php',
-       'LocalFile' => 'includes/filerepo/LocalFile.php',
-       'LocalFileDeleteBatch' => 'includes/filerepo/LocalFile.php',
-       'LocalFileMoveBatch' => 'includes/filerepo/LocalFile.php',
-       'LocalFileRestoreBatch' => 'includes/filerepo/LocalFile.php',
        'LocalRepo' => 'includes/filerepo/LocalRepo.php',
        'NullRepo' => 'includes/filerepo/NullRepo.php',
-       'OldLocalFile' => 'includes/filerepo/OldLocalFile.php',
        'RepoGroup' => 'includes/filerepo/RepoGroup.php',
-       'UnregisteredLocalFile' => 'includes/filerepo/UnregisteredLocalFile.php',
+       
+       # includes/filerepo/file
+       'ArchivedFile' => 'includes/filerepo/file/ArchivedFile.php',
+       'File' => 'includes/filerepo/file/File.php',
+       'ForeignAPIFile' => 'includes/filerepo/file/ForeignAPIFile.php',
+       'ForeignDBFile' => 'includes/filerepo/file/ForeignDBFile.php',
+       'LocalFile' => 'includes/filerepo/file/LocalFile.php',
+       'LocalFileDeleteBatch' => 'includes/filerepo/file/LocalFile.php',
+       'LocalFileMoveBatch' => 'includes/filerepo/file/LocalFile.php',
+       'LocalFileRestoreBatch' => 'includes/filerepo/file/LocalFile.php',
+       'OldLocalFile' => 'includes/filerepo/file/OldLocalFile.php',
+       'UnregisteredLocalFile' => 'includes/filerepo/file/UnregisteredLocalFile.php',
 
        # includes/installer
        'CliInstaller' => 'includes/installer/CliInstaller.php',
diff --git a/includes/filerepo/ArchivedFile.php b/includes/filerepo/ArchivedFile.php
deleted file mode 100644 (file)
index 34044ba..0000000
+++ /dev/null
@@ -1,471 +0,0 @@
-<?php
-/**
- * Deleted file in the 'filearchive' table
- *
- * @file
- * @ingroup FileRepo
- */
-
-/**
- * Class representing a row of the 'filearchive' table
- *
- * @ingroup FileRepo
- */
-class ArchivedFile {
-       /**#@+
-        * @private
-        */
-       var $id, # filearchive row ID
-               $name, # image name
-               $group, # FileStore storage group
-               $key, # FileStore sha1 key
-               $size, # file dimensions
-               $bits,  # size in bytes
-               $width, # width
-               $height, # height
-               $metadata, # metadata string
-               $mime, # mime type
-               $media_type, # media type
-               $description, # upload description
-               $user, # user ID of uploader
-               $user_text, # user name of uploader
-               $timestamp, # time of upload
-               $dataLoaded, # Whether or not all this has been loaded from the database (loadFromXxx)
-               $deleted, # Bitfield akin to rev_deleted
-               $pageCount,
-               $archive_name;
-
-       /**
-        * @var MediaHandler
-        */
-       var $handler;
-       /**
-        * @var Title
-        */
-       var $title; # image title
-
-       /**#@-*/
-
-       /**
-        * @throws MWException
-        * @param Title $title
-        * @param int $id
-        * @param string $key
-        */
-       function __construct( $title, $id=0, $key='' ) {
-               $this->id = -1;
-               $this->title = false;
-               $this->name = false;
-               $this->group = 'deleted'; // needed for direct use of constructor
-               $this->key = '';
-               $this->size = 0;
-               $this->bits = 0;
-               $this->width = 0;
-               $this->height = 0;
-               $this->metadata = '';
-               $this->mime = "unknown/unknown";
-               $this->media_type = '';
-               $this->description = '';
-               $this->user = 0;
-               $this->user_text = '';
-               $this->timestamp = null;
-               $this->deleted = 0;
-               $this->dataLoaded = false;
-               $this->exists = false;
-
-               if( $title instanceof Title ) {
-                       $this->title = File::normalizeTitle( $title, 'exception' );
-                       $this->name = $title->getDBkey();
-               }
-
-               if ($id) {
-                       $this->id = $id;
-               }
-
-               if ($key) {
-                       $this->key = $key;
-               }
-
-               if ( !$id && !$key && !( $title instanceof Title ) ) {
-                       throw new MWException( "No specifications provided to ArchivedFile constructor." );
-               }
-       }
-
-       /**
-        * Loads a file object from the filearchive table
-        * @return true on success or null
-        */
-       public function load() {
-               if ( $this->dataLoaded ) {
-                       return true;
-               }
-               $conds = array();
-
-               if( $this->id > 0 ) {
-                       $conds['fa_id'] = $this->id;
-               }
-               if( $this->key ) {
-                       $conds['fa_storage_group'] = $this->group;
-                       $conds['fa_storage_key'] = $this->key;
-               }
-               if( $this->title ) {
-                       $conds['fa_name'] = $this->title->getDBkey();
-               }
-
-               if( !count($conds)) {
-                       throw new MWException( "No specific information for retrieving archived file" );
-               }
-
-               if( !$this->title || $this->title->getNamespace() == NS_FILE ) {
-                       $dbr = wfGetDB( DB_SLAVE );
-                       $res = $dbr->select( 'filearchive',
-                               array(
-                                       'fa_id',
-                                       'fa_name',
-                                       'fa_archive_name',
-                                       'fa_storage_key',
-                                       'fa_storage_group',
-                                       'fa_size',
-                                       'fa_bits',
-                                       'fa_width',
-                                       'fa_height',
-                                       'fa_metadata',
-                                       'fa_media_type',
-                                       'fa_major_mime',
-                                       'fa_minor_mime',
-                                       'fa_description',
-                                       'fa_user',
-                                       'fa_user_text',
-                                       'fa_timestamp',
-                                       'fa_deleted' ),
-                               $conds,
-                               __METHOD__,
-                               array( 'ORDER BY' => 'fa_timestamp DESC' ) );
-                       if ( $res == false || $dbr->numRows( $res ) == 0 ) {
-                       // this revision does not exist?
-                               return;
-                       }
-                       $ret = $dbr->resultObject( $res );
-                       $row = $ret->fetchObject();
-
-                       // initialize fields for filestore image object
-                       $this->id = intval($row->fa_id);
-                       $this->name = $row->fa_name;
-                       $this->archive_name = $row->fa_archive_name;
-                       $this->group = $row->fa_storage_group;
-                       $this->key = $row->fa_storage_key;
-                       $this->size = $row->fa_size;
-                       $this->bits = $row->fa_bits;
-                       $this->width = $row->fa_width;
-                       $this->height = $row->fa_height;
-                       $this->metadata = $row->fa_metadata;
-                       $this->mime = "$row->fa_major_mime/$row->fa_minor_mime";
-                       $this->media_type = $row->fa_media_type;
-                       $this->description = $row->fa_description;
-                       $this->user = $row->fa_user;
-                       $this->user_text = $row->fa_user_text;
-                       $this->timestamp = $row->fa_timestamp;
-                       $this->deleted = $row->fa_deleted;
-               } else {
-                       throw new MWException( 'This title does not correspond to an image page.' );
-               }
-               $this->dataLoaded = true;
-               $this->exists = true;
-
-               return true;
-       }
-
-       /**
-        * Loads a file object from the filearchive table
-        *
-        * @param $row
-        *
-        * @return ArchivedFile
-        */
-       public static function newFromRow( $row ) {
-               $file = new ArchivedFile( Title::makeTitle( NS_FILE, $row->fa_name ) );
-
-               $file->id = intval($row->fa_id);
-               $file->name = $row->fa_name;
-               $file->archive_name = $row->fa_archive_name;
-               $file->group = $row->fa_storage_group;
-               $file->key = $row->fa_storage_key;
-               $file->size = $row->fa_size;
-               $file->bits = $row->fa_bits;
-               $file->width = $row->fa_width;
-               $file->height = $row->fa_height;
-               $file->metadata = $row->fa_metadata;
-               $file->mime = "$row->fa_major_mime/$row->fa_minor_mime";
-               $file->media_type = $row->fa_media_type;
-               $file->description = $row->fa_description;
-               $file->user = $row->fa_user;
-               $file->user_text = $row->fa_user_text;
-               $file->timestamp = $row->fa_timestamp;
-               $file->deleted = $row->fa_deleted;
-
-               return $file;
-       }
-
-       /**
-        * Return the associated title object
-        *
-        * @return Title
-        */
-       public function getTitle() {
-               return $this->title;
-       }
-
-       /**
-        * Return the file name
-        *
-        * @return string
-        */
-       public function getName() {
-               return $this->name;
-       }
-
-       /**
-        * @return int
-        */
-       public function getID() {
-               $this->load();
-               return $this->id;
-       }
-
-       /**
-        * @return bool
-        */
-       public function exists() {
-               $this->load();
-               return $this->exists;
-       }
-
-       /**
-        * Return the FileStore key
-        * @return string
-        */
-       public function getKey() {
-               $this->load();
-               return $this->key;
-       }
-
-       /**
-        * Return the FileStore key (overriding base File class)
-        * @return string
-        */
-       public function getStorageKey() {
-               return $this->getKey();
-       }
-
-       /**
-        * Return the FileStore storage group
-        * @return string
-        */
-       public function getGroup() {
-               return $this->group;
-       }
-
-       /**
-        * Return the width of the image
-        * @return int
-        */
-       public function getWidth() {
-               $this->load();
-               return $this->width;
-       }
-
-       /**
-        * Return the height of the image
-        * @return int
-        */
-       public function getHeight() {
-               $this->load();
-               return $this->height;
-       }
-
-       /**
-        * Get handler-specific metadata
-        * @return string
-        */
-       public function getMetadata() {
-               $this->load();
-               return $this->metadata;
-       }
-
-       /**
-        * Return the size of the image file, in bytes
-        * @return int
-        */
-       public function getSize() {
-               $this->load();
-               return $this->size;
-       }
-
-       /**
-        * Return the bits of the image file, in bytes
-        * @return int
-        */
-       public function getBits() {
-               $this->load();
-               return $this->bits;
-       }
-
-       /**
-        * Returns the mime type of the file.
-        * @return string
-        */
-       public function getMimeType() {
-               $this->load();
-               return $this->mime;
-       }
-
-       /**
-        * Get a MediaHandler instance for this file
-        * @return MediaHandler
-        */
-       function getHandler() {
-               if ( !isset( $this->handler ) ) {
-                       $this->handler = MediaHandler::getHandler( $this->getMimeType() );
-               }
-               return $this->handler;
-       }
-
-       /**
-        * Returns the number of pages of a multipage document, or false for
-        * documents which aren't multipage documents
-        */
-       function pageCount() {
-               if ( !isset( $this->pageCount ) ) {
-                       if ( $this->getHandler() && $this->handler->isMultiPage( $this ) ) {
-                               $this->pageCount = $this->handler->pageCount( $this );
-                       } else {
-                               $this->pageCount = false;
-                       }
-               }
-               return $this->pageCount;
-       }
-
-       /**
-        * Return the type of the media in the file.
-        * Use the value returned by this function with the MEDIATYPE_xxx constants.
-        * @return string
-        */
-       public function getMediaType() {
-               $this->load();
-               return $this->media_type;
-       }
-
-       /**
-        * Return upload timestamp.
-        *
-        * @return string
-        */
-       public function getTimestamp() {
-               $this->load();
-               return wfTimestamp( TS_MW, $this->timestamp );
-       }
-
-       /**
-        * Return the user ID of the uploader.
-        *
-        * @return int
-        */
-       public function getUser() {
-               $this->load();
-               if( $this->isDeleted( File::DELETED_USER ) ) {
-                       return 0;
-               } else {
-                       return $this->user;
-               }
-       }
-
-       /**
-        * Return the user name of the uploader.
-        *
-        * @return string
-        */
-       public function getUserText() {
-               $this->load();
-               if( $this->isDeleted( File::DELETED_USER ) ) {
-                       return 0;
-               } else {
-                       return $this->user_text;
-               }
-       }
-
-       /**
-        * Return upload description.
-        *
-        * @return string
-        */
-       public function getDescription() {
-               $this->load();
-               if( $this->isDeleted( File::DELETED_COMMENT ) ) {
-                       return 0;
-               } else {
-                       return $this->description;
-               }
-       }
-
-       /**
-        * Return the user ID of the uploader.
-        *
-        * @return int
-        */
-       public function getRawUser() {
-               $this->load();
-               return $this->user;
-       }
-
-       /**
-        * Return the user name of the uploader.
-        *
-        * @return string
-        */
-       public function getRawUserText() {
-               $this->load();
-               return $this->user_text;
-       }
-
-       /**
-        * Return upload description.
-        *
-        * @return string
-        */
-       public function getRawDescription() {
-               $this->load();
-               return $this->description;
-       }
-
-       /**
-        * Returns the deletion bitfield
-        * @return int
-        */
-       public function getVisibility() {
-               $this->load();
-               return $this->deleted;
-       }
-
-       /**
-        * for file or revision rows
-        *
-        * @param $field Integer: one of DELETED_* bitfield constants
-        * @return bool
-        */
-       public function isDeleted( $field ) {
-               $this->load();
-               return ($this->deleted & $field) == $field;
-       }
-
-       /**
-        * Determine if the current user is allowed to view a particular
-        * field of this FileStore image file, if it's marked as deleted.
-        * @param $field Integer
-        * @param $user User object to check, or null to use $wgUser
-        * @return bool
-        */
-       public function userCan( $field, User $user = null ) {
-               $this->load();
-               return Revision::userCanBitfield( $this->deleted, $field, $user );
-       }
-}
diff --git a/includes/filerepo/File.php b/includes/filerepo/File.php
deleted file mode 100644 (file)
index 8d12ef8..0000000
+++ /dev/null
@@ -1,1683 +0,0 @@
-<?php
-/**
- * Base code for files.
- *
- * @file
- * @ingroup FileRepo
- */
-
-/**
- * Implements some public methods and some protected utility functions which
- * are required by multiple child classes. Contains stub functionality for
- * unimplemented public methods.
- *
- * Stub functions which should be overridden are marked with STUB. Some more
- * concrete functions are also typically overridden by child classes.
- *
- * Note that only the repo object knows what its file class is called. You should
- * never name a file class explictly outside of the repo class. Instead use the
- * repo's factory functions to generate file objects, for example:
- *
- * RepoGroup::singleton()->getLocalRepo()->newFile($title);
- *
- * The convenience functions wfLocalFile() and wfFindFile() should be sufficient
- * in most cases.
- *
- * @ingroup FileRepo
- */
-abstract class File {
-       const DELETED_FILE = 1;
-       const DELETED_COMMENT = 2;
-       const DELETED_USER = 4;
-       const DELETED_RESTRICTED = 8;
-
-       /** 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;
-
-       /**
-        * Some member variables can be lazy-initialised using __get(). The
-        * initialisation function for these variables is always a function named
-        * like getVar(), where Var is the variable name with upper-case first
-        * letter.
-        *
-        * The following variables are initialised in this way in this base class:
-        *    name, extension, handler, path, canRender, isSafeFile,
-        *    transformScript, hashPath, pageCount, url
-        *
-        * Code within this class should generally use the accessor function
-        * directly, since __get() isn't re-entrant and therefore causes bugs that
-        * depend on initialisation order.
-        */
-
-       /**
-        * The following member variables are not lazy-initialised
-        */
-
-       /**
-        * @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 ) ) {
-                       return null;
-               } else {
-                       $this->$name = call_user_func( $function );
-                       return $this->$name;
-               }
-       }
-
-       /**
-        * Normalize a file extension to the common form, and ensure it's clean.
-        * Extensions with non-alphanumeric characters will be discarded.
-        *
-        * @param $ext string (without the .)
-        * @return string
-        */
-       static function normalizeExtension( $ext ) {
-               $lower = strtolower( $ext );
-               $squish = array(
-                       'htm' => 'html',
-                       'jpeg' => 'jpg',
-                       'mpeg' => 'mpg',
-                       'tiff' => 'tif',
-                       'ogv' => 'ogg' );
-               if( isset( $squish[$lower] ) ) {
-                       return $squish[$lower];
-               } elseif( preg_match( '/^[0-9a-z]+$/', $lower ) ) {
-                       return $lower;
-               } else {
-                       return '';
-               }
-       }
-
-       /**
-        * Checks if file extensions are compatible
-        *
-        * @param $old File Old file
-        * @param $new string New name
-        *
-        * @return bool|null
-        */
-       static function checkExtensionCompatibility( File $old, $new ) {
-               $oldMime = $old->getMimeType();
-               $n = strrpos( $new, '.' );
-               $newExt = self::normalizeExtension(
-                       $n ? substr( $new, $n + 1 ) : '' );
-               $mimeMagic = MimeMagic::singleton();
-               return $mimeMagic->isMatchingExtension( $newExt, $oldMime );
-       }
-
-       /**
-        * Upgrade the database row if there is one
-        * Called by ImagePage
-        * STUB
-        */
-       function upgradeRow() {}
-
-       /**
-        * Split an internet media type into its two components; if not
-        * a two-part name, set the minor type to 'unknown'.
-        *
-        * @param string $mime "text/html" etc
-        * @return array ("text", "html") etc
-        */
-       public static function splitMime( $mime ) {
-               if( strpos( $mime, '/' ) !== false ) {
-                       return explode( '/', $mime, 2 );
-               } else {
-                       return array( $mime, 'unknown' );
-               }
-       }
-
-       /**
-        * 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;
-       }
-
-       /**
-        * Get the file extension, e.g. "svg"
-        *
-        * @return string
-        */
-       function getExtension() {
-               if ( !isset( $this->extension ) ) {
-                       $n = strrpos( $this->getName(), '.' );
-                       $this->extension = self::normalizeExtension(
-                               $n ? substr( $this->getName(), $n + 1 ) : '' );
-               }
-               return $this->extension;
-       }
-
-       /**
-        * 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 ) {
-                       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;
-       }
-
-       /**
-        * 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
-        */
-       public function getFullUrl() {
-               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 {
-                               wfDebug(__METHOD__.': supposed to render '.$this->getName().' ('.$this->getMimeType()."), but can't!\n");
-                               return $this->getURL(); #hm... return NULL?
-                       }
-               } else {
-                       return $this->getURL();
-               }
-       }
-
-       /**
-       * Return the full filesystem path to the file. Note that this does
-       * not mean that a file actually exists under that location.
-       *
-       * This path depends on whether directory hashing is active or not,
-       * i.e. whether the files are all found in the same directory,
-       * or in hashed paths like /images/3/3c.
-       *
-       * Most callers don't check the return value, but ForeignAPIFile::getPath
-       * returns false.
-        *
-        * @return string|false
-       */
-       public function getPath() {
-               if ( !isset( $this->path ) ) {
-                       $this->assertRepoDefined();
-                       $this->path = $this->repo->getZonePath( 'public' ) . '/' . $this->getRel();
-               }
-               return $this->path;
-       }
-
-       /**
-        * 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;
-       }
-
-       /**
-        * Return the height of the image. Returns false if the height is unknown
-        * or undefined
-        *
-        * STUB
-        * Overridden by LocalFile, UnregisteredLocalFile
-        *
-        * @param $page int
-        *
-        * @return false|number
-        */
-       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;
-       }
-
-       /**
-        * Get the duration of a media file in seconds
-        *
-        * @return number
-        */
-       public function getLength() {
-               $handler = $this->getHandler();
-               if ( $handler ) {
-                       return $handler->getLength( $this );
-               } else {
-                       return 0;
-               }
-       }
-
-       /**
-        * 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;
-       }
-
-       /**
-       * 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;
-       }
-
-       /**
-        * Return the size of the image file, in bytes
-        * Overridden by LocalFile, UnregisteredLocalFile
-        * STUB
-        */
-       public function getSize() {
-               return false;
-       }
-
-       /**
-        * Returns the mime type of the file.
-        * Overridden by LocalFile, UnregisteredLocalFile
-        * STUB
-        *
-        * @return string
-        */
-       function getMimeType() {
-               return 'unknown/unknown';
-       }
-
-       /**
-        * Return the type of the media in the file.
-        * Use the value returned by this function with the MEDIATYPE_xxx constants.
-        * Overridden by LocalFile,
-        * STUB
-        */
-       function getMediaType() { return MEDIATYPE_UNKNOWN; }
-
-       /**
-        * Checks if the output of transform() for this file is likely
-        * to be valid. If this is false, various user elements will
-        * display a placeholder instead.
-        *
-        * Currently, this checks if the file is an image format
-        * 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 ) ) {
-                       $this->canRender = $this->getHandler() && $this->handler->canRender( $this );
-               }
-               return $this->canRender;
-       }
-
-       /**
-        * Accessor for __get()
-        */
-       protected function getCanRender() {
-               return $this->canRender();
-       }
-
-       /**
-        * Return true if the file is of a type that can't be directly
-        * rendered by typical browsers and needs to be re-rasterized.
-        *
-        * This returns true for everything but the bitmap types
-        * supported by all browsers, i.e. JPEG; GIF and PNG. It will
-        * also return true for any non-image formats.
-        *
-        * @return bool
-        */
-       function mustRender() {
-               return $this->getHandler() && $this->handler->mustRender( $this );
-       }
-
-       /**
-        * Alias for canRender()
-        *
-        * @return bool
-        */
-       function allowInlineDisplay() {
-               return $this->canRender();
-       }
-
-       /**
-        * Determines if this media file is in a format that is unlikely to
-        * contain viruses or malicious content. It uses the global
-        * $wgTrustedMediaFormats list to determine if the file is safe.
-        *
-        * This is used to show a warning on the description page of non-safe files.
-        * It may also be used to disallow direct [[media:...]] links to such files.
-        *
-        * 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 ) ) {
-                       $this->isSafeFile = $this->_getIsSafeFile();
-               }
-               return $this->isSafeFile;
-       }
-
-       /**
-        * Accessor for __get()
-        *
-        * @return bool
-        */
-       protected function getIsSafeFile() {
-               return $this->isSafeFile();
-       }
-
-       /**
-        * Uncached accessor
-        *
-        * @return bool
-        */
-       protected function _getIsSafeFile() {
-               if ( $this->allowInlineDisplay() ) {
-                       return true;
-               }
-               if ($this->isTrustedFile()) {
-                       return true;
-               }
-
-               global $wgTrustedMediaFormats;
-
-               $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 ( $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.
-        *
-        * @return bool
-        */
-       function isTrustedFile() {
-               #this could be implemented to check a flag in the databas,
-               #look for signatures, etc
-               return false;
-       }
-
-       /**
-        * Returns true if file exists in the repository.
-        *
-        * Overridden by LocalFile to avoid unnecessary stat calls.
-        *
-        * @return boolean Whether file exists in the repository.
-        */
-       public function exists() {
-               return $this->getPath() && file_exists( $this->path );
-       }
-
-       /**
-        * Returns true if file exists in the repository and can be included in a page.
-        * 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() {
-               return $this->exists();
-       }
-
-       /**
-        * @return string
-        */
-       function getTransformScript() {
-               if ( !isset( $this->transformScript ) ) {
-                       $this->transformScript = false;
-                       if ( $this->repo ) {
-                               $script = $this->repo->getThumbScriptUrl();
-                               if ( $script ) {
-                                       $this->transformScript = "$script?f=" . urlencode( $this->getName() );
-                               }
-                       }
-               }
-               return $this->transformScript;
-       }
-
-       /**
-        * Get a ThumbnailImage which is the same size as the source
-        *
-        * @param $handlerParams array
-        *
-        * @return string
-        */
-       function getUnscaledThumb( $handlerParams = array() ) {
-               $hp =& $handlerParams;
-               $page = isset( $hp['page'] ) ? $hp['page'] : false;
-               $width = $this->getWidth( $page );
-               if ( !$width ) {
-                       return $this->iconThumb();
-               }
-               $hp['width'] = $width;
-               return $this->transform( $hp );
-       }
-
-       /**
-        * Return the file name of a thumbnail with the specified 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(), $params );
-               $thumbName = $this->handler->makeParamString( $params ) . '-' . $name;
-               if ( $thumbExt != $extension ) {
-                       $thumbName .= ".$thumbExt";
-               }
-               return $thumbName;
-       }
-
-       /**
-        * Create a thumbnail of the image having the specified width/height.
-        * The thumbnail will not be created if the width is larger than the
-        * image's width. Let the browser do the scaling in this case.
-        * The thumbnail is stored on disk and is only computed if the thumbnail
-        * file does not exist OR if it is older than the image.
-        * Returns the URL.
-        *
-        * Keeps aspect ratio of original image. If both width and height are
-        * specified, the generated image will be no bigger than width x height,
-        * and will also have correct aspect ratio.
-        *
-        * @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 );
-               if ( $height != -1 ) {
-                       $params['height'] = $height;
-               }
-               $thumb = $this->transform( $params );
-               if( is_null( $thumb ) || $thumb->isError() ) return '';
-               return $thumb->getUrl();
-       }
-
-       /**
-        * Do the work of a transform (from an original into a thumb).
-        * Contains filesystem-specific functions.
-        *
-        * @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
-        *
-        * @return MediaTransformOutput | false
-        */
-       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 );
-               }
-
-               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 $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;
-
-               wfProfileIn( __METHOD__ );
-               do {
-                       if ( !$this->canRender() ) {
-                               // not a bitmap or renderable image, don't try.
-                               $thumb = $this->iconThumb();
-                               break;
-                       }
-
-                       // Get the descriptionUrl to embed it as comment into the thumbnail. Bug 19791.
-                       $descriptionUrl =  $this->getDescriptionUrl();
-                       if ( $descriptionUrl ) {
-                               $params['descriptionUrl'] = wfExpandUrl( $descriptionUrl, PROTO_CANONICAL );
-                       }
-
-                       $script = $this->getTransformScript();
-                       if ( $script && !($flags & self::RENDER_NOW) ) {
-                               // Use a script to transform on client request, if possible
-                               $thumb = $this->handler->getScriptedTransform( $this, $script, $params );
-                               if( $thumb ) {
-                                       break;
-                               }
-                       }
-
-                       $normalisedParams = $params;
-                       $this->handler->normaliseParams( $this, $normalisedParams );
-                       $thumbName = $this->thumbName( $normalisedParams );
-                       $thumbUrl = $this->getThumbUrl( $thumbName );
-
-                       $thumb = $this->maybeDoTransform( $thumbName, $thumbUrl, $params, $flags );
-
-                       // 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 ) );
-                       }
-               } while (false);
-
-               wfProfileOut( __METHOD__ );
-               return is_object( $thumb ) ? $thumb : false;
-       }
-
-       /**
-        * Hook into transform() to allow migration of thumbnail files
-        * STUB
-        * Overridden by LocalFile
-        */
-       function migrateThumbFile( $thumbName ) {}
-
-       /**
-        * Get a MediaHandler instance for this file
-        * @return MediaHandler
-        */
-       function getHandler() {
-               if ( !isset( $this->handler ) ) {
-                       $this->handler = MediaHandler::getHandler( $this->getMimeType() );
-               }
-               return $this->handler;
-       }
-
-       /**
-        * Get a ThumbnailImage representing a file type icon
-        * @return ThumbnailImage
-        */
-       function iconThumb() {
-               global $wgStylePath, $wgStyleDirectory;
-
-               $try = array( 'fileicon-' . $this->getExtension() . '.png', 'fileicon.png' );
-               foreach( $try as $icon ) {
-                       $path = '/common/images/icons/' . $icon;
-                       $filepath = $wgStyleDirectory . $path;
-                       if( file_exists( $filepath ) ) {
-                               return new ThumbnailImage( $this, $wgStylePath . $path, 120, 120 );
-                       }
-               }
-               return null;
-       }
-
-       /**
-        * Get last thumbnailing error.
-        * Largely obsolete.
-        */
-       function getLastError() {
-               return $this->lastError;
-       }
-
-       /**
-        * Get all thumbnail names previously generated for this file
-        * STUB
-        * Overridden by LocalFile
-        */
-       function getThumbnails() {
-               return array();
-       }
-
-       /**
-        * Purge shared caches such as thumbnails and DB data caching
-        * STUB
-        * Overridden by LocalFile
-        */
-       function purgeCache() {}
-
-       /**
-        * Purge the file description page, but don't go after
-        * pages using the file. Use when modifying file history
-        * but not the current data.
-        */
-       function purgeDescription() {
-               $title = $this->getTitle();
-               if ( $title ) {
-                       $title->invalidateCache();
-                       $title->purgeSquid();
-               }
-       }
-
-       /**
-        * Purge metadata and all affected pages when the file is created,
-        * deleted, or majorly updated.
-        */
-       function purgeEverything() {
-               // Delete thumbnails and refresh file metadata cache
-               $this->purgeCache();
-               $this->purgeDescription();
-
-               // Purge cache of all pages using this file
-               $title = $this->getTitle();
-               if ( $title ) {
-                       $update = new HTMLCacheUpdate( $title, 'imagelinks' );
-                       $update->doUpdate();
-               }
-       }
-
-       /**
-        * Return a fragment of the history of file.
-        *
-        * STUB
-        * @param $limit integer Limit of rows to return
-        * @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();
-       }
-
-       /**
-        * Return the history of this file, line by line. Starts with current version,
-        * then old versions. Should return an object similar to an image/oldimage
-        * database row.
-        *
-        * STUB
-        * Overridden in LocalFile
-        */
-       public function nextHistoryLine() {
-               return false;
-       }
-
-       /**
-        * Reset the history pointer to the first element of the history.
-        * Always call this function after using nextHistoryLine() to free db resources
-        * STUB
-        * Overridden in LocalFile.
-        */
-       public function resetHistory() {}
-
-       /**
-        * 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;
-       }
-
-       /**
-        * Get the path of the file relative to the public zone root
-        *
-        * @return string
-        */
-       function getRel() {
-               return $this->getHashPath() . $this->getName();
-       }
-
-       /**
-        * Get urlencoded relative path of the file
-        *
-        * @return string
-        */
-       function getUrlRel() {
-               return $this->getHashPath() . rawurlencode( $this->getName() );
-       }
-
-       /**
-        * 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 ) {
-                       $path = substr( $path, 0, -1 );
-               } else {
-                       $path .= $suffix;
-               }
-               return $path;
-       }
-
-       /**
-        * 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 ) {
-               $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
-        *
-        * @param $suffix bool|string if not false, the name of a thumbnail file
-        *
-        * @return string
-        */
-       function getThumbPath( $suffix = false ) {
-               $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
-        *
-        * @param $suffix bool|string if not false, the name of an archived file
-        *
-        * @return string
-        */
-       function getArchiveUrl( $suffix = false ) {
-               $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 {
-                       $path .= rawurlencode( $suffix );
-               }
-               return $path;
-       }
-
-       /**
-        * 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 );
-               }
-               return $path;
-       }
-
-       /**
-        * 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 );
-               } else {
-                       $path .= rawurlencode( $suffix );
-               }
-               return $path;
-       }
-
-       /**
-        * 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 );
-               }
-               return $path;
-       }
-
-       /**
-        * 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 );
-               }
-               return $path;
-       }
-
-       /**
-        * @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' );
-       }
-
-       /**
-        * 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();
-       }
-
-       /**
-        * Move or copy a file to its public location. If a file exists at the
-        * 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 $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 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
-        */
-       function publish( $srcPath, $flags = 0 ) {
-               $this->readOnlyError();
-       }
-
-       /**
-        * @return bool
-        */
-       function formatMetadata() {
-               if ( !$this->getHandler() ) {
-                       return false;
-               }
-               return $this->getHandler()->formatMetadata( $this, $this->getMetadata() );
-       }
-
-       /**
-        * Returns true if the file comes from the local file repository.
-        *
-        * @return bool
-        */
-       function isLocal() {
-               $repo = $this->getRepo();
-               return $repo && $repo->isLocal();
-       }
-
-       /**
-        * Returns the name of the repository.
-        *
-        * @return string
-        */
-       function getRepoName() {
-               return $this->repo ? $this->repo->getName() : 'unknown';
-       }
-
-       /**
-        * Returns the repository
-        *
-        * @return FileRepo|false
-        */
-       function getRepo() {
-               return $this->repo;
-       }
-
-       /**
-        * Returns true if the image is an old version
-        * STUB
-        *
-        * @return bool
-        */
-       function isOld() {
-               return false;
-       }
-
-       /**
-        * 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;
-       }
-
-       /**
-        * Was this file ever deleted from the wiki?
-        *
-        * @return bool
-        */
-       function wasDeleted() {
-               $title = $this->getTitle();
-               return $title && $title->isDeletedQuick();
-       }
-
-       /**
-        * Move file to the new title
-        *
-        * Move current, old version and all thumbnails
-        * to the new filename. Old file is deleted.
-        *
-        * Cache purging is done; checks for validity
-        * and logging are caller's responsibility
-        *
-        * @param $target Title New file name
-        * @return FileRepoStatus object.
-        */
-        function move( $target ) {
-               $this->readOnlyError();
-        }
-
-       /**
-        * Delete all versions of the file.
-        *
-        * Moves the files into an archive directory (or deletes them)
-        * and removes the database rows.
-        *
-        * Cache purging is done; logging is caller's responsibility.
-        *
-        * @param $reason String
-        * @param $suppress Boolean: hide content from sysops?
-        * @return true on success, false on some kind of failure
-        * STUB
-        * Overridden by LocalFile
-        */
-       function delete( $reason, $suppress = false ) {
-               $this->readOnlyError();
-       }
-
-       /**
-        * Restore all or specified deleted revisions to the given file.
-        * Permissions and logging are left to the caller.
-        *
-        * May throw database exceptions on error.
-        *
-        * @param $versions array set of record ids of deleted items to restore,
-        *                    or empty to restore all revisions.
-        * @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 ) {
-               $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
-        * question only has a single page.
-        *
-        * @return Bool
-        */
-       function isMultipage() {
-               return $this->getHandler() && $this->handler->isMultiPage( $this );
-       }
-
-       /**
-        * 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 ) ) {
-                       if ( $this->getHandler() && $this->handler->isMultiPage( $this ) ) {
-                               $this->pageCount = $this->handler->pageCount( $this );
-                       } else {
-                               $this->pageCount = false;
-                       }
-               }
-               return $this->pageCount;
-       }
-
-       /**
-        * 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
-               if ( $srcWidth == 0 ) {
-                       return 0;
-               } else {
-                       return round( $srcHeight * $dstWidth / $srcWidth );
-               }
-       }
-
-       /**
-        * Get an image size array like that returned by getImageSize(), or false if it
-        * can't be determined.
-        *
-        * @param $fileName String: The filename
-        * @return Array
-        */
-       function getImageSize( $fileName ) {
-               if ( !$this->getHandler() ) {
-                       return false;
-               }
-               return $this->handler->getImageSize( $this, $fileName );
-       }
-
-       /**
-        * Get the URL of the image description page. May return false if it is
-        * unknown or not applicable.
-        *
-        * @return string
-        */
-       function getDescriptionUrl() {
-               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 || !$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(),
-                                                                       $this->getName() );
-                               $obj = $wgMemc->get($key);
-                               if ($obj) {
-                                       wfDebug("success!\n");
-                                       return $obj;
-                               }
-                               wfDebug("miss\n");
-                       }
-                       wfDebug( "Fetching shared description from $renderUrl\n" );
-                       $res = Http::get( $renderUrl );
-                       if ( $res && $this->repo->descriptionCacheExpiry > 0 ) {
-                               $wgMemc->set( $key, $res, $this->repo->descriptionCacheExpiry );
-                       }
-                       return $res;
-               } else {
-                       return false;
-               }
-       }
-
-       /**
-        * Get discription of file revision
-        * STUB
-        *
-        * @return string
-        */
-       function getDescription() {
-               return null;
-       }
-
-       /**
-        * Get the 14-character timestamp of the file upload, or false if
-        * it doesn't exist
-        *
-        * @return string
-        */
-       function getTimestamp() {
-               $path = $this->getPath();
-               if ( !file_exists( $path ) ) {
-                       return false;
-               }
-               return wfTimestamp( TS_MW, filemtime( $path ) );
-       }
-
-       /**
-        * Get the SHA-1 base 36 hash of the file
-        *
-        * @return string
-        */
-       function getSha1() {
-               return self::sha1Base36( $this->getPath() );
-       }
-
-       /**
-        * Get the deletion archive key, <sha1>.<ext>
-        *
-        * @return string
-        */
-       function getStorageKey() {
-               $hash = $this->getSha1();
-               if ( !$hash ) {
-                       return false;
-               }
-               $ext = $this->getExtension();
-               $dotExt = $ext === '' ? '' : ".$ext";
-               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 $field Integer
-        * @param $user User object to check, or null to use $wgUser
-        * @return Boolean
-        */
-       function userCan( $field, User $user = null ) {
-               return true;
-       }
-
-       /**
-        * Get an associative array containing information about a file in the local filesystem.
-        *
-        * @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__ );
-               wfDebug( __METHOD__.": Getting file info for $path\n" );
-               $info = array(
-                       'fileExists' => file_exists( $path ) && !is_dir( $path )
-               );
-               $gis = false;
-               if ( $info['fileExists'] ) {
-                       $magic = MimeMagic::singleton();
-
-                       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'] );
-
-                       # Get size in bytes
-                       $info['size'] = filesize( $path );
-
-                       # Height, width and metadata
-                       $handler = MediaHandler::getHandler( $info['mime'] );
-                       if ( $handler ) {
-                               $tempImage = (object)array();
-                               $info['metadata'] = $handler->getMetadata( $tempImage, $path );
-                               $gis = $handler->getImageSize( $tempImage, $path, $info['metadata'] );
-                       } else {
-                               $gis = false;
-                               $info['metadata'] = '';
-                       }
-                       $info['sha1'] = self::sha1Base36( $path );
-
-                       wfDebug(__METHOD__.": $path loaded, {$info['size']} bytes, {$info['mime']}.\n");
-               } else {
-                       $info['mime'] = null;
-                       $info['media_type'] = MEDIATYPE_UNKNOWN;
-                       $info['metadata'] = '';
-                       $info['sha1'] = '';
-                       wfDebug(__METHOD__.": $path NOT FOUND!\n");
-               }
-               if( $gis ) {
-                       # NOTE: $gis[2] contains a code for the image type. This is no longer used.
-                       $info['width'] = $gis[0];
-                       $info['height'] = $gis[1];
-                       if ( isset( $gis['bits'] ) ) {
-                               $info['bits'] = $gis['bits'];
-                       } else {
-                               $info['bits'] = 0;
-                       }
-               } else {
-                       $info['width'] = 0;
-                       $info['height'] = 0;
-                       $info['bits'] = 0;
-               }
-               wfProfileOut( __METHOD__ );
-               return $info;
-       }
-
-       /**
-        * Get a SHA-1 hash of a file in the local filesystem, in base-36 lower case
-        * encoding, zero padded to 31 digits.
-        *
-        * 160 log 2 / log 36 = 30.95, so the 160-bit hash fills 31 digits in base 36
-        * fairly neatly.
-        *
-        * @param $path string
-        *
-        * @return false|string False on failure
-        */
-       static function sha1Base36( $path ) {
-               wfSuppressWarnings();
-               $hash = sha1_file( $path );
-               wfRestoreWarnings();
-               if ( $hash === false ) {
-                       return false;
-               } else {
-                       return wfBaseConvert( $hash, 16, 36, 31 );
-               }
-       }
-
-       /**
-        * @return string
-        */
-       function getLongDesc() {
-               $handler = $this->getHandler();
-               if ( $handler ) {
-                       return $handler->getLongDesc( $this );
-               } else {
-                       return MediaHandler::getGeneralLongDesc( $this );
-               }
-       }
-
-       /**
-        * @return string
-        */
-       function getShortDesc() {
-               $handler = $this->getHandler();
-               if ( $handler ) {
-                       return $handler->getShortDesc( $this );
-               } else {
-                       return MediaHandler::getGeneralShortDesc( $this );
-               }
-       }
-
-       /**
-        * @return string
-        */
-       function getDimensionsString() {
-               $handler = $this->getHandler();
-               if ( $handler ) {
-                       return $handler->getDimensionsString( $this );
-               } else {
-                       return '';
-               }
-       }
-
-       /**
-        * @return
-        */
-       function getRedirected() {
-               return $this->redirected;
-       }
-
-       /**
-        * @return Title
-        */
-       function getRedirectedTitle() {
-               if ( $this->redirected ) {
-                       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" );
-               }
-       }
-}
diff --git a/includes/filerepo/ForeignAPIFile.php b/includes/filerepo/ForeignAPIFile.php
deleted file mode 100644 (file)
index 9cd798d..0000000
+++ /dev/null
@@ -1,242 +0,0 @@
-<?php
-/**
- * Foreign file accessible through api.php requests.
- *
- * @file
- * @ingroup FileRepo
- */
-
-/**
- * Foreign file accessible through api.php requests.
- * Very hacky and inefficient, do not use :D
- *
- * @ingroup FileRepo
- */
-class ForeignAPIFile extends File {
-
-       private $mExists;
-
-       protected $repoClass = 'ForeignApiRepo';
-
-       /**
-        * @param $title
-        * @param $repo ForeignApiRepo
-        * @param $info
-        * @param bool $exists
-        */
-       function __construct( $title, $repo, $info, $exists = false ) {
-               parent::__construct( $title, $repo );
-
-               $this->mInfo = $info;
-               $this->mExists = $exists;
-
-               $this->assertRepoDefined();
-       }
-
-       /**
-        * @param $title Title
-        * @param $repo ForeignApiRepo
-        * @return ForeignAPIFile|null
-        */
-       static function newFromTitle( Title $title, $repo ) {
-               $data = $repo->fetchImageQuery( array(
-                       'titles' => 'File:' . $title->getDBKey(),
-                       'iiprop' => self::getProps(),
-                       'prop'   => 'imageinfo',
-                       'iimetadataversion' => MediaHandler::getMetadataVersion()
-               ) );
-
-               $info = $repo->getImageInfo( $data );
-
-               if( $info ) {
-                       $lastRedirect = isset( $data['query']['redirects'] )
-                               ? count( $data['query']['redirects'] ) - 1
-                               : -1;
-                       if( $lastRedirect >= 0 ) {
-                               $newtitle = Title::newFromText( $data['query']['redirects'][$lastRedirect]['to']);
-                               $img = new self( $newtitle, $repo, $info, true );
-                               if( $img ) {
-                                       $img->redirectedFrom( $title->getDBkey() );
-                               }
-                       } else {
-                               $img = new self( $title, $repo, $info, true );
-                       }
-                       return $img;
-               } else {
-                       return null;
-               }
-       }
-
-       /**
-        * Get the property string for iiprop and aiprop
-        */
-       static function getProps() {
-               return 'timestamp|user|comment|url|size|sha1|metadata|mime';
-       }
-
-       // Dummy functions...
-       public function exists() {
-               return $this->mExists;
-       }
-
-       public function getPath() {
-               return false;
-       }
-
-       function transform( $params, $flags = 0 ) {
-               if( !$this->canRender() ) {
-                       // show icon
-                       return parent::transform( $params, $flags );
-               }
-
-               // Note, the this->canRender() check above implies
-               // that we have a handler, and it can do makeParamString.
-               $otherParams = $this->handler->makeParamString( $params );
-
-               $thumbUrl = $this->repo->getThumbUrlFromCache(
-                       $this->getName(),
-                       isset( $params['width'] ) ? $params['width'] : -1,
-                       isset( $params['height'] ) ? $params['height'] : -1,
-                       $otherParams );
-               return $this->handler->getTransform( $this, 'bogus', $thumbUrl, $params );
-       }
-
-       // Info we can get from API...
-       public function getWidth( $page = 1 ) {
-               return isset( $this->mInfo['width'] ) ? intval( $this->mInfo['width'] ) : 0;
-       }
-
-       /**
-        * @param $page int
-        * @return int
-        */
-       public function getHeight( $page = 1 ) {
-               return isset( $this->mInfo['height'] ) ? intval( $this->mInfo['height'] ) : 0;
-       }
-
-       public function getMetadata() {
-               if ( isset( $this->mInfo['metadata'] ) ) {
-                       return serialize( self::parseMetadata( $this->mInfo['metadata'] ) );
-               }
-               return null;
-       }
-
-       public static function parseMetadata( $metadata ) {
-               if( !is_array( $metadata ) ) {
-                       return $metadata;
-               }
-               $ret = array();
-               foreach( $metadata as $meta ) {
-                       $ret[ $meta['name'] ] = self::parseMetadata( $meta['value'] );
-               }
-               return $ret;
-       }
-
-       public function getSize() {
-               return isset( $this->mInfo['size'] ) ? intval( $this->mInfo['size'] ) : null;
-       }
-
-       public function getUrl() {
-               return isset( $this->mInfo['url'] ) ? strval( $this->mInfo['url'] ) : null;
-       }
-
-       public function getUser( $method='text' ) {
-               return isset( $this->mInfo['user'] ) ? strval( $this->mInfo['user'] ) : null;
-       }
-
-       public function getDescription() {
-               return isset( $this->mInfo['comment'] ) ? strval( $this->mInfo['comment'] ) : null;
-       }
-
-       function getSha1() {
-               return isset( $this->mInfo['sha1'] ) ?
-                       wfBaseConvert( strval( $this->mInfo['sha1'] ), 16, 36, 31 ) :
-                       null;
-       }
-
-       function getTimestamp() {
-               return wfTimestamp( TS_MW,
-                       isset( $this->mInfo['timestamp'] ) ?
-                       strval( $this->mInfo['timestamp'] ) :
-                       null
-               );
-       }
-
-       function getMimeType() {
-               if( !isset( $this->mInfo['mime'] ) ) {
-                       $magic = MimeMagic::singleton();
-                       $this->mInfo['mime'] = $magic->guessTypesForExtension( $this->getExtension() );
-               }
-               return $this->mInfo['mime'];
-       }
-
-       /// @todo FIXME: May guess wrong on file types that can be eg audio or video
-       function getMediaType() {
-               $magic = MimeMagic::singleton();
-               return $magic->getMediaType( null, $this->getMimeType() );
-       }
-
-       function getDescriptionUrl() {
-               return isset( $this->mInfo['descriptionurl'] )
-                       ? $this->mInfo['descriptionurl']
-                       : false;
-       }
-
-       /**
-        * Only useful if we're locally caching thumbs anyway...
-        */
-       function getThumbPath( $suffix = '' ) {
-               if ( $this->repo->canCacheThumbs() ) {
-                       $path = $this->repo->getZonePath('thumb') . '/' . $this->getHashPath( $this->getName() );
-                       if ( $suffix ) {
-                               $path = $path . $suffix . '/';
-                       }
-                       return $path;
-               } else {
-                       return null;
-               }
-       }
-
-       function getThumbnails() {
-               $files = array();
-               $dir = $this->getThumbPath( $this->getName() );
-               if ( is_dir( $dir ) ) {
-                       $handle = opendir( $dir );
-                       if ( $handle ) {
-                               while ( false !== ( $file = readdir($handle) ) ) {
-                                       if ( $file[0] != '.'  ) {
-                                               $files[] = $file;
-                                       }
-                               }
-                               closedir( $handle );
-                       }
-               }
-               return $files;
-       }
-
-       function purgeCache() {
-               $this->purgeThumbnails();
-               $this->purgeDescriptionPage();
-       }
-
-       function purgeDescriptionPage() {
-               global $wgMemc, $wgContLang;
-               $url = $this->repo->getDescriptionRenderUrl( $this->getName(), $wgContLang->getCode() );
-               $key = $this->repo->getLocalCacheKey( 'RemoteFileDescription', 'url', md5($url) );
-               $wgMemc->delete( $key );
-       }
-
-       function purgeThumbnails() {
-               global $wgMemc;
-               $key = $this->repo->getLocalCacheKey( 'ForeignAPIRepo', 'ThumbUrl', $this->getName() );
-               $wgMemc->delete( $key );
-               $files = $this->getThumbnails();
-               $dir = $this->getThumbPath( $this->getName() );
-               foreach ( $files as $file ) {
-                       unlink( $dir . $file );
-               }
-               if ( is_dir( $dir ) ) {
-                       rmdir( $dir ); // Might have already gone away, spews errors if we don't.
-               }
-       }
-}
diff --git a/includes/filerepo/ForeignDBFile.php b/includes/filerepo/ForeignDBFile.php
deleted file mode 100644 (file)
index 09bee39..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-<?php
-/**
- * Foreign file with an accessible MediaWiki database
- *
- * @file
- * @ingroup FileRepo
- */
-
-/**
- * Foreign file with an accessible MediaWiki database
- *
- * @ingroup FileRepo
- */
-class ForeignDBFile extends LocalFile {
-
-       /**
-        * @param $title
-        * @param $repo
-        * @param $unused
-        * @return ForeignDBFile
-        */
-       static function newFromTitle( $title, $repo, $unused = null ) {
-               return new self( $title, $repo );
-       }
-
-       /**
-        * Create a ForeignDBFile from a title
-        * Do not call this except from inside a repo class.
-        *
-        * @param $row
-        * @param $repo
-        *
-        * @return ForeignDBFile
-        */
-       static function newFromRow( $row, $repo ) {
-               $title = Title::makeTitle( NS_FILE, $row->img_name );
-               $file = new self( $title, $repo );
-               $file->loadFromRow( $row );
-               return $file;
-       }
-
-       function publish( $srcPath, $flags = 0 ) {
-               $this->readOnlyError();
-       }
-
-       function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
-               $watch = false, $timestamp = false ) {
-               $this->readOnlyError();
-       }
-
-       function restore( $versions = array(), $unsuppress = false ) {
-               $this->readOnlyError();
-       }
-
-       function delete( $reason, $suppress = false ) {
-               $this->readOnlyError();
-       }
-
-       function move( $target ) {
-               $this->readOnlyError();
-       }
-
-       /**
-        * @return string
-        */
-       function getDescriptionUrl() {
-               // Restore remote behaviour
-               return File::getDescriptionUrl();
-       }
-
-       /**
-        * @return string
-        */
-       function getDescriptionText() {
-               // Restore remote behaviour
-               return File::getDescriptionText();
-       }
-}
diff --git a/includes/filerepo/LocalFile.php b/includes/filerepo/LocalFile.php
deleted file mode 100644 (file)
index d11030f..0000000
+++ /dev/null
@@ -1,2323 +0,0 @@
-<?php
-/**
- * Local file in the wiki's own database
- *
- * @file
- * @ingroup FileRepo
- */
-
-/**
- * Bump this number when serialized cache records may be incompatible.
- */
-define( 'MW_FILE_VERSION', 8 );
-
-/**
- * Class to represent a local file in the wiki's own database
- *
- * Provides methods to retrieve paths (physical, logical, URL),
- * to generate image thumbnails or for uploading.
- *
- * Note that only the repo object knows what its file class is called. You should
- * never name a file class explictly outside of the repo class. Instead use the
- * repo's factory functions to generate file objects, for example:
- *
- * RepoGroup::singleton()->getLocalRepo()->newFile($title);
- *
- * The convenience functions wfLocalFile() and wfFindFile() should be sufficient
- * in most cases.
- *
- * @ingroup FileRepo
- */
-class LocalFile extends File {
-       /**#@+
-        * @private
-        */
-       var
-               $fileExists,       # does the file exist on disk? (loadFromXxx)
-               $historyLine,      # Number of line to return by nextHistoryLine() (constructor)
-               $historyRes,       # result of the query for the file's history (nextHistoryLine)
-               $width,            # \
-               $height,           #  |
-               $bits,             #   --- returned by getimagesize (loadFromXxx)
-               $attr,             # /
-               $media_type,       # MEDIATYPE_xxx (bitmap, drawing, audio...)
-               $mime,             # MIME type, determined by MimeMagic::guessMimeType
-               $major_mime,       # Major mime type
-               $minor_mime,       # Minor mime type
-               $size,             # Size in bytes (loadFromXxx)
-               $metadata,         # Handler-specific metadata
-               $timestamp,        # Upload timestamp
-               $sha1,             # SHA-1 base 36 content hash
-               $user, $user_text, # User, who uploaded the file
-               $description,      # Description of current revision of the file
-               $dataLoaded,       # Whether or not all this has been loaded from the database (loadFromXxx)
-               $upgraded,         # Whether the row was upgraded on load
-               $locked,           # True if the image row is locked
-               $missing,          # True if file is not present in file system. Not to be cached in memcached
-               $deleted;          # Bitfield akin to rev_deleted
-
-       /**#@-*/
-
-       protected $repoClass = 'LocalRepo';
-
-       /**
-        * Create a LocalFile from a title
-        * Do not call this except from inside a repo class.
-        *
-        * Note: $unused param is only here to avoid an E_STRICT
-        *
-        * @param $title
-        * @param $repo
-        * @param $unused
-        *
-        * @return LocalFile
-        */
-       static function newFromTitle( $title, $repo, $unused = null ) {
-               return new self( $title, $repo );
-       }
-
-       /**
-        * Create a LocalFile from a title
-        * Do not call this except from inside a repo class.
-        *
-        * @param $row
-        * @param $repo
-        *
-        * @return LocalFile
-        */
-       static function newFromRow( $row, $repo ) {
-               $title = Title::makeTitle( NS_FILE, $row->img_name );
-               $file = new self( $title, $repo );
-               $file->loadFromRow( $row );
-
-               return $file;
-       }
-
-       /**
-        * Create a LocalFile from a SHA-1 key
-        * Do not call this except from inside a repo class.
-        *
-        * @param $sha1 string base-36 SHA-1
-        * @param $repo LocalRepo
-        * @param string|bool $timestamp MW_timestamp (optional)
-        *
-        * @return bool|LocalFile
-        */
-       static function newFromKey( $sha1, $repo, $timestamp = false ) {
-               $dbr = $repo->getSlaveDB();
-
-               $conds = array( 'img_sha1' => $sha1 );
-               if ( $timestamp ) {
-                       $conds['img_timestamp'] = $dbr->timestamp( $timestamp );
-               }
-
-               $row = $dbr->selectRow( 'image', self::selectFields(), $conds, __METHOD__ );
-               if ( $row ) {
-                       return self::newFromRow( $row, $repo );
-               } else {
-                       return false;
-               }
-       }
-
-       /**
-        * Fields in the image table
-        */
-       static function selectFields() {
-               return array(
-                       'img_name',
-                       'img_size',
-                       'img_width',
-                       'img_height',
-                       'img_metadata',
-                       'img_bits',
-                       'img_media_type',
-                       'img_major_mime',
-                       'img_minor_mime',
-                       'img_description',
-                       'img_user',
-                       'img_user_text',
-                       'img_timestamp',
-                       'img_sha1',
-               );
-       }
-
-       /**
-        * Constructor.
-        * Do not call this except from inside a repo class.
-        */
-       function __construct( $title, $repo ) {
-               parent::__construct( $title, $repo );
-
-               $this->metadata = '';
-               $this->historyLine = 0;
-               $this->historyRes = null;
-               $this->dataLoaded = false;
-
-               $this->assertRepoDefined();
-               $this->assertTitleDefined();
-       }
-
-       /**
-        * Get the memcached key for the main data for this file, or false if
-        * there is no access to the shared cache.
-        */
-       function getCacheKey() {
-               $hashedName = md5( $this->getName() );
-
-               return $this->repo->getSharedCacheKey( 'file', $hashedName );
-       }
-
-       /**
-        * Try to load file metadata from memcached. Returns true on success.
-        */
-       function loadFromCache() {
-               global $wgMemc;
-
-               wfProfileIn( __METHOD__ );
-               $this->dataLoaded = false;
-               $key = $this->getCacheKey();
-
-               if ( !$key ) {
-                       wfProfileOut( __METHOD__ );
-                       return false;
-               }
-
-               $cachedValues = $wgMemc->get( $key );
-
-               // Check if the key existed and belongs to this version of MediaWiki
-               if ( isset( $cachedValues['version'] ) && ( $cachedValues['version'] == MW_FILE_VERSION ) ) {
-                       wfDebug( "Pulling file metadata from cache key $key\n" );
-                       $this->fileExists = $cachedValues['fileExists'];
-                       if ( $this->fileExists ) {
-                               $this->setProps( $cachedValues );
-                       }
-                       $this->dataLoaded = true;
-               }
-
-               if ( $this->dataLoaded ) {
-                       wfIncrStats( 'image_cache_hit' );
-               } else {
-                       wfIncrStats( 'image_cache_miss' );
-               }
-
-               wfProfileOut( __METHOD__ );
-               return $this->dataLoaded;
-       }
-
-       /**
-        * Save the file metadata to memcached
-        */
-       function saveToCache() {
-               global $wgMemc;
-
-               $this->load();
-               $key = $this->getCacheKey();
-
-               if ( !$key ) {
-                       return;
-               }
-
-               $fields = $this->getCacheFields( '' );
-               $cache = array( 'version' => MW_FILE_VERSION );
-               $cache['fileExists'] = $this->fileExists;
-
-               if ( $this->fileExists ) {
-                       foreach ( $fields as $field ) {
-                               $cache[$field] = $this->$field;
-                       }
-               }
-
-               $wgMemc->set( $key, $cache, 60 * 60 * 24 * 7 ); // A week
-       }
-
-       /**
-        * Load metadata from the file itself
-        */
-       function loadFromFile() {
-               $this->setProps( self::getPropsFromPath( $this->getPath() ) );
-       }
-
-       function getCacheFields( $prefix = 'img_' ) {
-               static $fields = array( 'size', 'width', 'height', 'bits', 'media_type',
-                       'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1', 'user', 'user_text', 'description' );
-               static $results = array();
-
-               if ( $prefix == '' ) {
-                       return $fields;
-               }
-
-               if ( !isset( $results[$prefix] ) ) {
-                       $prefixedFields = array();
-                       foreach ( $fields as $field ) {
-                               $prefixedFields[] = $prefix . $field;
-                       }
-                       $results[$prefix] = $prefixedFields;
-               }
-
-               return $results[$prefix];
-       }
-
-       /**
-        * Load file metadata from the DB
-        */
-       function loadFromDB() {
-               # Polymorphic function name to distinguish foreign and local fetches
-               $fname = get_class( $this ) . '::' . __FUNCTION__;
-               wfProfileIn( $fname );
-
-               # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
-               $this->dataLoaded = true;
-
-               $dbr = $this->repo->getMasterDB();
-
-               $row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ),
-                       array( 'img_name' => $this->getName() ), $fname );
-
-               if ( $row ) {
-                       $this->loadFromRow( $row );
-               } else {
-                       $this->fileExists = false;
-               }
-
-               wfProfileOut( $fname );
-       }
-
-       /**
-        * Decode a row from the database (either object or array) to an array
-        * with timestamps and MIME types decoded, and the field prefix removed.
-        */
-       function decodeRow( $row, $prefix = 'img_' ) {
-               $array = (array)$row;
-               $prefixLength = strlen( $prefix );
-
-               // Sanity check prefix once
-               if ( substr( key( $array ), 0, $prefixLength ) !== $prefix ) {
-                       throw new MWException( __METHOD__ .  ': incorrect $prefix parameter' );
-               }
-
-               $decoded = array();
-
-               foreach ( $array as $name => $value ) {
-                       $decoded[substr( $name, $prefixLength )] = $value;
-               }
-
-               $decoded['timestamp'] = wfTimestamp( TS_MW, $decoded['timestamp'] );
-
-               if ( empty( $decoded['major_mime'] ) ) {
-                       $decoded['mime'] = 'unknown/unknown';
-               } else {
-                       if ( !$decoded['minor_mime'] ) {
-                               $decoded['minor_mime'] = 'unknown';
-                       }
-                       $decoded['mime'] = $decoded['major_mime'] . '/' . $decoded['minor_mime'];
-               }
-
-               # Trim zero padding from char/binary field
-               $decoded['sha1'] = rtrim( $decoded['sha1'], "\0" );
-
-               return $decoded;
-       }
-
-       /**
-        * Load file metadata from a DB result row
-        */
-       function loadFromRow( $row, $prefix = 'img_' ) {
-               $this->dataLoaded = true;
-               $array = $this->decodeRow( $row, $prefix );
-
-               foreach ( $array as $name => $value ) {
-                       $this->$name = $value;
-               }
-
-               $this->fileExists = true;
-               $this->maybeUpgradeRow();
-       }
-
-       /**
-        * Load file metadata from cache or DB, unless already loaded
-        */
-       function load() {
-               if ( !$this->dataLoaded ) {
-                       if ( !$this->loadFromCache() ) {
-                               $this->loadFromDB();
-                               $this->saveToCache();
-                       }
-                       $this->dataLoaded = true;
-               }
-       }
-
-       /**
-        * Upgrade a row if it needs it
-        */
-       function maybeUpgradeRow() {
-               global $wgUpdateCompatibleMetadata;
-               if ( wfReadOnly() ) {
-                       return;
-               }
-
-               if ( is_null( $this->media_type ) ||
-                       $this->mime == 'image/svg'
-               ) {
-                       $this->upgradeRow();
-                       $this->upgraded = true;
-               } else {
-                       $handler = $this->getHandler();
-                       if ( $handler ) {
-                               $validity = $handler->isMetadataValid( $this, $this->metadata );
-                               if ( $validity === MediaHandler::METADATA_BAD
-                                       || ( $validity === MediaHandler::METADATA_COMPATIBLE && $wgUpdateCompatibleMetadata )
-                               ) {
-                                       $this->upgradeRow();
-                                       $this->upgraded = true;
-                               }
-                       }
-               }
-       }
-
-       function getUpgraded() {
-               return $this->upgraded;
-       }
-
-       /**
-        * Fix assorted version-related problems with the image row by reloading it from the file
-        */
-       function upgradeRow() {
-               wfProfileIn( __METHOD__ );
-
-               $this->loadFromFile();
-
-               # Don't destroy file info of missing files
-               if ( !$this->fileExists ) {
-                       wfDebug( __METHOD__ . ": file does not exist, aborting\n" );
-                       wfProfileOut( __METHOD__ );
-                       return;
-               }
-
-               $dbw = $this->repo->getMasterDB();
-               list( $major, $minor ) = self::splitMime( $this->mime );
-
-               if ( wfReadOnly() ) {
-                       wfProfileOut( __METHOD__ );
-                       return;
-               }
-               wfDebug( __METHOD__ . ': upgrading ' . $this->getName() . " to the current schema\n" );
-
-               $dbw->update( 'image',
-                       array(
-                               'img_width' => $this->width,
-                               'img_height' => $this->height,
-                               'img_bits' => $this->bits,
-                               'img_media_type' => $this->media_type,
-                               'img_major_mime' => $major,
-                               'img_minor_mime' => $minor,
-                               'img_metadata' => $this->metadata,
-                               'img_sha1' => $this->sha1,
-                       ), array( 'img_name' => $this->getName() ),
-                       __METHOD__
-               );
-
-               $this->saveToCache();
-               wfProfileOut( __METHOD__ );
-       }
-
-       /**
-        * Set properties in this object to be equal to those given in the
-        * associative array $info. Only cacheable fields can be set.
-        *
-        * If 'mime' is given, it will be split into major_mime/minor_mime.
-        * If major_mime/minor_mime are given, $this->mime will also be set.
-        */
-       function setProps( $info ) {
-               $this->dataLoaded = true;
-               $fields = $this->getCacheFields( '' );
-               $fields[] = 'fileExists';
-
-               foreach ( $fields as $field ) {
-                       if ( isset( $info[$field] ) ) {
-                               $this->$field = $info[$field];
-                       }
-               }
-
-               // Fix up mime fields
-               if ( isset( $info['major_mime'] ) ) {
-                       $this->mime = "{$info['major_mime']}/{$info['minor_mime']}";
-               } elseif ( isset( $info['mime'] ) ) {
-                       $this->mime = $info['mime'];
-                       list( $this->major_mime, $this->minor_mime ) = self::splitMime( $this->mime );
-               }
-       }
-
-       /** splitMime inherited */
-       /** getName inherited */
-       /** getTitle inherited */
-       /** getURL inherited */
-       /** getViewURL inherited */
-       /** getPath inherited */
-       /** isVisible inhereted */
-
-       function isMissing() {
-               if ( $this->missing === null ) {
-                       list( $fileExists ) = $this->repo->fileExistsBatch( array( $this->getVirtualUrl() ), FileRepo::FILES_ONLY );
-                       $this->missing = !$fileExists;
-               }
-               return $this->missing;
-       }
-
-       /**
-        * Return the width of the image
-        *
-        * Returns false on error
-        */
-       public function getWidth( $page = 1 ) {
-               $this->load();
-
-               if ( $this->isMultipage() ) {
-                       $dim = $this->getHandler()->getPageDimensions( $this, $page );
-                       if ( $dim ) {
-                               return $dim['width'];
-                       } else {
-                               return false;
-                       }
-               } else {
-                       return $this->width;
-               }
-       }
-
-       /**
-        * Return the height of the image
-        *
-        * Returns false on error
-        */
-       public function getHeight( $page = 1 ) {
-               $this->load();
-
-               if ( $this->isMultipage() ) {
-                       $dim = $this->getHandler()->getPageDimensions( $this, $page );
-                       if ( $dim ) {
-                               return $dim['height'];
-                       } else {
-                               return false;
-                       }
-               } else {
-                       return $this->height;
-               }
-       }
-
-       /**
-        * Returns ID or name of user who uploaded the file
-        *
-        * @param $type string 'text' or 'id'
-        */
-       function getUser( $type = 'text' ) {
-               $this->load();
-
-               if ( $type == 'text' ) {
-                       return $this->user_text;
-               } elseif ( $type == 'id' ) {
-                       return $this->user;
-               }
-       }
-
-       /**
-        * Get handler-specific metadata
-        */
-       function getMetadata() {
-               $this->load();
-               return $this->metadata;
-       }
-
-       function getBitDepth() {
-               $this->load();
-               return $this->bits;
-       }
-
-       /**
-        * Return the size of the image file, in bytes
-        */
-       public function getSize() {
-               $this->load();
-               return $this->size;
-       }
-
-       /**
-        * Returns the mime type of the file.
-        */
-       function getMimeType() {
-               $this->load();
-               return $this->mime;
-       }
-
-       /**
-        * Return the type of the media in the file.
-        * Use the value returned by this function with the MEDIATYPE_xxx constants.
-        */
-       function getMediaType() {
-               $this->load();
-               return $this->media_type;
-       }
-
-       /** canRender inherited */
-       /** mustRender inherited */
-       /** allowInlineDisplay inherited */
-       /** isSafeFile inherited */
-       /** isTrustedFile inherited */
-
-       /**
-        * Returns true if the file exists on disk.
-        * @return boolean Whether file exist on disk.
-        */
-       public function exists() {
-               $this->load();
-               return $this->fileExists;
-       }
-
-       /** getTransformScript inherited */
-       /** getUnscaledThumb inherited */
-       /** thumbName inherited */
-       /** createThumb inherited */
-       /** transform inherited */
-
-       /**
-        * Fix thumbnail files from 1.4 or before, with extreme prejudice
-        */
-       function migrateThumbFile( $thumbName ) {
-               $thumbDir = $this->getThumbPath();
-               $thumbPath = "$thumbDir/$thumbName";
-
-               if ( is_dir( $thumbPath ) ) {
-                       // Directory where file should be
-                       // This happened occasionally due to broken migration code in 1.5
-                       // Rename to broken-*
-                       for ( $i = 0; $i < 100 ; $i++ ) {
-                               $broken = $this->repo->getZonePath( 'public' ) . "/broken-$i-$thumbName";
-                               if ( !file_exists( $broken ) ) {
-                                       rename( $thumbPath, $broken );
-                                       break;
-                               }
-                       }
-                       // Doesn't exist anymore
-                       clearstatcache();
-               }
-
-               if ( is_file( $thumbDir ) ) {
-                       // File where directory should be
-                       unlink( $thumbDir );
-                       // Doesn't exist anymore
-                       clearstatcache();
-               }
-       }
-
-       /** getHandler inherited */
-       /** iconThumb inherited */
-       /** getLastError inherited */
-
-       /**
-        * Get all thumbnail names previously generated for this file
-        * @param $archiveName string|false Name of an archive file
-        * @return array first element is the base dir, then files in that base dir.
-        */
-       function getThumbnails( $archiveName = false ) {
-               $this->load();
-
-               if ( $archiveName ) {
-                       $dir = $this->getArchiveThumbPath( $archiveName );
-               } else {
-                       $dir = $this->getThumbPath();
-               }
-               $files = array();
-               $files[] = $dir;
-
-               if ( is_dir( $dir ) ) {
-                       $handle = opendir( $dir );
-
-                       if ( $handle ) {
-                               while ( false !== ( $file = readdir( $handle ) ) ) {
-                                       if ( $file { 0 } != '.' ) {
-                                               $files[] = $file;
-                                       }
-                               }
-
-                               closedir( $handle );
-                       }
-               }
-
-               return $files;
-       }
-
-       /**
-        * Refresh metadata in memcached, but don't touch thumbnails or squid
-        */
-       function purgeMetadataCache() {
-               $this->loadFromDB();
-               $this->saveToCache();
-               $this->purgeHistory();
-       }
-
-       /**
-        * Purge the shared history (OldLocalFile) cache
-        */
-       function purgeHistory() {
-               global $wgMemc;
-
-               $hashedName = md5( $this->getName() );
-               $oldKey = $this->repo->getSharedCacheKey( 'oldfile', $hashedName );
-
-               // Must purge thumbnails for old versions too! bug 30192
-               foreach( $this->getHistory() as $oldFile ) {
-                       $oldFile->purgeThumbnails();
-               }
-
-               if ( $oldKey ) {
-                       $wgMemc->delete( $oldKey );
-               }
-       }
-
-       /**
-        * Delete all previously generated thumbnails, refresh metadata in memcached and purge the squid
-        */
-       function purgeCache() {
-               // Refresh metadata cache
-               $this->purgeMetadataCache();
-
-               // Delete thumbnails
-               $this->purgeThumbnails();
-
-               // Purge squid cache for this file
-               SquidUpdate::purge( array( $this->getURL() ) );
-       }
-
-       /**
-        * Delete cached transformed files for an archived version only.
-        * @param $archiveName string name of the archived file
-        */
-       function purgeOldThumbnails( $archiveName ) {
-               global $wgUseSquid;
-               // Get a list of old thumbnails and URLs
-               $files = $this->getThumbnails( $archiveName );
-               $dir = array_shift( $files );
-               $this->purgeThumbList( $dir, $files );
-
-               // Directory should be empty, delete it too. This will probably suck on
-               // something like NFS or if the directory isn't actually empty, so hide
-               // the warnings :D
-               wfSuppressWarnings();
-               if( !rmdir( $dir ) ) {
-                       wfDebug( __METHOD__ . ": unable to remove archive directory: $dir\n" );
-               }
-               wfRestoreWarnings();
-
-               // Purge any custom thumbnail caches
-               wfRunHooks( 'LocalFilePurgeThumbnails', array( $this, $archiveName ) );
-
-               // Purge the squid
-               if ( $wgUseSquid ) {
-                       $urls = array();
-                       foreach( $files as $file ) {
-                               $urls[] = $this->getArchiveThumbUrl( $archiveName, $file );
-                       }
-                       SquidUpdate::purge( $urls );
-               }
-       }
-
-
-       /**
-        * Delete cached transformed files for the current version only.
-        */
-       function purgeThumbnails() {
-               global $wgUseSquid;
-
-               // Delete thumbnails
-               $files = $this->getThumbnails();
-               $dir = array_shift( $files );
-               $this->purgeThumbList( $dir, $files );
-
-               // Purge any custom thumbnail caches
-               wfRunHooks( 'LocalFilePurgeThumbnails', array( $this, false ) );
-
-               // Purge the squid
-               if ( $wgUseSquid ) {
-                       $urls = array();
-                       foreach( $files as $file ) {
-                               $urls[] = $this->getThumbUrl( $file );
-                       }
-                       SquidUpdate::purge( $urls );
-               }
-       }
-
-       /**
-        * Delete a list of thumbnails visible at urls
-        * @param $dir string base dir of the files.
-        * @param $files array of strings: relative filenames (to $dir)
-        */
-       protected function purgeThumbList($dir, $files) {
-               wfDebug( __METHOD__ . ": " . var_export( $files, true ) . "\n" );
-               foreach ( $files as $file ) {
-                       # Check that the base file name is part of the thumb name
-                       # This is a basic sanity check to avoid erasing unrelated directories
-                       if ( strpos( $file, $this->getName() ) !== false ) {
-                               wfSuppressWarnings();
-                               unlink( "$dir/$file" );
-                               wfRestoreWarnings();
-                       }
-               }
-       }
-
-       /** purgeDescription inherited */
-       /** purgeEverything inherited */
-
-       function getHistory( $limit = null, $start = null, $end = null, $inc = true ) {
-               $dbr = $this->repo->getSlaveDB();
-               $tables = array( 'oldimage' );
-               $fields = OldLocalFile::selectFields();
-               $conds = $opts = $join_conds = array();
-               $eq = $inc ? '=' : '';
-               $conds[] = "oi_name = " . $dbr->addQuotes( $this->title->getDBkey() );
-
-               if ( $start ) {
-                       $conds[] = "oi_timestamp <$eq " . $dbr->addQuotes( $dbr->timestamp( $start ) );
-               }
-
-               if ( $end ) {
-                       $conds[] = "oi_timestamp >$eq " . $dbr->addQuotes( $dbr->timestamp( $end ) );
-               }
-
-               if ( $limit ) {
-                       $opts['LIMIT'] = $limit;
-               }
-
-               // Search backwards for time > x queries
-               $order = ( !$start && $end !== null ) ? 'ASC' : 'DESC';
-               $opts['ORDER BY'] = "oi_timestamp $order";
-               $opts['USE INDEX'] = array( 'oldimage' => 'oi_name_timestamp' );
-
-               wfRunHooks( 'LocalFile::getHistory', array( &$this, &$tables, &$fields,
-                       &$conds, &$opts, &$join_conds ) );
-
-               $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $opts, $join_conds );
-               $r = array();
-
-               foreach ( $res as $row ) {
-                       if ( $this->repo->oldFileFromRowFactory ) {
-                               $r[] = call_user_func( $this->repo->oldFileFromRowFactory, $row, $this->repo );
-                       } else {
-                               $r[] = OldLocalFile::newFromRow( $row, $this->repo );
-                       }
-               }
-
-               if ( $order == 'ASC' ) {
-                       $r = array_reverse( $r ); // make sure it ends up descending
-               }
-
-               return $r;
-       }
-
-       /**
-        * Return the history of this file, line by line.
-        * starts with current version, then old versions.
-        * uses $this->historyLine to check which line to return:
-        *  0      return line for current version
-        *  1      query for old versions, return first one
-        *  2, ... return next old version from above query
-        */
-       public function nextHistoryLine() {
-               # Polymorphic function name to distinguish foreign and local fetches
-               $fname = get_class( $this ) . '::' . __FUNCTION__;
-
-               $dbr = $this->repo->getSlaveDB();
-
-               if ( $this->historyLine == 0 ) {// called for the first time, return line from cur
-                       $this->historyRes = $dbr->select( 'image',
-                               array(
-                                       '*',
-                                       "'' AS oi_archive_name",
-                                       '0 as oi_deleted',
-                                       'img_sha1'
-                               ),
-                               array( 'img_name' => $this->title->getDBkey() ),
-                               $fname
-                       );
-
-                       if ( 0 == $dbr->numRows( $this->historyRes ) ) {
-                               $this->historyRes = null;
-                               return false;
-                       }
-               } elseif ( $this->historyLine == 1 ) {
-                       $this->historyRes = $dbr->select( 'oldimage', '*',
-                               array( 'oi_name' => $this->title->getDBkey() ),
-                               $fname,
-                               array( 'ORDER BY' => 'oi_timestamp DESC' )
-                       );
-               }
-               $this->historyLine ++;
-
-               return $dbr->fetchObject( $this->historyRes );
-       }
-
-       /**
-        * Reset the history pointer to the first element of the history
-        */
-       public function resetHistory() {
-               $this->historyLine = 0;
-
-               if ( !is_null( $this->historyRes ) ) {
-                       $this->historyRes = null;
-               }
-       }
-
-       /** getHashPath inherited */
-       /** getRel inherited */
-       /** getUrlRel inherited */
-       /** getArchiveRel inherited */
-       /** getArchivePath inherited */
-       /** getThumbPath inherited */
-       /** getArchiveUrl inherited */
-       /** getThumbUrl inherited */
-       /** getArchiveVirtualUrl inherited */
-       /** getThumbVirtualUrl inherited */
-       /** isHashed inherited */
-
-       /**
-        * Upload a file and record it in the DB
-        * @param $srcPath String: source path or virtual URL
-        * @param $comment String: upload description
-        * @param $pageText String: text to use for the new description page,
-        *                  if a new description page is created
-        * @param $flags Integer: flags for publish()
-        * @param $props Array: File properties, if known. This can be used to reduce the
-        *               upload time when uploading virtual URLs for which the file info
-        *               is already known
-        * @param $timestamp String: timestamp for img_timestamp, or false to use the current time
-        * @param $user Mixed: User object or null to use $wgUser
-        *
-        * @return FileRepoStatus object. On success, the value member contains the
-        *     archive name, or an empty string if it was a new file.
-        */
-       function upload( $srcPath, $comment, $pageText, $flags = 0, $props = false, $timestamp = false, $user = null ) {
-               global $wgContLang;
-               // truncate nicely or the DB will do it for us
-               // non-nicely (dangling multi-byte chars, non-truncated
-               // version in cache).
-               $comment = $wgContLang->truncate( $comment, 255 );
-               $this->lock();
-               $status = $this->publish( $srcPath, $flags );
-
-               if ( $status->ok ) {
-                       if ( !$this->recordUpload2( $status->value, $comment, $pageText, $props, $timestamp, $user ) ) {
-                               $status->fatal( 'filenotfound', $srcPath );
-                       }
-               }
-
-               $this->unlock();
-
-               return $status;
-       }
-
-       /**
-        * Record a file upload in the upload log and the image table
-        */
-       function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
-               $watch = false, $timestamp = false )
-       {
-               $pageText = SpecialUpload::getInitialPageText( $desc, $license, $copyStatus, $source );
-
-               if ( !$this->recordUpload2( $oldver, $desc, $pageText ) ) {
-                       return false;
-               }
-
-               if ( $watch ) {
-                       global $wgUser;
-                       $wgUser->addWatch( $this->getTitle() );
-               }
-               return true;
-       }
-
-       /**
-        * Record a file upload in the upload log and the image table
-        */
-       function recordUpload2(
-               $oldver, $comment, $pageText, $props = false, $timestamp = false, $user = null
-       ) {
-               if ( is_null( $user ) ) {
-                       global $wgUser;
-                       $user = $wgUser;
-               }
-
-               $dbw = $this->repo->getMasterDB();
-               $dbw->begin();
-
-               if ( !$props ) {
-                       $props = $this->repo->getFileProps( $this->getVirtualUrl() );
-               }
-
-               if ( $timestamp === false ) {
-                       $timestamp = $dbw->timestamp();
-               }
-
-               $props['description'] = $comment;
-               $props['user'] = $user->getId();
-               $props['user_text'] = $user->getName();
-               $props['timestamp'] = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
-               $this->setProps( $props );
-
-               # Delete thumbnails
-               $this->purgeThumbnails();
-
-               # The file is already on its final location, remove it from the squid cache
-               SquidUpdate::purge( array( $this->getURL() ) );
-
-               # Fail now if the file isn't there
-               if ( !$this->fileExists ) {
-                       wfDebug( __METHOD__ . ": File " . $this->getRel() . " went missing!\n" );
-                       return false;
-               }
-
-               $reupload = false;
-
-               # Test to see if the row exists using INSERT IGNORE
-               # This avoids race conditions by locking the row until the commit, and also
-               # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
-               $dbw->insert( 'image',
-                       array(
-                               'img_name' => $this->getName(),
-                               'img_size' => $this->size,
-                               'img_width' => intval( $this->width ),
-                               'img_height' => intval( $this->height ),
-                               'img_bits' => $this->bits,
-                               'img_media_type' => $this->media_type,
-                               'img_major_mime' => $this->major_mime,
-                               'img_minor_mime' => $this->minor_mime,
-                               'img_timestamp' => $timestamp,
-                               'img_description' => $comment,
-                               'img_user' => $user->getId(),
-                               'img_user_text' => $user->getName(),
-                               'img_metadata' => $this->metadata,
-                               'img_sha1' => $this->sha1
-                       ),
-                       __METHOD__,
-                       'IGNORE'
-               );
-
-               if ( $dbw->affectedRows() == 0 ) {
-                       $reupload = true;
-
-                       # Collision, this is an update of a file
-                       # Insert previous contents into oldimage
-                       $dbw->insertSelect( 'oldimage', 'image',
-                               array(
-                                       'oi_name' => 'img_name',
-                                       'oi_archive_name' => $dbw->addQuotes( $oldver ),
-                                       'oi_size' => 'img_size',
-                                       'oi_width' => 'img_width',
-                                       'oi_height' => 'img_height',
-                                       'oi_bits' => 'img_bits',
-                                       'oi_timestamp' => 'img_timestamp',
-                                       'oi_description' => 'img_description',
-                                       'oi_user' => 'img_user',
-                                       'oi_user_text' => 'img_user_text',
-                                       'oi_metadata' => 'img_metadata',
-                                       'oi_media_type' => 'img_media_type',
-                                       'oi_major_mime' => 'img_major_mime',
-                                       'oi_minor_mime' => 'img_minor_mime',
-                                       'oi_sha1' => 'img_sha1'
-                               ), array( 'img_name' => $this->getName() ), __METHOD__
-                       );
-
-                       # Update the current image row
-                       $dbw->update( 'image',
-                               array( /* SET */
-                                       'img_size' => $this->size,
-                                       'img_width' => intval( $this->width ),
-                                       'img_height' => intval( $this->height ),
-                                       'img_bits' => $this->bits,
-                                       'img_media_type' => $this->media_type,
-                                       'img_major_mime' => $this->major_mime,
-                                       'img_minor_mime' => $this->minor_mime,
-                                       'img_timestamp' => $timestamp,
-                                       'img_description' => $comment,
-                                       'img_user' => $user->getId(),
-                                       'img_user_text' => $user->getName(),
-                                       'img_metadata' => $this->metadata,
-                                       'img_sha1' => $this->sha1
-                               ), array( /* WHERE */
-                                       'img_name' => $this->getName()
-                               ), __METHOD__
-                       );
-               } else {
-                       # This is a new file
-                       # Update the image count
-                       $dbw->begin( __METHOD__ );
-                       $dbw->update(
-                               'site_stats',
-                               array( 'ss_images = ss_images+1' ),
-                               '*',
-                               __METHOD__
-                       );
-                       $dbw->commit( __METHOD__ );
-               }
-
-               $descTitle = $this->getTitle();
-               $wikiPage = new WikiFilePage( $descTitle );
-               $wikiPage->setFile( $this );
-
-               # Add the log entry
-               $log = new LogPage( 'upload' );
-               $action = $reupload ? 'overwrite' : 'upload';
-               $log->addEntry( $action, $descTitle, $comment, array(), $user );
-
-               if ( $descTitle->exists() ) {
-                       # Create a null revision
-                       $latest = $descTitle->getLatestRevID();
-                       $nullRevision = Revision::newNullRevision(
-                               $dbw,
-                               $descTitle->getArticleId(),
-                               $log->getRcComment(),
-                               false
-                       );
-                       if (!is_null($nullRevision)) {
-                               $nullRevision->insertOn( $dbw );
-
-                               wfRunHooks( 'NewRevisionFromEditComplete', array( $wikiPage, $nullRevision, $latest, $user ) );
-                               $wikiPage->updateRevisionOn( $dbw, $nullRevision );
-                       }
-                       # Invalidate the cache for the description page
-                       $descTitle->invalidateCache();
-                       $descTitle->purgeSquid();
-               } else {
-                       # New file; create the description page.
-                       # There's already a log entry, so don't make a second RC entry
-                       # Squid and file cache for the description page are purged by doEdit.
-                       $wikiPage->doEdit( $pageText, $comment, EDIT_NEW | EDIT_SUPPRESS_RC );
-               }
-
-               # Commit the transaction now, in case something goes wrong later
-               # The most important thing is that files don't get lost, especially archives
-               $dbw->commit();
-
-               # Save to cache and purge the squid
-               # We shall not saveToCache before the commit since otherwise
-               # in case of a rollback there is an usable file from memcached
-               # which in fact doesn't really exist (bug 24978)
-               $this->saveToCache();
-
-               # Hooks, hooks, the magic of hooks...
-               wfRunHooks( 'FileUpload', array( $this, $reupload, $descTitle->exists() ) );
-
-               # Invalidate cache for all pages using this file
-               $update = new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' );
-               $update->doUpdate();
-
-               # Invalidate cache for all pages that redirects on this page
-               $redirs = $this->getTitle()->getRedirectsHere();
-
-               foreach ( $redirs as $redir ) {
-                       $update = new HTMLCacheUpdate( $redir, 'imagelinks' );
-                       $update->doUpdate();
-               }
-
-               return true;
-       }
-
-       /**
-        * Move or copy a file to its public location. If a file exists at the
-        * 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 $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 FileRepoStatus object. On success, the value member contains the
-        *     archive name, or an empty string if it was a new file.
-        */
-       function publish( $srcPath, $flags = 0 ) {
-               return $this->publishTo( $srcPath, $this->getRel(), $flags );
-       }
-
-       /**
-        * Move or copy a file to a specified location. 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 $srcPath String: local filesystem path to the source image
-        * @param $dstRel String: target relative path
-        * @param $flags Integer: a bitwise combination of:
-        *     File::DELETE_SOURCE      Delete the source file, i.e. move rather than copy
-        * @return FileRepoStatus object. On success, the value member contains the
-        *     archive name, or an empty string if it was a new file.
-        */
-       function publishTo( $srcPath, $dstRel, $flags = 0 ) {
-               $this->lock();
-
-               $archiveName = wfTimestamp( TS_MW ) . '!'. $this->getName();
-               $archiveRel = 'archive/' . $this->getHashPath() . $archiveName;
-               $flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0;
-               $status = $this->repo->publish( $srcPath, $dstRel, $archiveRel, $flags );
-
-               if ( $status->value == 'new' ) {
-                       $status->value = '';
-               } else {
-                       $status->value = $archiveName;
-               }
-
-               $this->unlock();
-
-               return $status;
-       }
-
-       /** getLinksTo inherited */
-       /** getExifData inherited */
-       /** isLocal inherited */
-       /** wasDeleted inherited */
-
-       /**
-        * Move file to the new title
-        *
-        * Move current, old version and all thumbnails
-        * to the new filename. Old file is deleted.
-        *
-        * Cache purging is done; checks for validity
-        * and logging are caller's responsibility
-        *
-        * @param $target Title New file name
-        * @return FileRepoStatus object.
-        */
-       function move( $target ) {
-               wfDebugLog( 'imagemove', "Got request to move {$this->name} to " . $target->getText() );
-               $this->lock();
-
-               $batch = new LocalFileMoveBatch( $this, $target );
-               $batch->addCurrent();
-               $batch->addOlds();
-
-               $status = $batch->execute();
-               wfDebugLog( 'imagemove', "Finished moving {$this->name}" );
-
-               $this->purgeEverything();
-               $this->unlock();
-
-               if ( $status->isOk() ) {
-                       // Now switch the object
-                       $this->title = $target;
-                       // Force regeneration of the name and hashpath
-                       unset( $this->name );
-                       unset( $this->hashPath );
-                       // Purge the new image
-                       $this->purgeEverything();
-               }
-
-               return $status;
-       }
-
-       /**
-        * Delete all versions of the file.
-        *
-        * Moves the files into an archive directory (or deletes them)
-        * and removes the database rows.
-        *
-        * Cache purging is done; logging is caller's responsibility.
-        *
-        * @param $reason
-        * @param $suppress
-        * @return FileRepoStatus object.
-        */
-       function delete( $reason, $suppress = false ) {
-               $this->lock();
-
-               $batch = new LocalFileDeleteBatch( $this, $reason, $suppress );
-               $batch->addCurrent();
-
-               # Get old version relative paths
-               $dbw = $this->repo->getMasterDB();
-               $result = $dbw->select( 'oldimage',
-                       array( 'oi_archive_name' ),
-                       array( 'oi_name' => $this->getName() ) );
-               foreach ( $result as $row ) {
-                       $batch->addOld( $row->oi_archive_name );
-                       $this->purgeOldThumbnails( $row->oi_archive_name );
-               }
-               $status = $batch->execute();
-
-               if ( $status->ok ) {
-                       // Update site_stats
-                       $site_stats = $dbw->tableName( 'site_stats' );
-                       $dbw->query( "UPDATE $site_stats SET ss_images=ss_images-1", __METHOD__ );
-                       $this->purgeEverything();
-               }
-
-               $this->unlock();
-
-               return $status;
-       }
-
-       /**
-        * Delete an old version of the file.
-        *
-        * Moves the file into an archive directory (or deletes it)
-        * and removes the database row.
-        *
-        * Cache purging is done; logging is caller's responsibility.
-        *
-        * @param $archiveName String
-        * @param $reason String
-        * @param $suppress Boolean
-        * @throws MWException or FSException on database or file store failure
-        * @return FileRepoStatus object.
-        */
-       function deleteOld( $archiveName, $reason, $suppress = false ) {
-               $this->lock();
-
-               $batch = new LocalFileDeleteBatch( $this, $reason, $suppress );
-               $batch->addOld( $archiveName );
-               $this->purgeOldThumbnails( $archiveName );
-               $status = $batch->execute();
-
-               $this->unlock();
-
-               if ( $status->ok ) {
-                       $this->purgeDescription();
-                       $this->purgeHistory();
-               }
-
-               return $status;
-       }
-
-       /**
-        * Restore all or specified deleted revisions to the given file.
-        * Permissions and logging are left to the caller.
-        *
-        * May throw database exceptions on error.
-        *
-        * @param $versions set of record ids of deleted items to restore,
-        *                    or empty to restore all revisions.
-        * @param $unsuppress Boolean
-        * @return FileRepoStatus
-        */
-       function restore( $versions = array(), $unsuppress = false ) {
-               $batch = new LocalFileRestoreBatch( $this, $unsuppress );
-
-               if ( !$versions ) {
-                       $batch->addAll();
-               } else {
-                       $batch->addIds( $versions );
-               }
-
-               $status = $batch->execute();
-
-               if ( !$status->isGood() ) {
-                       return $status;
-               }
-
-               $cleanupStatus = $batch->cleanup();
-               $cleanupStatus->successCount = 0;
-               $cleanupStatus->failCount = 0;
-               $status->merge( $cleanupStatus );
-
-               return $status;
-       }
-
-       /** isMultipage inherited */
-       /** pageCount inherited */
-       /** scaleHeight inherited */
-       /** getImageSize inherited */
-
-       /**
-        * Get the URL of the file description page.
-        */
-       function getDescriptionUrl() {
-               return $this->title->getLocalUrl();
-       }
-
-       /**
-        * Get the HTML text of the description page
-        * This is not used by ImagePage for local files, since (among other things)
-        * it skips the parser cache.
-        */
-       function getDescriptionText() {
-               global $wgParser;
-               $revision = Revision::newFromTitle( $this->title );
-               if ( !$revision ) return false;
-               $text = $revision->getText();
-               if ( !$text ) return false;
-               $pout = $wgParser->parse( $text, $this->title, new ParserOptions() );
-               return $pout->getText();
-       }
-
-       function getDescription() {
-               $this->load();
-               return $this->description;
-       }
-
-       function getTimestamp() {
-               $this->load();
-               return $this->timestamp;
-       }
-
-       function getSha1() {
-               $this->load();
-               // Initialise now if necessary
-               if ( $this->sha1 == '' && $this->fileExists ) {
-                       $this->sha1 = File::sha1Base36( $this->getPath() );
-                       if ( !wfReadOnly() && strval( $this->sha1 ) != '' ) {
-                               $dbw = $this->repo->getMasterDB();
-                               $dbw->update( 'image',
-                                       array( 'img_sha1' => $this->sha1 ),
-                                       array( 'img_name' => $this->getName() ),
-                                       __METHOD__ );
-                               $this->saveToCache();
-                       }
-               }
-
-               return $this->sha1;
-       }
-
-       /**
-        * Start a transaction and lock the image for update
-        * Increments a reference counter if the lock is already held
-        * @return boolean True if the image exists, false otherwise
-        */
-       function lock() {
-               $dbw = $this->repo->getMasterDB();
-
-               if ( !$this->locked ) {
-                       $dbw->begin();
-                       $this->locked++;
-               }
-
-               return $dbw->selectField( 'image', '1', array( 'img_name' => $this->getName() ), __METHOD__ );
-       }
-
-       /**
-        * Decrement the lock reference count. If the reference count is reduced to zero, commits
-        * the transaction and thereby releases the image lock.
-        */
-       function unlock() {
-               if ( $this->locked ) {
-                       --$this->locked;
-                       if ( !$this->locked ) {
-                               $dbw = $this->repo->getMasterDB();
-                               $dbw->commit();
-                       }
-               }
-       }
-
-       /**
-        * Roll back the DB transaction and mark the image unlocked
-        */
-       function unlockAndRollback() {
-               $this->locked = false;
-               $dbw = $this->repo->getMasterDB();
-               $dbw->rollback();
-       }
-} // LocalFile class
-
-# ------------------------------------------------------------------------------
-
-/**
- * Helper class for file deletion
- * @ingroup FileRepo
- */
-class LocalFileDeleteBatch {
-
-       /**
-        * @var LocalFile
-        */
-       var $file;
-
-       var $reason, $srcRels = array(), $archiveUrls = array(), $deletionBatch, $suppress;
-       var $status;
-
-       function __construct( File $file, $reason = '', $suppress = false ) {
-               $this->file = $file;
-               $this->reason = $reason;
-               $this->suppress = $suppress;
-               $this->status = $file->repo->newGood();
-       }
-
-       function addCurrent() {
-               $this->srcRels['.'] = $this->file->getRel();
-       }
-
-       function addOld( $oldName ) {
-               $this->srcRels[$oldName] = $this->file->getArchiveRel( $oldName );
-               $this->archiveUrls[] = $this->file->getArchiveUrl( $oldName );
-       }
-
-       function getOldRels() {
-               if ( !isset( $this->srcRels['.'] ) ) {
-                       $oldRels =& $this->srcRels;
-                       $deleteCurrent = false;
-               } else {
-                       $oldRels = $this->srcRels;
-                       unset( $oldRels['.'] );
-                       $deleteCurrent = true;
-               }
-
-               return array( $oldRels, $deleteCurrent );
-       }
-
-       protected function getHashes() {
-               $hashes = array();
-               list( $oldRels, $deleteCurrent ) = $this->getOldRels();
-
-               if ( $deleteCurrent ) {
-                       $hashes['.'] = $this->file->getSha1();
-               }
-
-               if ( count( $oldRels ) ) {
-                       $dbw = $this->file->repo->getMasterDB();
-                       $res = $dbw->select(
-                               'oldimage',
-                               array( 'oi_archive_name', 'oi_sha1' ),
-                               'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')',
-                               __METHOD__
-                       );
-
-                       foreach ( $res as $row ) {
-                               if ( rtrim( $row->oi_sha1, "\0" ) === '' ) {
-                                       // Get the hash from the file
-                                       $oldUrl = $this->file->getArchiveVirtualUrl( $row->oi_archive_name );
-                                       $props = $this->file->repo->getFileProps( $oldUrl );
-
-                                       if ( $props['fileExists'] ) {
-                                               // Upgrade the oldimage row
-                                               $dbw->update( 'oldimage',
-                                                       array( 'oi_sha1' => $props['sha1'] ),
-                                                       array( 'oi_name' => $this->file->getName(), 'oi_archive_name' => $row->oi_archive_name ),
-                                                       __METHOD__ );
-                                               $hashes[$row->oi_archive_name] = $props['sha1'];
-                                       } else {
-                                               $hashes[$row->oi_archive_name] = false;
-                                       }
-                               } else {
-                                       $hashes[$row->oi_archive_name] = $row->oi_sha1;
-                               }
-                       }
-               }
-
-               $missing = array_diff_key( $this->srcRels, $hashes );
-
-               foreach ( $missing as $name => $rel ) {
-                       $this->status->error( 'filedelete-old-unregistered', $name );
-               }
-
-               foreach ( $hashes as $name => $hash ) {
-                       if ( !$hash ) {
-                               $this->status->error( 'filedelete-missing', $this->srcRels[$name] );
-                               unset( $hashes[$name] );
-                       }
-               }
-
-               return $hashes;
-       }
-
-       function doDBInserts() {
-               global $wgUser;
-
-               $dbw = $this->file->repo->getMasterDB();
-               $encTimestamp = $dbw->addQuotes( $dbw->timestamp() );
-               $encUserId = $dbw->addQuotes( $wgUser->getId() );
-               $encReason = $dbw->addQuotes( $this->reason );
-               $encGroup = $dbw->addQuotes( 'deleted' );
-               $ext = $this->file->getExtension();
-               $dotExt = $ext === '' ? '' : ".$ext";
-               $encExt = $dbw->addQuotes( $dotExt );
-               list( $oldRels, $deleteCurrent ) = $this->getOldRels();
-
-               // Bitfields to further suppress the content
-               if ( $this->suppress ) {
-                       $bitfield = 0;
-                       // This should be 15...
-                       $bitfield |= Revision::DELETED_TEXT;
-                       $bitfield |= Revision::DELETED_COMMENT;
-                       $bitfield |= Revision::DELETED_USER;
-                       $bitfield |= Revision::DELETED_RESTRICTED;
-               } else {
-                       $bitfield = 'oi_deleted';
-               }
-
-               if ( $deleteCurrent ) {
-                       $concat = $dbw->buildConcat( array( "img_sha1", $encExt ) );
-                       $where = array( 'img_name' => $this->file->getName() );
-                       $dbw->insertSelect( 'filearchive', 'image',
-                               array(
-                                       'fa_storage_group' => $encGroup,
-                                       'fa_storage_key'   => "CASE WHEN img_sha1='' THEN '' ELSE $concat END",
-                                       'fa_deleted_user'      => $encUserId,
-                                       'fa_deleted_timestamp' => $encTimestamp,
-                                       'fa_deleted_reason'    => $encReason,
-                                       'fa_deleted'               => $this->suppress ? $bitfield : 0,
-
-                                       'fa_name'         => 'img_name',
-                                       'fa_archive_name' => 'NULL',
-                                       'fa_size'         => 'img_size',
-                                       'fa_width'        => 'img_width',
-                                       'fa_height'       => 'img_height',
-                                       'fa_metadata'     => 'img_metadata',
-                                       'fa_bits'         => 'img_bits',
-                                       'fa_media_type'   => 'img_media_type',
-                                       'fa_major_mime'   => 'img_major_mime',
-                                       'fa_minor_mime'   => 'img_minor_mime',
-                                       'fa_description'  => 'img_description',
-                                       'fa_user'         => 'img_user',
-                                       'fa_user_text'    => 'img_user_text',
-                                       'fa_timestamp'    => 'img_timestamp'
-                               ), $where, __METHOD__ );
-               }
-
-               if ( count( $oldRels ) ) {
-                       $concat = $dbw->buildConcat( array( "oi_sha1", $encExt ) );
-                       $where = array(
-                               'oi_name' => $this->file->getName(),
-                               'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')' );
-                       $dbw->insertSelect( 'filearchive', 'oldimage',
-                               array(
-                                       'fa_storage_group' => $encGroup,
-                                       'fa_storage_key'   => "CASE WHEN oi_sha1='' THEN '' ELSE $concat END",
-                                       'fa_deleted_user'      => $encUserId,
-                                       'fa_deleted_timestamp' => $encTimestamp,
-                                       'fa_deleted_reason'    => $encReason,
-                                       'fa_deleted'               => $this->suppress ? $bitfield : 'oi_deleted',
-
-                                       'fa_name'         => 'oi_name',
-                                       'fa_archive_name' => 'oi_archive_name',
-                                       'fa_size'         => 'oi_size',
-                                       'fa_width'        => 'oi_width',
-                                       'fa_height'       => 'oi_height',
-                                       'fa_metadata'     => 'oi_metadata',
-                                       'fa_bits'         => 'oi_bits',
-                                       'fa_media_type'   => 'oi_media_type',
-                                       'fa_major_mime'   => 'oi_major_mime',
-                                       'fa_minor_mime'   => 'oi_minor_mime',
-                                       'fa_description'  => 'oi_description',
-                                       'fa_user'         => 'oi_user',
-                                       'fa_user_text'    => 'oi_user_text',
-                                       'fa_timestamp'    => 'oi_timestamp',
-                                       'fa_deleted'      => $bitfield
-                               ), $where, __METHOD__ );
-               }
-       }
-
-       function doDBDeletes() {
-               $dbw = $this->file->repo->getMasterDB();
-               list( $oldRels, $deleteCurrent ) = $this->getOldRels();
-
-               if ( count( $oldRels ) ) {
-                       $dbw->delete( 'oldimage',
-                               array(
-                                       'oi_name' => $this->file->getName(),
-                                       'oi_archive_name' => array_keys( $oldRels )
-                               ), __METHOD__ );
-               }
-
-               if ( $deleteCurrent ) {
-                       $dbw->delete( 'image', array( 'img_name' => $this->file->getName() ), __METHOD__ );
-               }
-       }
-
-       /**
-        * Run the transaction
-        */
-       function execute() {
-               global $wgUseSquid;
-               wfProfileIn( __METHOD__ );
-
-               $this->file->lock();
-               // Leave private files alone
-               $privateFiles = array();
-               list( $oldRels, $deleteCurrent ) = $this->getOldRels();
-               $dbw = $this->file->repo->getMasterDB();
-
-               if ( !empty( $oldRels ) ) {
-                       $res = $dbw->select( 'oldimage',
-                               array( 'oi_archive_name' ),
-                               array( 'oi_name' => $this->file->getName(),
-                                       'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')',
-                                       $dbw->bitAnd( 'oi_deleted', File::DELETED_FILE ) => File::DELETED_FILE ),
-                               __METHOD__ );
-
-                       foreach ( $res as $row ) {
-                               $privateFiles[$row->oi_archive_name] = 1;
-                       }
-               }
-               // Prepare deletion batch
-               $hashes = $this->getHashes();
-               $this->deletionBatch = array();
-               $ext = $this->file->getExtension();
-               $dotExt = $ext === '' ? '' : ".$ext";
-
-               foreach ( $this->srcRels as $name => $srcRel ) {
-                       // Skip files that have no hash (missing source).
-                       // Keep private files where they are.
-                       if ( isset( $hashes[$name] ) && !array_key_exists( $name, $privateFiles ) ) {
-                               $hash = $hashes[$name];
-                               $key = $hash . $dotExt;
-                               $dstRel = $this->file->repo->getDeletedHashPath( $key ) . $key;
-                               $this->deletionBatch[$name] = array( $srcRel, $dstRel );
-                       }
-               }
-
-               // Lock the filearchive rows so that the files don't get deleted by a cleanup operation
-               // We acquire this lock by running the inserts now, before the file operations.
-               //
-               // This potentially has poor lock contention characteristics -- an alternative
-               // scheme would be to insert stub filearchive entries with no fa_name and commit
-               // them in a separate transaction, then run the file ops, then update the fa_name fields.
-               $this->doDBInserts();
-
-               // Removes non-existent file from the batch, so we don't get errors.
-               $this->deletionBatch = $this->removeNonexistentFiles( $this->deletionBatch );
-
-               // Execute the file deletion batch
-               $status = $this->file->repo->deleteBatch( $this->deletionBatch );
-
-               if ( !$status->isGood() ) {
-                       $this->status->merge( $status );
-               }
-
-               if ( !$this->status->ok ) {
-                       // Critical file deletion error
-                       // Roll back inserts, release lock and abort
-                       // TODO: delete the defunct filearchive rows if we are using a non-transactional DB
-                       $this->file->unlockAndRollback();
-                       wfProfileOut( __METHOD__ );
-                       return $this->status;
-               }
-
-               // Purge squid
-               if ( $wgUseSquid ) {
-                       $urls = array();
-
-                       foreach ( $this->srcRels as $srcRel ) {
-                               $urlRel = str_replace( '%2F', '/', rawurlencode( $srcRel ) );
-                               $urls[] = $this->file->repo->getZoneUrl( 'public' ) . '/' . $urlRel;
-                       }
-                       SquidUpdate::purge( $urls );
-               }
-
-               // Delete image/oldimage rows
-               $this->doDBDeletes();
-
-               // Commit and return
-               $this->file->unlock();
-               wfProfileOut( __METHOD__ );
-
-               return $this->status;
-       }
-
-       /**
-        * Removes non-existent files from a deletion batch.
-        */
-       function removeNonexistentFiles( $batch ) {
-               $files = $newBatch = array();
-
-               foreach ( $batch as $batchItem ) {
-                       list( $src, $dest ) = $batchItem;
-                       $files[$src] = $this->file->repo->getVirtualUrl( 'public' ) . '/' . rawurlencode( $src );
-               }
-
-               $result = $this->file->repo->fileExistsBatch( $files, FSRepo::FILES_ONLY );
-
-               foreach ( $batch as $batchItem ) {
-                       if ( $result[$batchItem[0]] ) {
-                               $newBatch[] = $batchItem;
-                       }
-               }
-
-               return $newBatch;
-       }
-}
-
-# ------------------------------------------------------------------------------
-
-/**
- * Helper class for file undeletion
- * @ingroup FileRepo
- */
-class LocalFileRestoreBatch {
-       /**
-        * @var LocalFile
-        */
-       var $file;
-
-       var $cleanupBatch, $ids, $all, $unsuppress = false;
-
-       function __construct( File $file, $unsuppress = false ) {
-               $this->file = $file;
-               $this->cleanupBatch = $this->ids = array();
-               $this->ids = array();
-               $this->unsuppress = $unsuppress;
-       }
-
-       /**
-        * Add a file by ID
-        */
-       function addId( $fa_id ) {
-               $this->ids[] = $fa_id;
-       }
-
-       /**
-        * Add a whole lot of files by ID
-        */
-       function addIds( $ids ) {
-               $this->ids = array_merge( $this->ids, $ids );
-       }
-
-       /**
-        * Add all revisions of the file
-        */
-       function addAll() {
-               $this->all = true;
-       }
-
-       /**
-        * Run the transaction, except the cleanup batch.
-        * The cleanup batch should be run in a separate transaction, because it locks different
-        * rows and there's no need to keep the image row locked while it's acquiring those locks
-        * The caller may have its own transaction open.
-        * So we save the batch and let the caller call cleanup()
-        */
-       function execute() {
-               global $wgLang;
-
-               if ( !$this->all && !$this->ids ) {
-                       // Do nothing
-                       return $this->file->repo->newGood();
-               }
-
-               $exists = $this->file->lock();
-               $dbw = $this->file->repo->getMasterDB();
-               $status = $this->file->repo->newGood();
-
-               // Fetch all or selected archived revisions for the file,
-               // sorted from the most recent to the oldest.
-               $conditions = array( 'fa_name' => $this->file->getName() );
-
-               if ( !$this->all ) {
-                       $conditions[] = 'fa_id IN (' . $dbw->makeList( $this->ids ) . ')';
-               }
-
-               $result = $dbw->select( 'filearchive', '*',
-                       $conditions,
-                       __METHOD__,
-                       array( 'ORDER BY' => 'fa_timestamp DESC' )
-               );
-
-               $idsPresent = array();
-               $storeBatch = array();
-               $insertBatch = array();
-               $insertCurrent = false;
-               $deleteIds = array();
-               $first = true;
-               $archiveNames = array();
-
-               foreach ( $result as $row ) {
-                       $idsPresent[] = $row->fa_id;
-
-                       if ( $row->fa_name != $this->file->getName() ) {
-                               $status->error( 'undelete-filename-mismatch', $wgLang->timeanddate( $row->fa_timestamp ) );
-                               $status->failCount++;
-                               continue;
-                       }
-
-                       if ( $row->fa_storage_key == '' ) {
-                               // Revision was missing pre-deletion
-                               $status->error( 'undelete-bad-store-key', $wgLang->timeanddate( $row->fa_timestamp ) );
-                               $status->failCount++;
-                               continue;
-                       }
-
-                       $deletedRel = $this->file->repo->getDeletedHashPath( $row->fa_storage_key ) . $row->fa_storage_key;
-                       $deletedUrl = $this->file->repo->getVirtualUrl() . '/deleted/' . $deletedRel;
-
-                       $sha1 = substr( $row->fa_storage_key, 0, strcspn( $row->fa_storage_key, '.' ) );
-
-                       # Fix leading zero
-                       if ( strlen( $sha1 ) == 32 && $sha1[0] == '0' ) {
-                               $sha1 = substr( $sha1, 1 );
-                       }
-
-                       if ( is_null( $row->fa_major_mime ) || $row->fa_major_mime == 'unknown'
-                               || is_null( $row->fa_minor_mime ) || $row->fa_minor_mime == 'unknown'
-                               || is_null( $row->fa_media_type ) || $row->fa_media_type == 'UNKNOWN'
-                               || is_null( $row->fa_metadata ) ) {
-                               // Refresh our metadata
-                               // Required for a new current revision; nice for older ones too. :)
-                               $props = RepoGroup::singleton()->getFileProps( $deletedUrl );
-                       } else {
-                               $props = array(
-                                       'minor_mime' => $row->fa_minor_mime,
-                                       'major_mime' => $row->fa_major_mime,
-                                       'media_type' => $row->fa_media_type,
-                                       'metadata'   => $row->fa_metadata
-                               );
-                       }
-
-                       if ( $first && !$exists ) {
-                               // This revision will be published as the new current version
-                               $destRel = $this->file->getRel();
-                               $insertCurrent = array(
-                                       'img_name'        => $row->fa_name,
-                                       'img_size'        => $row->fa_size,
-                                       'img_width'       => $row->fa_width,
-                                       'img_height'      => $row->fa_height,
-                                       'img_metadata'    => $props['metadata'],
-                                       'img_bits'        => $row->fa_bits,
-                                       'img_media_type'  => $props['media_type'],
-                                       'img_major_mime'  => $props['major_mime'],
-                                       'img_minor_mime'  => $props['minor_mime'],
-                                       'img_description' => $row->fa_description,
-                                       'img_user'        => $row->fa_user,
-                                       'img_user_text'   => $row->fa_user_text,
-                                       'img_timestamp'   => $row->fa_timestamp,
-                                       'img_sha1'        => $sha1
-                               );
-
-                               // The live (current) version cannot be hidden!
-                               if ( !$this->unsuppress && $row->fa_deleted ) {
-                                       $storeBatch[] = array( $deletedUrl, 'public', $destRel );
-                                       $this->cleanupBatch[] = $row->fa_storage_key;
-                               }
-                       } else {
-                               $archiveName = $row->fa_archive_name;
-
-                               if ( $archiveName == '' ) {
-                                       // This was originally a current version; we
-                                       // have to devise a new archive name for it.
-                                       // Format is <timestamp of archiving>!<name>
-                                       $timestamp = wfTimestamp( TS_UNIX, $row->fa_deleted_timestamp );
-
-                                       do {
-                                               $archiveName = wfTimestamp( TS_MW, $timestamp ) . '!' . $row->fa_name;
-                                               $timestamp++;
-                                       } while ( isset( $archiveNames[$archiveName] ) );
-                               }
-
-                               $archiveNames[$archiveName] = true;
-                               $destRel = $this->file->getArchiveRel( $archiveName );
-                               $insertBatch[] = array(
-                                       'oi_name'         => $row->fa_name,
-                                       'oi_archive_name' => $archiveName,
-                                       'oi_size'         => $row->fa_size,
-                                       'oi_width'        => $row->fa_width,
-                                       'oi_height'       => $row->fa_height,
-                                       'oi_bits'         => $row->fa_bits,
-                                       'oi_description'  => $row->fa_description,
-                                       'oi_user'         => $row->fa_user,
-                                       'oi_user_text'    => $row->fa_user_text,
-                                       'oi_timestamp'    => $row->fa_timestamp,
-                                       'oi_metadata'     => $props['metadata'],
-                                       'oi_media_type'   => $props['media_type'],
-                                       'oi_major_mime'   => $props['major_mime'],
-                                       'oi_minor_mime'   => $props['minor_mime'],
-                                       'oi_deleted'      => $this->unsuppress ? 0 : $row->fa_deleted,
-                                       'oi_sha1'         => $sha1 );
-                       }
-
-                       $deleteIds[] = $row->fa_id;
-
-                       if ( !$this->unsuppress && $row->fa_deleted & File::DELETED_FILE ) {
-                               // private files can stay where they are
-                               $status->successCount++;
-                       } else {
-                               $storeBatch[] = array( $deletedUrl, 'public', $destRel );
-                               $this->cleanupBatch[] = $row->fa_storage_key;
-                       }
-
-                       $first = false;
-               }
-
-               unset( $result );
-
-               // Add a warning to the status object for missing IDs
-               $missingIds = array_diff( $this->ids, $idsPresent );
-
-               foreach ( $missingIds as $id ) {
-                       $status->error( 'undelete-missing-filearchive', $id );
-               }
-
-               // Remove missing files from batch, so we don't get errors when undeleting them
-               $storeBatch = $this->removeNonexistentFiles( $storeBatch );
-
-               // Run the store batch
-               // Use the OVERWRITE_SAME flag to smooth over a common error
-               $storeStatus = $this->file->repo->storeBatch( $storeBatch, FileRepo::OVERWRITE_SAME );
-               $status->merge( $storeStatus );
-
-               if ( !$status->isGood() ) {
-                       // Even if some files could be copied, fail entirely as that is the
-                       // easiest thing to do without data loss
-                       $this->cleanupFailedBatch( $storeStatus, $storeBatch );
-                       $status->ok = false;
-                       $this->file->unlock();
-
-                       return $status;
-               }
-
-               // Run the DB updates
-               // Because we have locked the image row, key conflicts should be rare.
-               // If they do occur, we can roll back the transaction at this time with
-               // no data loss, but leaving unregistered files scattered throughout the
-               // public zone.
-               // This is not ideal, which is why it's important to lock the image row.
-               if ( $insertCurrent ) {
-                       $dbw->insert( 'image', $insertCurrent, __METHOD__ );
-               }
-
-               if ( $insertBatch ) {
-                       $dbw->insert( 'oldimage', $insertBatch, __METHOD__ );
-               }
-
-               if ( $deleteIds ) {
-                       $dbw->delete( 'filearchive',
-                               array( 'fa_id IN (' . $dbw->makeList( $deleteIds ) . ')' ),
-                               __METHOD__ );
-               }
-
-               // If store batch is empty (all files are missing), deletion is to be considered successful
-               if ( $status->successCount > 0 || !$storeBatch ) {
-                       if ( !$exists ) {
-                               wfDebug( __METHOD__ . " restored {$status->successCount} items, creating a new current\n" );
-
-                               // Update site_stats
-                               $site_stats = $dbw->tableName( 'site_stats' );
-                               $dbw->query( "UPDATE $site_stats SET ss_images=ss_images+1", __METHOD__ );
-
-                               $this->file->purgeEverything();
-                       } else {
-                               wfDebug( __METHOD__ . " restored {$status->successCount} as archived versions\n" );
-                               $this->file->purgeDescription();
-                               $this->file->purgeHistory();
-                       }
-               }
-
-               $this->file->unlock();
-
-               return $status;
-       }
-
-       /**
-        * Removes non-existent files from a store batch.
-        */
-       function removeNonexistentFiles( $triplets ) {
-               $files = $filteredTriplets = array();
-               foreach ( $triplets as $file )
-                       $files[$file[0]] = $file[0];
-
-               $result = $this->file->repo->fileExistsBatch( $files, FSRepo::FILES_ONLY );
-
-               foreach ( $triplets as $file ) {
-                       if ( $result[$file[0]] ) {
-                               $filteredTriplets[] = $file;
-                       }
-               }
-
-               return $filteredTriplets;
-       }
-
-       /**
-        * Removes non-existent files from a cleanup batch.
-        */
-       function removeNonexistentFromCleanup( $batch ) {
-               $files = $newBatch = array();
-               $repo = $this->file->repo;
-
-               foreach ( $batch as $file ) {
-                       $files[$file] = $repo->getVirtualUrl( 'deleted' ) . '/' .
-                               rawurlencode( $repo->getDeletedHashPath( $file ) . $file );
-               }
-
-               $result = $repo->fileExistsBatch( $files, FSRepo::FILES_ONLY );
-
-               foreach ( $batch as $file ) {
-                       if ( $result[$file] ) {
-                               $newBatch[] = $file;
-                       }
-               }
-
-               return $newBatch;
-       }
-
-       /**
-        * Delete unused files in the deleted zone.
-        * This should be called from outside the transaction in which execute() was called.
-        */
-       function cleanup() {
-               if ( !$this->cleanupBatch ) {
-                       return $this->file->repo->newGood();
-               }
-
-               $this->cleanupBatch = $this->removeNonexistentFromCleanup( $this->cleanupBatch );
-
-               $status = $this->file->repo->cleanupDeletedBatch( $this->cleanupBatch );
-
-               return $status;
-       }
-
-       /**
-        * Cleanup a failed batch. The batch was only partially successful, so
-        * rollback by removing all items that were succesfully copied.
-        *
-        * @param Status $storeStatus
-        * @param array $storeBatch
-        */
-       function cleanupFailedBatch( $storeStatus, $storeBatch ) {
-               $cleanupBatch = array();
-
-               foreach ( $storeStatus->success as $i => $success ) {
-                       // Check if this item of the batch was successfully copied
-                       if ( $success ) {
-                               // Item was successfully copied and needs to be removed again
-                               // Extract ($dstZone, $dstRel) from the batch
-                               $cleanupBatch[] = array( $storeBatch[$i][1], $storeBatch[$i][2] );
-                       }
-               }
-               $this->file->repo->cleanupBatch( $cleanupBatch );
-       }
-}
-
-# ------------------------------------------------------------------------------
-
-/**
- * Helper class for file movement
- * @ingroup FileRepo
- */
-class LocalFileMoveBatch {
-
-       /**
-        * @var File
-        */
-       var $file;
-
-       /**
-        * @var Title
-        */
-       var $target;
-
-       var $cur, $olds, $oldCount, $archive, $db;
-
-       function __construct( File $file, Title $target ) {
-               $this->file = $file;
-               $this->target = $target;
-               $this->oldHash = $this->file->repo->getHashPath( $this->file->getName() );
-               $this->newHash = $this->file->repo->getHashPath( $this->target->getDBkey() );
-               $this->oldName = $this->file->getName();
-               $this->newName = $this->file->repo->getNameFromTitle( $this->target );
-               $this->oldRel = $this->oldHash . $this->oldName;
-               $this->newRel = $this->newHash . $this->newName;
-               $this->db = $file->repo->getMasterDb();
-       }
-
-       /**
-        * Add the current image to the batch
-        */
-       function addCurrent() {
-               $this->cur = array( $this->oldRel, $this->newRel );
-       }
-
-       /**
-        * Add the old versions of the image to the batch
-        */
-       function addOlds() {
-               $archiveBase = 'archive';
-               $this->olds = array();
-               $this->oldCount = 0;
-
-               $result = $this->db->select( 'oldimage',
-                       array( 'oi_archive_name', 'oi_deleted' ),
-                       array( 'oi_name' => $this->oldName ),
-                       __METHOD__
-               );
-
-               foreach ( $result as $row ) {
-                       $oldName = $row->oi_archive_name;
-                       $bits = explode( '!', $oldName, 2 );
-
-                       if ( count( $bits ) != 2 ) {
-                               wfDebug( "Old file name missing !: '$oldName' \n" );
-                               continue;
-                       }
-
-                       list( $timestamp, $filename ) = $bits;
-
-                       if ( $this->oldName != $filename ) {
-                               wfDebug( "Old file name doesn't match: '$oldName' \n" );
-                               continue;
-                       }
-
-                       $this->oldCount++;
-
-                       // Do we want to add those to oldCount?
-                       if ( $row->oi_deleted & File::DELETED_FILE ) {
-                               continue;
-                       }
-
-                       $this->olds[] = array(
-                               "{$archiveBase}/{$this->oldHash}{$oldName}",
-                               "{$archiveBase}/{$this->newHash}{$timestamp}!{$this->newName}"
-                       );
-               }
-       }
-
-       /**
-        * Perform the move.
-        */
-       function execute() {
-               $repo = $this->file->repo;
-               $status = $repo->newGood();
-               $triplets = $this->getMoveTriplets();
-
-               $triplets = $this->removeNonexistentFiles( $triplets );
-
-               // Copy the files into their new location
-               $statusMove = $repo->storeBatch( $triplets );
-               wfDebugLog( 'imagemove', "Moved files for {$this->file->getName()}: {$statusMove->successCount} successes, {$statusMove->failCount} failures" );
-               if ( !$statusMove->isGood() ) {
-                       wfDebugLog( 'imagemove', "Error in moving files: " . $statusMove->getWikiText() );
-                       $this->cleanupTarget( $triplets );
-                       $statusMove->ok = false;
-                       return $statusMove;
-               }
-
-               $this->db->begin();
-               $statusDb = $this->doDBUpdates();
-               wfDebugLog( 'imagemove', "Renamed {$this->file->getName()} in database: {$statusDb->successCount} successes, {$statusDb->failCount} failures" );
-               if ( !$statusDb->isGood() ) {
-                       $this->db->rollback();
-                       // Something went wrong with the DB updates, so remove the target files
-                       $this->cleanupTarget( $triplets );
-                       $statusDb->ok = false;
-                       return $statusDb;
-               }
-               $this->db->commit();
-
-               // Everything went ok, remove the source files
-               $this->cleanupSource( $triplets );
-
-               $status->merge( $statusDb );
-               $status->merge( $statusMove );
-
-               return $status;
-       }
-
-       /**
-        * Do the database updates and return a new FileRepoStatus indicating how
-        * many rows where updated.
-        *
-        * @return FileRepoStatus
-        */
-       function doDBUpdates() {
-               $repo = $this->file->repo;
-               $status = $repo->newGood();
-               $dbw = $this->db;
-
-               // Update current image
-               $dbw->update(
-                       'image',
-                       array( 'img_name' => $this->newName ),
-                       array( 'img_name' => $this->oldName ),
-                       __METHOD__
-               );
-
-               if ( $dbw->affectedRows() ) {
-                       $status->successCount++;
-               } else {
-                       $status->failCount++;
-                       $status->fatal( 'imageinvalidfilename' );
-                       return $status;
-               }
-
-               // Update old images
-               $dbw->update(
-                       'oldimage',
-                       array(
-                               'oi_name' => $this->newName,
-                               'oi_archive_name = ' . $dbw->strreplace( 'oi_archive_name', $dbw->addQuotes( $this->oldName ), $dbw->addQuotes( $this->newName ) ),
-                       ),
-                       array( 'oi_name' => $this->oldName ),
-                       __METHOD__
-               );
-
-               $affected = $dbw->affectedRows();
-               $total = $this->oldCount;
-               $status->successCount += $affected;
-               $status->failCount += $total - $affected;
-               if ( $status->failCount ) {
-                       $status->error( 'imageinvalidfilename' );
-               }
-
-               return $status;
-       }
-
-       /**
-        * Generate triplets for FSRepo::storeBatch().
-        */
-       function getMoveTriplets() {
-               $moves = array_merge( array( $this->cur ), $this->olds );
-               $triplets = array();    // The format is: (srcUrl, destZone, destUrl)
-
-               foreach ( $moves as $move ) {
-                       // $move: (oldRelativePath, newRelativePath)
-                       $srcUrl = $this->file->repo->getVirtualUrl() . '/public/' . rawurlencode( $move[0] );
-                       $triplets[] = array( $srcUrl, 'public', $move[1] );
-                       wfDebugLog( 'imagemove', "Generated move triplet for {$this->file->getName()}: {$srcUrl} :: public :: {$move[1]}" );
-               }
-
-               return $triplets;
-       }
-
-       /**
-        * Removes non-existent files from move batch.
-        */
-       function removeNonexistentFiles( $triplets ) {
-               $files = array();
-
-               foreach ( $triplets as $file ) {
-                       $files[$file[0]] = $file[0];
-               }
-
-               $result = $this->file->repo->fileExistsBatch( $files, FSRepo::FILES_ONLY );
-               $filteredTriplets = array();
-
-               foreach ( $triplets as $file ) {
-                       if ( $result[$file[0]] ) {
-                               $filteredTriplets[] = $file;
-                       } else {
-                               wfDebugLog( 'imagemove', "File {$file[0]} does not exist" );
-                       }
-               }
-
-               return $filteredTriplets;
-       }
-
-       /**
-        * Cleanup a partially moved array of triplets by deleting the target
-        * files. Called if something went wrong half way.
-        */
-       function cleanupTarget( $triplets ) {
-               // Create dest pairs from the triplets
-               $pairs = array();
-               foreach ( $triplets as $triplet ) {
-                       $pairs[] = array( $triplet[1], $triplet[2] );
-               }
-
-               $this->file->repo->cleanupBatch( $pairs );
-       }
-
-       /**
-        * Cleanup a fully moved array of triplets by deleting the source files.
-        * Called at the end of the move process if everything else went ok.
-        */
-       function cleanupSource( $triplets ) {
-               // Create source file names from the triplets
-               $files = array();
-               foreach ( $triplets as $triplet ) {
-                       $files[] = $triplet[0];
-               }
-
-               $this->file->repo->cleanupBatch( $files );
-       }
-}
diff --git a/includes/filerepo/OldLocalFile.php b/includes/filerepo/OldLocalFile.php
deleted file mode 100644 (file)
index a22da16..0000000
+++ /dev/null
@@ -1,295 +0,0 @@
-<?php
-/**
- * Old file in the oldimage table
- *
- * @file
- * @ingroup FileRepo
- */
-
-/**
- * Class to represent a file in the oldimage table
- *
- * @ingroup FileRepo
- */
-class OldLocalFile extends LocalFile {
-       var $requestedTime, $archive_name;
-
-       const CACHE_VERSION = 1;
-       const MAX_CACHE_ROWS = 20;
-
-       static function newFromTitle( $title, $repo, $time = null ) {
-               # The null default value is only here to avoid an E_STRICT
-               if ( $time === null ) {
-                       throw new MWException( __METHOD__.' got null for $time parameter' );
-               }
-               return new self( $title, $repo, $time, null );
-       }
-
-       static function newFromArchiveName( $title, $repo, $archiveName ) {
-               return new self( $title, $repo, null, $archiveName );
-       }
-
-       static function newFromRow( $row, $repo ) {
-               $title = Title::makeTitle( NS_FILE, $row->oi_name );
-               $file = new self( $title, $repo, null, $row->oi_archive_name );
-               $file->loadFromRow( $row, 'oi_' );
-               return $file;
-       }
-
-       /**
-        * Create a OldLocalFile from a SHA-1 key
-        * Do not call this except from inside a repo class.
-        *
-        * @param $sha1 string base-36 SHA-1
-        * @param $repo LocalRepo
-        * @param string|bool $timestamp MW_timestamp (optional)
-        *
-        * @return bool|OldLocalFile
-        */
-       static function newFromKey( $sha1, $repo, $timestamp = false ) {
-               $dbr = $repo->getSlaveDB();
-
-               $conds = array( 'oi_sha1' => $sha1 );
-               if ( $timestamp ) {
-                       $conds['oi_timestamp'] = $dbr->timestamp( $timestamp );
-               }
-
-               $row = $dbr->selectRow( 'oldimage', self::selectFields(), $conds, __METHOD__ );
-               if ( $row ) {
-                       return self::newFromRow( $row, $repo );
-               } else {
-                       return false;
-               }
-       }
-       
-       /**
-        * Fields in the oldimage table
-        */
-       static function selectFields() {
-               return array(
-                       'oi_name',
-                       'oi_archive_name',
-                       'oi_size',
-                       'oi_width',
-                       'oi_height',
-                       'oi_metadata',
-                       'oi_bits',
-                       'oi_media_type',
-                       'oi_major_mime',
-                       'oi_minor_mime',
-                       'oi_description',
-                       'oi_user',
-                       'oi_user_text',
-                       'oi_timestamp',
-                       'oi_deleted',
-                       'oi_sha1',
-               );
-       }
-
-       /**
-        * @param $title Title
-        * @param $repo FileRepo
-        * @param $time String: timestamp or null to load by archive name
-        * @param $archiveName String: archive name or null to load by timestamp
-        */
-       function __construct( $title, $repo, $time, $archiveName ) {
-               parent::__construct( $title, $repo );
-               $this->requestedTime = $time;
-               $this->archive_name = $archiveName;
-               if ( is_null( $time ) && is_null( $archiveName ) ) {
-                       throw new MWException( __METHOD__.': must specify at least one of $time or $archiveName' );
-               }
-       }
-
-       function getCacheKey() {
-               return false;
-       }
-
-       function getArchiveName() {
-               if ( !isset( $this->archive_name ) ) {
-                       $this->load();
-               }
-               return $this->archive_name;
-       }
-
-       function isOld() {
-               return true;
-       }
-
-       function isVisible() {
-               return $this->exists() && !$this->isDeleted(File::DELETED_FILE);
-       }
-
-       function loadFromDB() {
-               wfProfileIn( __METHOD__ );
-               $this->dataLoaded = true;
-               $dbr = $this->repo->getSlaveDB();
-               $conds = array( 'oi_name' => $this->getName() );
-               if ( is_null( $this->requestedTime ) ) {
-                       $conds['oi_archive_name'] = $this->archive_name;
-               } else {
-                       $conds[] = 'oi_timestamp = ' . $dbr->addQuotes( $dbr->timestamp( $this->requestedTime ) );
-               }
-               $row = $dbr->selectRow( 'oldimage', $this->getCacheFields( 'oi_' ),
-                       $conds, __METHOD__, array( 'ORDER BY' => 'oi_timestamp DESC' ) );
-               if ( $row ) {
-                       $this->loadFromRow( $row, 'oi_' );
-               } else {
-                       $this->fileExists = false;
-               }
-               wfProfileOut( __METHOD__ );
-       }
-
-       function getCacheFields( $prefix = 'img_' ) {
-               $fields = parent::getCacheFields( $prefix );
-               $fields[] = $prefix . 'archive_name';
-               $fields[] = $prefix . 'deleted';
-               return $fields;
-       }
-
-       function getRel() {
-               return 'archive/' . $this->getHashPath() . $this->getArchiveName();
-       }
-
-       function getUrlRel() {
-               return 'archive/' . $this->getHashPath() . rawurlencode( $this->getArchiveName() );
-       }
-
-       function upgradeRow() {
-               wfProfileIn( __METHOD__ );
-               $this->loadFromFile();
-
-               # Don't destroy file info of missing files
-               if ( !$this->fileExists ) {
-                       wfDebug( __METHOD__.": file does not exist, aborting\n" );
-                       wfProfileOut( __METHOD__ );
-                       return;
-               }
-
-               $dbw = $this->repo->getMasterDB();
-               list( $major, $minor ) = self::splitMime( $this->mime );
-
-               wfDebug(__METHOD__.': upgrading '.$this->archive_name." to the current schema\n");
-               $dbw->update( 'oldimage',
-                       array(
-                               'oi_width' => $this->width,
-                               'oi_height' => $this->height,
-                               'oi_bits' => $this->bits,
-                               'oi_media_type' => $this->media_type,
-                               'oi_major_mime' => $major,
-                               'oi_minor_mime' => $minor,
-                               'oi_metadata' => $this->metadata,
-                               'oi_sha1' => $this->sha1,
-                       ), array(
-                               'oi_name' => $this->getName(),
-                               'oi_archive_name' => $this->archive_name ),
-                       __METHOD__
-               );
-               wfProfileOut( __METHOD__ );
-       }
-
-       /**
-        * @param $field Integer: one of DELETED_* bitfield constants
-        *               for file or revision rows
-        * @return bool
-        */
-       function isDeleted( $field ) {
-               $this->load();
-               return ($this->deleted & $field) == $field;
-       }
-
-       /**
-        * Returns bitfield value
-        * @return int
-        */
-       function getVisibility() {
-               $this->load();
-               return (int)$this->deleted;
-       }
-
-       /**
-        * Determine if the current user is allowed to view a particular
-        * field of this image file, if it's marked as deleted.
-        *
-        * @param $field Integer
-        * @param $user User object to check, or null to use $wgUser
-        * @return bool
-        */
-       function userCan( $field, User $user = null ) {
-               $this->load();
-               return Revision::userCanBitfield( $this->deleted, $field, $user );
-       }
-       
-       /**
-        * Upload a file directly into archive. Generally for Special:Import.
-        * 
-        * @param $srcPath string File system path of the source file
-        * @param $archiveName string Full archive name of the file, in the form 
-        *      $timestamp!$filename, where $filename must match $this->getName()
-        *
-        * @return FileRepoStatus
-        */
-       function uploadOld( $srcPath, $archiveName, $timestamp, $comment, $user, $flags = 0 ) {
-               $this->lock();
-               
-               $dstRel = 'archive/' . $this->getHashPath() . $archiveName;
-               $status = $this->publishTo( $srcPath, $dstRel,
-                       $flags & File::DELETE_SOURCE ? FileRepo::DELETE_SOURCE : 0
-               );
-               
-               if ( $status->isGood() ) {
-                       if ( !$this->recordOldUpload( $srcPath, $archiveName, $timestamp, $comment, $user ) ) {
-                               $status->fatal( 'filenotfound', $srcPath );
-                       }
-               }
-               
-               $this->unlock();
-               
-               return $status;
-       }
-       
-       /**
-        * Record a file upload in the oldimage table, without adding log entries.
-        * 
-        * @param $srcPath string File system path to the source file
-        * @param $archiveName string The archive name of the file
-        * @param $comment string Upload comment
-        * @param $user User User who did this upload
-        * @return bool
-        */
-       function recordOldUpload( $srcPath, $archiveName, $timestamp, $comment, $user ) {
-               $dbw = $this->repo->getMasterDB();
-               $dbw->begin();
-
-               $dstPath = $this->repo->getZonePath( 'public' ) . '/' . $this->getRel();
-               $props = self::getPropsFromPath( $dstPath );
-               if ( !$props['fileExists'] ) {
-                       return false;
-               }
-
-               $dbw->insert( 'oldimage',
-                       array(
-                               'oi_name'         => $this->getName(),
-                               'oi_archive_name' => $archiveName,
-                               'oi_size'         => $props['size'],
-                               'oi_width'        => intval( $props['width'] ),
-                               'oi_height'       => intval( $props['height'] ),
-                               'oi_bits'         => $props['bits'],
-                               'oi_timestamp'    => $dbw->timestamp( $timestamp ),
-                               'oi_description'  => $comment,
-                               'oi_user'         => $user->getId(),
-                               'oi_user_text'    => $user->getName(),
-                               'oi_metadata'     => $props['metadata'],
-                               'oi_media_type'   => $props['media_type'],
-                               'oi_major_mime'   => $props['major_mime'],
-                               'oi_minor_mime'   => $props['minor_mime'],
-                               'oi_sha1'         => $props['sha1'],
-                       ), __METHOD__
-               );
-
-               $dbw->commit();
-
-               return true;
-       }
-       
-}
diff --git a/includes/filerepo/UnregisteredLocalFile.php b/includes/filerepo/UnregisteredLocalFile.php
deleted file mode 100644 (file)
index 6a0e097..0000000
+++ /dev/null
@@ -1,143 +0,0 @@
-<?php
-/**
- * File without associated database record
- *
- * @file
- * @ingroup FileRepo
- */
-
-/**
- * A file object referring to either a standalone local file, or a file in a
- * local repository with no database, for example an FSRepo repository.
- *
- * Read-only.
- *
- * TODO: Currently it doesn't really work in the repository role, there are
- * lots of functions missing. It is used by the WebStore extension in the
- * standalone role.
- *
- * @ingroup FileRepo
- */
-class UnregisteredLocalFile extends File {
-       var $title, $path, $mime, $dims;
-
-       /**
-        * @var MediaHandler
-        */
-       var $handler;
-
-       /**
-        * @param $path
-        * @param $mime
-        * @return UnregisteredLocalFile
-        */
-       static function newFromPath( $path, $mime ) {
-               return new self( false, false, $path, $mime );
-       }
-
-       /**
-        * @param $title
-        * @param $repo
-        * @return UnregisteredLocalFile
-        */
-       static function newFromTitle( $title, $repo ) {
-               return new self( $title, $repo, false, false );
-       }
-
-       /**
-        * Create an UnregisteredLocalFile based on a path or a (title,repo) pair.
-        * A FileRepo object is not required here, unlike most other File classes.
-        * 
-        * @throws MWException
-        * @param $title Title|false
-        * @param $repo FSRepo
-        * @param $path string
-        * @param $mime string
-        */
-       function __construct( $title = false, $repo = false, $path = false, $mime = false ) {
-               if ( !( $title && $repo ) && !$path ) {
-                       throw new MWException( __METHOD__.': not enough parameters, must specify title and repo, or a full path' );
-               }
-               if ( $title instanceof Title ) {
-                       $this->title = File::normalizeTitle( $title, 'exception' );
-                       $this->name = $repo->getNameFromTitle( $title );
-               } else {
-                       $this->name = basename( $path );
-                       $this->title = File::normalizeTitle( $this->name, 'exception' );
-               }
-               $this->repo = $repo;
-               if ( $path ) {
-                       $this->path = $path;
-               } else {
-                       $this->path = $repo->getRootDirectory() . '/' .
-                               $repo->getHashPath( $this->name ) . $this->name;
-               }
-               if ( $mime ) {
-                       $this->mime = $mime;
-               }
-               $this->dims = array();
-       }
-
-       private function cachePageDimensions( $page = 1 ) {
-               if ( !isset( $this->dims[$page] ) ) {
-                       if ( !$this->getHandler() ) {
-                               return false;
-                       }
-                       $this->dims[$page] = $this->handler->getPageDimensions( $this, $page );
-               }
-               return $this->dims[$page];
-       }
-
-       function getWidth( $page = 1 ) {
-               $dim = $this->cachePageDimensions( $page );
-               return $dim['width'];
-       }
-
-       function getHeight( $page = 1 ) {
-               $dim = $this->cachePageDimensions( $page );
-               return $dim['height'];
-       }
-
-       function getMimeType() {
-               if ( !isset( $this->mime ) ) {
-                       $magic = MimeMagic::singleton();
-                       $this->mime = $magic->guessMimeType( $this->getPath() );
-               }
-               return $this->mime;
-       }
-
-       function getImageSize( $filename ) {
-               if ( !$this->getHandler() ) {
-                       return false;
-               }
-               return $this->handler->getImageSize( $this, $this->getPath() );
-       }
-
-       function getMetadata() {
-               if ( !isset( $this->metadata ) ) {
-                       if ( !$this->getHandler() ) {
-                               $this->metadata = false;
-                       } else {
-                               $this->metadata = $this->handler->getMetadata( $this, $this->getPath() );
-                       }
-               }
-               return $this->metadata;
-       }
-
-       function getURL() {
-               if ( $this->repo ) {
-                       return $this->repo->getZoneUrl( 'public' ) . '/' .
-                               $this->repo->getHashPath( $this->name ) . rawurlencode( $this->name );
-               } else {
-                       return false;
-               }
-       }
-
-       function getSize() {
-               if ( file_exists( $this->path ) ) {
-                       return filesize( $this->path );
-               } else {
-                       return false;
-               }
-       }
-}
diff --git a/includes/filerepo/file/ArchivedFile.php b/includes/filerepo/file/ArchivedFile.php
new file mode 100644 (file)
index 0000000..34044ba
--- /dev/null
@@ -0,0 +1,471 @@
+<?php
+/**
+ * Deleted file in the 'filearchive' table
+ *
+ * @file
+ * @ingroup FileRepo
+ */
+
+/**
+ * Class representing a row of the 'filearchive' table
+ *
+ * @ingroup FileRepo
+ */
+class ArchivedFile {
+       /**#@+
+        * @private
+        */
+       var $id, # filearchive row ID
+               $name, # image name
+               $group, # FileStore storage group
+               $key, # FileStore sha1 key
+               $size, # file dimensions
+               $bits,  # size in bytes
+               $width, # width
+               $height, # height
+               $metadata, # metadata string
+               $mime, # mime type
+               $media_type, # media type
+               $description, # upload description
+               $user, # user ID of uploader
+               $user_text, # user name of uploader
+               $timestamp, # time of upload
+               $dataLoaded, # Whether or not all this has been loaded from the database (loadFromXxx)
+               $deleted, # Bitfield akin to rev_deleted
+               $pageCount,
+               $archive_name;
+
+       /**
+        * @var MediaHandler
+        */
+       var $handler;
+       /**
+        * @var Title
+        */
+       var $title; # image title
+
+       /**#@-*/
+
+       /**
+        * @throws MWException
+        * @param Title $title
+        * @param int $id
+        * @param string $key
+        */
+       function __construct( $title, $id=0, $key='' ) {
+               $this->id = -1;
+               $this->title = false;
+               $this->name = false;
+               $this->group = 'deleted'; // needed for direct use of constructor
+               $this->key = '';
+               $this->size = 0;
+               $this->bits = 0;
+               $this->width = 0;
+               $this->height = 0;
+               $this->metadata = '';
+               $this->mime = "unknown/unknown";
+               $this->media_type = '';
+               $this->description = '';
+               $this->user = 0;
+               $this->user_text = '';
+               $this->timestamp = null;
+               $this->deleted = 0;
+               $this->dataLoaded = false;
+               $this->exists = false;
+
+               if( $title instanceof Title ) {
+                       $this->title = File::normalizeTitle( $title, 'exception' );
+                       $this->name = $title->getDBkey();
+               }
+
+               if ($id) {
+                       $this->id = $id;
+               }
+
+               if ($key) {
+                       $this->key = $key;
+               }
+
+               if ( !$id && !$key && !( $title instanceof Title ) ) {
+                       throw new MWException( "No specifications provided to ArchivedFile constructor." );
+               }
+       }
+
+       /**
+        * Loads a file object from the filearchive table
+        * @return true on success or null
+        */
+       public function load() {
+               if ( $this->dataLoaded ) {
+                       return true;
+               }
+               $conds = array();
+
+               if( $this->id > 0 ) {
+                       $conds['fa_id'] = $this->id;
+               }
+               if( $this->key ) {
+                       $conds['fa_storage_group'] = $this->group;
+                       $conds['fa_storage_key'] = $this->key;
+               }
+               if( $this->title ) {
+                       $conds['fa_name'] = $this->title->getDBkey();
+               }
+
+               if( !count($conds)) {
+                       throw new MWException( "No specific information for retrieving archived file" );
+               }
+
+               if( !$this->title || $this->title->getNamespace() == NS_FILE ) {
+                       $dbr = wfGetDB( DB_SLAVE );
+                       $res = $dbr->select( 'filearchive',
+                               array(
+                                       'fa_id',
+                                       'fa_name',
+                                       'fa_archive_name',
+                                       'fa_storage_key',
+                                       'fa_storage_group',
+                                       'fa_size',
+                                       'fa_bits',
+                                       'fa_width',
+                                       'fa_height',
+                                       'fa_metadata',
+                                       'fa_media_type',
+                                       'fa_major_mime',
+                                       'fa_minor_mime',
+                                       'fa_description',
+                                       'fa_user',
+                                       'fa_user_text',
+                                       'fa_timestamp',
+                                       'fa_deleted' ),
+                               $conds,
+                               __METHOD__,
+                               array( 'ORDER BY' => 'fa_timestamp DESC' ) );
+                       if ( $res == false || $dbr->numRows( $res ) == 0 ) {
+                       // this revision does not exist?
+                               return;
+                       }
+                       $ret = $dbr->resultObject( $res );
+                       $row = $ret->fetchObject();
+
+                       // initialize fields for filestore image object
+                       $this->id = intval($row->fa_id);
+                       $this->name = $row->fa_name;
+                       $this->archive_name = $row->fa_archive_name;
+                       $this->group = $row->fa_storage_group;
+                       $this->key = $row->fa_storage_key;
+                       $this->size = $row->fa_size;
+                       $this->bits = $row->fa_bits;
+                       $this->width = $row->fa_width;
+                       $this->height = $row->fa_height;
+                       $this->metadata = $row->fa_metadata;
+                       $this->mime = "$row->fa_major_mime/$row->fa_minor_mime";
+                       $this->media_type = $row->fa_media_type;
+                       $this->description = $row->fa_description;
+                       $this->user = $row->fa_user;
+                       $this->user_text = $row->fa_user_text;
+                       $this->timestamp = $row->fa_timestamp;
+                       $this->deleted = $row->fa_deleted;
+               } else {
+                       throw new MWException( 'This title does not correspond to an image page.' );
+               }
+               $this->dataLoaded = true;
+               $this->exists = true;
+
+               return true;
+       }
+
+       /**
+        * Loads a file object from the filearchive table
+        *
+        * @param $row
+        *
+        * @return ArchivedFile
+        */
+       public static function newFromRow( $row ) {
+               $file = new ArchivedFile( Title::makeTitle( NS_FILE, $row->fa_name ) );
+
+               $file->id = intval($row->fa_id);
+               $file->name = $row->fa_name;
+               $file->archive_name = $row->fa_archive_name;
+               $file->group = $row->fa_storage_group;
+               $file->key = $row->fa_storage_key;
+               $file->size = $row->fa_size;
+               $file->bits = $row->fa_bits;
+               $file->width = $row->fa_width;
+               $file->height = $row->fa_height;
+               $file->metadata = $row->fa_metadata;
+               $file->mime = "$row->fa_major_mime/$row->fa_minor_mime";
+               $file->media_type = $row->fa_media_type;
+               $file->description = $row->fa_description;
+               $file->user = $row->fa_user;
+               $file->user_text = $row->fa_user_text;
+               $file->timestamp = $row->fa_timestamp;
+               $file->deleted = $row->fa_deleted;
+
+               return $file;
+       }
+
+       /**
+        * Return the associated title object
+        *
+        * @return Title
+        */
+       public function getTitle() {
+               return $this->title;
+       }
+
+       /**
+        * Return the file name
+        *
+        * @return string
+        */
+       public function getName() {
+               return $this->name;
+       }
+
+       /**
+        * @return int
+        */
+       public function getID() {
+               $this->load();
+               return $this->id;
+       }
+
+       /**
+        * @return bool
+        */
+       public function exists() {
+               $this->load();
+               return $this->exists;
+       }
+
+       /**
+        * Return the FileStore key
+        * @return string
+        */
+       public function getKey() {
+               $this->load();
+               return $this->key;
+       }
+
+       /**
+        * Return the FileStore key (overriding base File class)
+        * @return string
+        */
+       public function getStorageKey() {
+               return $this->getKey();
+       }
+
+       /**
+        * Return the FileStore storage group
+        * @return string
+        */
+       public function getGroup() {
+               return $this->group;
+       }
+
+       /**
+        * Return the width of the image
+        * @return int
+        */
+       public function getWidth() {
+               $this->load();
+               return $this->width;
+       }
+
+       /**
+        * Return the height of the image
+        * @return int
+        */
+       public function getHeight() {
+               $this->load();
+               return $this->height;
+       }
+
+       /**
+        * Get handler-specific metadata
+        * @return string
+        */
+       public function getMetadata() {
+               $this->load();
+               return $this->metadata;
+       }
+
+       /**
+        * Return the size of the image file, in bytes
+        * @return int
+        */
+       public function getSize() {
+               $this->load();
+               return $this->size;
+       }
+
+       /**
+        * Return the bits of the image file, in bytes
+        * @return int
+        */
+       public function getBits() {
+               $this->load();
+               return $this->bits;
+       }
+
+       /**
+        * Returns the mime type of the file.
+        * @return string
+        */
+       public function getMimeType() {
+               $this->load();
+               return $this->mime;
+       }
+
+       /**
+        * Get a MediaHandler instance for this file
+        * @return MediaHandler
+        */
+       function getHandler() {
+               if ( !isset( $this->handler ) ) {
+                       $this->handler = MediaHandler::getHandler( $this->getMimeType() );
+               }
+               return $this->handler;
+       }
+
+       /**
+        * Returns the number of pages of a multipage document, or false for
+        * documents which aren't multipage documents
+        */
+       function pageCount() {
+               if ( !isset( $this->pageCount ) ) {
+                       if ( $this->getHandler() && $this->handler->isMultiPage( $this ) ) {
+                               $this->pageCount = $this->handler->pageCount( $this );
+                       } else {
+                               $this->pageCount = false;
+                       }
+               }
+               return $this->pageCount;
+       }
+
+       /**
+        * Return the type of the media in the file.
+        * Use the value returned by this function with the MEDIATYPE_xxx constants.
+        * @return string
+        */
+       public function getMediaType() {
+               $this->load();
+               return $this->media_type;
+       }
+
+       /**
+        * Return upload timestamp.
+        *
+        * @return string
+        */
+       public function getTimestamp() {
+               $this->load();
+               return wfTimestamp( TS_MW, $this->timestamp );
+       }
+
+       /**
+        * Return the user ID of the uploader.
+        *
+        * @return int
+        */
+       public function getUser() {
+               $this->load();
+               if( $this->isDeleted( File::DELETED_USER ) ) {
+                       return 0;
+               } else {
+                       return $this->user;
+               }
+       }
+
+       /**
+        * Return the user name of the uploader.
+        *
+        * @return string
+        */
+       public function getUserText() {
+               $this->load();
+               if( $this->isDeleted( File::DELETED_USER ) ) {
+                       return 0;
+               } else {
+                       return $this->user_text;
+               }
+       }
+
+       /**
+        * Return upload description.
+        *
+        * @return string
+        */
+       public function getDescription() {
+               $this->load();
+               if( $this->isDeleted( File::DELETED_COMMENT ) ) {
+                       return 0;
+               } else {
+                       return $this->description;
+               }
+       }
+
+       /**
+        * Return the user ID of the uploader.
+        *
+        * @return int
+        */
+       public function getRawUser() {
+               $this->load();
+               return $this->user;
+       }
+
+       /**
+        * Return the user name of the uploader.
+        *
+        * @return string
+        */
+       public function getRawUserText() {
+               $this->load();
+               return $this->user_text;
+       }
+
+       /**
+        * Return upload description.
+        *
+        * @return string
+        */
+       public function getRawDescription() {
+               $this->load();
+               return $this->description;
+       }
+
+       /**
+        * Returns the deletion bitfield
+        * @return int
+        */
+       public function getVisibility() {
+               $this->load();
+               return $this->deleted;
+       }
+
+       /**
+        * for file or revision rows
+        *
+        * @param $field Integer: one of DELETED_* bitfield constants
+        * @return bool
+        */
+       public function isDeleted( $field ) {
+               $this->load();
+               return ($this->deleted & $field) == $field;
+       }
+
+       /**
+        * Determine if the current user is allowed to view a particular
+        * field of this FileStore image file, if it's marked as deleted.
+        * @param $field Integer
+        * @param $user User object to check, or null to use $wgUser
+        * @return bool
+        */
+       public function userCan( $field, User $user = null ) {
+               $this->load();
+               return Revision::userCanBitfield( $this->deleted, $field, $user );
+       }
+}
diff --git a/includes/filerepo/file/File.php b/includes/filerepo/file/File.php
new file mode 100644 (file)
index 0000000..8d12ef8
--- /dev/null
@@ -0,0 +1,1683 @@
+<?php
+/**
+ * Base code for files.
+ *
+ * @file
+ * @ingroup FileRepo
+ */
+
+/**
+ * Implements some public methods and some protected utility functions which
+ * are required by multiple child classes. Contains stub functionality for
+ * unimplemented public methods.
+ *
+ * Stub functions which should be overridden are marked with STUB. Some more
+ * concrete functions are also typically overridden by child classes.
+ *
+ * Note that only the repo object knows what its file class is called. You should
+ * never name a file class explictly outside of the repo class. Instead use the
+ * repo's factory functions to generate file objects, for example:
+ *
+ * RepoGroup::singleton()->getLocalRepo()->newFile($title);
+ *
+ * The convenience functions wfLocalFile() and wfFindFile() should be sufficient
+ * in most cases.
+ *
+ * @ingroup FileRepo
+ */
+abstract class File {
+       const DELETED_FILE = 1;
+       const DELETED_COMMENT = 2;
+       const DELETED_USER = 4;
+       const DELETED_RESTRICTED = 8;
+
+       /** 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;
+
+       /**
+        * Some member variables can be lazy-initialised using __get(). The
+        * initialisation function for these variables is always a function named
+        * like getVar(), where Var is the variable name with upper-case first
+        * letter.
+        *
+        * The following variables are initialised in this way in this base class:
+        *    name, extension, handler, path, canRender, isSafeFile,
+        *    transformScript, hashPath, pageCount, url
+        *
+        * Code within this class should generally use the accessor function
+        * directly, since __get() isn't re-entrant and therefore causes bugs that
+        * depend on initialisation order.
+        */
+
+       /**
+        * The following member variables are not lazy-initialised
+        */
+
+       /**
+        * @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 ) ) {
+                       return null;
+               } else {
+                       $this->$name = call_user_func( $function );
+                       return $this->$name;
+               }
+       }
+
+       /**
+        * Normalize a file extension to the common form, and ensure it's clean.
+        * Extensions with non-alphanumeric characters will be discarded.
+        *
+        * @param $ext string (without the .)
+        * @return string
+        */
+       static function normalizeExtension( $ext ) {
+               $lower = strtolower( $ext );
+               $squish = array(
+                       'htm' => 'html',
+                       'jpeg' => 'jpg',
+                       'mpeg' => 'mpg',
+                       'tiff' => 'tif',
+                       'ogv' => 'ogg' );
+               if( isset( $squish[$lower] ) ) {
+                       return $squish[$lower];
+               } elseif( preg_match( '/^[0-9a-z]+$/', $lower ) ) {
+                       return $lower;
+               } else {
+                       return '';
+               }
+       }
+
+       /**
+        * Checks if file extensions are compatible
+        *
+        * @param $old File Old file
+        * @param $new string New name
+        *
+        * @return bool|null
+        */
+       static function checkExtensionCompatibility( File $old, $new ) {
+               $oldMime = $old->getMimeType();
+               $n = strrpos( $new, '.' );
+               $newExt = self::normalizeExtension(
+                       $n ? substr( $new, $n + 1 ) : '' );
+               $mimeMagic = MimeMagic::singleton();
+               return $mimeMagic->isMatchingExtension( $newExt, $oldMime );
+       }
+
+       /**
+        * Upgrade the database row if there is one
+        * Called by ImagePage
+        * STUB
+        */
+       function upgradeRow() {}
+
+       /**
+        * Split an internet media type into its two components; if not
+        * a two-part name, set the minor type to 'unknown'.
+        *
+        * @param string $mime "text/html" etc
+        * @return array ("text", "html") etc
+        */
+       public static function splitMime( $mime ) {
+               if( strpos( $mime, '/' ) !== false ) {
+                       return explode( '/', $mime, 2 );
+               } else {
+                       return array( $mime, 'unknown' );
+               }
+       }
+
+       /**
+        * 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;
+       }
+
+       /**
+        * Get the file extension, e.g. "svg"
+        *
+        * @return string
+        */
+       function getExtension() {
+               if ( !isset( $this->extension ) ) {
+                       $n = strrpos( $this->getName(), '.' );
+                       $this->extension = self::normalizeExtension(
+                               $n ? substr( $this->getName(), $n + 1 ) : '' );
+               }
+               return $this->extension;
+       }
+
+       /**
+        * 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 ) {
+                       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;
+       }
+
+       /**
+        * 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
+        */
+       public function getFullUrl() {
+               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 {
+                               wfDebug(__METHOD__.': supposed to render '.$this->getName().' ('.$this->getMimeType()."), but can't!\n");
+                               return $this->getURL(); #hm... return NULL?
+                       }
+               } else {
+                       return $this->getURL();
+               }
+       }
+
+       /**
+       * Return the full filesystem path to the file. Note that this does
+       * not mean that a file actually exists under that location.
+       *
+       * This path depends on whether directory hashing is active or not,
+       * i.e. whether the files are all found in the same directory,
+       * or in hashed paths like /images/3/3c.
+       *
+       * Most callers don't check the return value, but ForeignAPIFile::getPath
+       * returns false.
+        *
+        * @return string|false
+       */
+       public function getPath() {
+               if ( !isset( $this->path ) ) {
+                       $this->assertRepoDefined();
+                       $this->path = $this->repo->getZonePath( 'public' ) . '/' . $this->getRel();
+               }
+               return $this->path;
+       }
+
+       /**
+        * 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;
+       }
+
+       /**
+        * Return the height of the image. Returns false if the height is unknown
+        * or undefined
+        *
+        * STUB
+        * Overridden by LocalFile, UnregisteredLocalFile
+        *
+        * @param $page int
+        *
+        * @return false|number
+        */
+       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;
+       }
+
+       /**
+        * Get the duration of a media file in seconds
+        *
+        * @return number
+        */
+       public function getLength() {
+               $handler = $this->getHandler();
+               if ( $handler ) {
+                       return $handler->getLength( $this );
+               } else {
+                       return 0;
+               }
+       }
+
+       /**
+        * 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;
+       }
+
+       /**
+       * 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;
+       }
+
+       /**
+        * Return the size of the image file, in bytes
+        * Overridden by LocalFile, UnregisteredLocalFile
+        * STUB
+        */
+       public function getSize() {
+               return false;
+       }
+
+       /**
+        * Returns the mime type of the file.
+        * Overridden by LocalFile, UnregisteredLocalFile
+        * STUB
+        *
+        * @return string
+        */
+       function getMimeType() {
+               return 'unknown/unknown';
+       }
+
+       /**
+        * Return the type of the media in the file.
+        * Use the value returned by this function with the MEDIATYPE_xxx constants.
+        * Overridden by LocalFile,
+        * STUB
+        */
+       function getMediaType() { return MEDIATYPE_UNKNOWN; }
+
+       /**
+        * Checks if the output of transform() for this file is likely
+        * to be valid. If this is false, various user elements will
+        * display a placeholder instead.
+        *
+        * Currently, this checks if the file is an image format
+        * 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 ) ) {
+                       $this->canRender = $this->getHandler() && $this->handler->canRender( $this );
+               }
+               return $this->canRender;
+       }
+
+       /**
+        * Accessor for __get()
+        */
+       protected function getCanRender() {
+               return $this->canRender();
+       }
+
+       /**
+        * Return true if the file is of a type that can't be directly
+        * rendered by typical browsers and needs to be re-rasterized.
+        *
+        * This returns true for everything but the bitmap types
+        * supported by all browsers, i.e. JPEG; GIF and PNG. It will
+        * also return true for any non-image formats.
+        *
+        * @return bool
+        */
+       function mustRender() {
+               return $this->getHandler() && $this->handler->mustRender( $this );
+       }
+
+       /**
+        * Alias for canRender()
+        *
+        * @return bool
+        */
+       function allowInlineDisplay() {
+               return $this->canRender();
+       }
+
+       /**
+        * Determines if this media file is in a format that is unlikely to
+        * contain viruses or malicious content. It uses the global
+        * $wgTrustedMediaFormats list to determine if the file is safe.
+        *
+        * This is used to show a warning on the description page of non-safe files.
+        * It may also be used to disallow direct [[media:...]] links to such files.
+        *
+        * 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 ) ) {
+                       $this->isSafeFile = $this->_getIsSafeFile();
+               }
+               return $this->isSafeFile;
+       }
+
+       /**
+        * Accessor for __get()
+        *
+        * @return bool
+        */
+       protected function getIsSafeFile() {
+               return $this->isSafeFile();
+       }
+
+       /**
+        * Uncached accessor
+        *
+        * @return bool
+        */
+       protected function _getIsSafeFile() {
+               if ( $this->allowInlineDisplay() ) {
+                       return true;
+               }
+               if ($this->isTrustedFile()) {
+                       return true;
+               }
+
+               global $wgTrustedMediaFormats;
+
+               $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 ( $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.
+        *
+        * @return bool
+        */
+       function isTrustedFile() {
+               #this could be implemented to check a flag in the databas,
+               #look for signatures, etc
+               return false;
+       }
+
+       /**
+        * Returns true if file exists in the repository.
+        *
+        * Overridden by LocalFile to avoid unnecessary stat calls.
+        *
+        * @return boolean Whether file exists in the repository.
+        */
+       public function exists() {
+               return $this->getPath() && file_exists( $this->path );
+       }
+
+       /**
+        * Returns true if file exists in the repository and can be included in a page.
+        * 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() {
+               return $this->exists();
+       }
+
+       /**
+        * @return string
+        */
+       function getTransformScript() {
+               if ( !isset( $this->transformScript ) ) {
+                       $this->transformScript = false;
+                       if ( $this->repo ) {
+                               $script = $this->repo->getThumbScriptUrl();
+                               if ( $script ) {
+                                       $this->transformScript = "$script?f=" . urlencode( $this->getName() );
+                               }
+                       }
+               }
+               return $this->transformScript;
+       }
+
+       /**
+        * Get a ThumbnailImage which is the same size as the source
+        *
+        * @param $handlerParams array
+        *
+        * @return string
+        */
+       function getUnscaledThumb( $handlerParams = array() ) {
+               $hp =& $handlerParams;
+               $page = isset( $hp['page'] ) ? $hp['page'] : false;
+               $width = $this->getWidth( $page );
+               if ( !$width ) {
+                       return $this->iconThumb();
+               }
+               $hp['width'] = $width;
+               return $this->transform( $hp );
+       }
+
+       /**
+        * Return the file name of a thumbnail with the specified 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(), $params );
+               $thumbName = $this->handler->makeParamString( $params ) . '-' . $name;
+               if ( $thumbExt != $extension ) {
+                       $thumbName .= ".$thumbExt";
+               }
+               return $thumbName;
+       }
+
+       /**
+        * Create a thumbnail of the image having the specified width/height.
+        * The thumbnail will not be created if the width is larger than the
+        * image's width. Let the browser do the scaling in this case.
+        * The thumbnail is stored on disk and is only computed if the thumbnail
+        * file does not exist OR if it is older than the image.
+        * Returns the URL.
+        *
+        * Keeps aspect ratio of original image. If both width and height are
+        * specified, the generated image will be no bigger than width x height,
+        * and will also have correct aspect ratio.
+        *
+        * @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 );
+               if ( $height != -1 ) {
+                       $params['height'] = $height;
+               }
+               $thumb = $this->transform( $params );
+               if( is_null( $thumb ) || $thumb->isError() ) return '';
+               return $thumb->getUrl();
+       }
+
+       /**
+        * Do the work of a transform (from an original into a thumb).
+        * Contains filesystem-specific functions.
+        *
+        * @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
+        *
+        * @return MediaTransformOutput | false
+        */
+       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 );
+               }
+
+               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 $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;
+
+               wfProfileIn( __METHOD__ );
+               do {
+                       if ( !$this->canRender() ) {
+                               // not a bitmap or renderable image, don't try.
+                               $thumb = $this->iconThumb();
+                               break;
+                       }
+
+                       // Get the descriptionUrl to embed it as comment into the thumbnail. Bug 19791.
+                       $descriptionUrl =  $this->getDescriptionUrl();
+                       if ( $descriptionUrl ) {
+                               $params['descriptionUrl'] = wfExpandUrl( $descriptionUrl, PROTO_CANONICAL );
+                       }
+
+                       $script = $this->getTransformScript();
+                       if ( $script && !($flags & self::RENDER_NOW) ) {
+                               // Use a script to transform on client request, if possible
+                               $thumb = $this->handler->getScriptedTransform( $this, $script, $params );
+                               if( $thumb ) {
+                                       break;
+                               }
+                       }
+
+                       $normalisedParams = $params;
+                       $this->handler->normaliseParams( $this, $normalisedParams );
+                       $thumbName = $this->thumbName( $normalisedParams );
+                       $thumbUrl = $this->getThumbUrl( $thumbName );
+
+                       $thumb = $this->maybeDoTransform( $thumbName, $thumbUrl, $params, $flags );
+
+                       // 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 ) );
+                       }
+               } while (false);
+
+               wfProfileOut( __METHOD__ );
+               return is_object( $thumb ) ? $thumb : false;
+       }
+
+       /**
+        * Hook into transform() to allow migration of thumbnail files
+        * STUB
+        * Overridden by LocalFile
+        */
+       function migrateThumbFile( $thumbName ) {}
+
+       /**
+        * Get a MediaHandler instance for this file
+        * @return MediaHandler
+        */
+       function getHandler() {
+               if ( !isset( $this->handler ) ) {
+                       $this->handler = MediaHandler::getHandler( $this->getMimeType() );
+               }
+               return $this->handler;
+       }
+
+       /**
+        * Get a ThumbnailImage representing a file type icon
+        * @return ThumbnailImage
+        */
+       function iconThumb() {
+               global $wgStylePath, $wgStyleDirectory;
+
+               $try = array( 'fileicon-' . $this->getExtension() . '.png', 'fileicon.png' );
+               foreach( $try as $icon ) {
+                       $path = '/common/images/icons/' . $icon;
+                       $filepath = $wgStyleDirectory . $path;
+                       if( file_exists( $filepath ) ) {
+                               return new ThumbnailImage( $this, $wgStylePath . $path, 120, 120 );
+                       }
+               }
+               return null;
+       }
+
+       /**
+        * Get last thumbnailing error.
+        * Largely obsolete.
+        */
+       function getLastError() {
+               return $this->lastError;
+       }
+
+       /**
+        * Get all thumbnail names previously generated for this file
+        * STUB
+        * Overridden by LocalFile
+        */
+       function getThumbnails() {
+               return array();
+       }
+
+       /**
+        * Purge shared caches such as thumbnails and DB data caching
+        * STUB
+        * Overridden by LocalFile
+        */
+       function purgeCache() {}
+
+       /**
+        * Purge the file description page, but don't go after
+        * pages using the file. Use when modifying file history
+        * but not the current data.
+        */
+       function purgeDescription() {
+               $title = $this->getTitle();
+               if ( $title ) {
+                       $title->invalidateCache();
+                       $title->purgeSquid();
+               }
+       }
+
+       /**
+        * Purge metadata and all affected pages when the file is created,
+        * deleted, or majorly updated.
+        */
+       function purgeEverything() {
+               // Delete thumbnails and refresh file metadata cache
+               $this->purgeCache();
+               $this->purgeDescription();
+
+               // Purge cache of all pages using this file
+               $title = $this->getTitle();
+               if ( $title ) {
+                       $update = new HTMLCacheUpdate( $title, 'imagelinks' );
+                       $update->doUpdate();
+               }
+       }
+
+       /**
+        * Return a fragment of the history of file.
+        *
+        * STUB
+        * @param $limit integer Limit of rows to return
+        * @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();
+       }
+
+       /**
+        * Return the history of this file, line by line. Starts with current version,
+        * then old versions. Should return an object similar to an image/oldimage
+        * database row.
+        *
+        * STUB
+        * Overridden in LocalFile
+        */
+       public function nextHistoryLine() {
+               return false;
+       }
+
+       /**
+        * Reset the history pointer to the first element of the history.
+        * Always call this function after using nextHistoryLine() to free db resources
+        * STUB
+        * Overridden in LocalFile.
+        */
+       public function resetHistory() {}
+
+       /**
+        * 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;
+       }
+
+       /**
+        * Get the path of the file relative to the public zone root
+        *
+        * @return string
+        */
+       function getRel() {
+               return $this->getHashPath() . $this->getName();
+       }
+
+       /**
+        * Get urlencoded relative path of the file
+        *
+        * @return string
+        */
+       function getUrlRel() {
+               return $this->getHashPath() . rawurlencode( $this->getName() );
+       }
+
+       /**
+        * 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 ) {
+                       $path = substr( $path, 0, -1 );
+               } else {
+                       $path .= $suffix;
+               }
+               return $path;
+       }
+
+       /**
+        * 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 ) {
+               $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
+        *
+        * @param $suffix bool|string if not false, the name of a thumbnail file
+        *
+        * @return string
+        */
+       function getThumbPath( $suffix = false ) {
+               $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
+        *
+        * @param $suffix bool|string if not false, the name of an archived file
+        *
+        * @return string
+        */
+       function getArchiveUrl( $suffix = false ) {
+               $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 {
+                       $path .= rawurlencode( $suffix );
+               }
+               return $path;
+       }
+
+       /**
+        * 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 );
+               }
+               return $path;
+       }
+
+       /**
+        * 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 );
+               } else {
+                       $path .= rawurlencode( $suffix );
+               }
+               return $path;
+       }
+
+       /**
+        * 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 );
+               }
+               return $path;
+       }
+
+       /**
+        * 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 );
+               }
+               return $path;
+       }
+
+       /**
+        * @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' );
+       }
+
+       /**
+        * 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();
+       }
+
+       /**
+        * Move or copy a file to its public location. If a file exists at the
+        * 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 $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 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
+        */
+       function publish( $srcPath, $flags = 0 ) {
+               $this->readOnlyError();
+       }
+
+       /**
+        * @return bool
+        */
+       function formatMetadata() {
+               if ( !$this->getHandler() ) {
+                       return false;
+               }
+               return $this->getHandler()->formatMetadata( $this, $this->getMetadata() );
+       }
+
+       /**
+        * Returns true if the file comes from the local file repository.
+        *
+        * @return bool
+        */
+       function isLocal() {
+               $repo = $this->getRepo();
+               return $repo && $repo->isLocal();
+       }
+
+       /**
+        * Returns the name of the repository.
+        *
+        * @return string
+        */
+       function getRepoName() {
+               return $this->repo ? $this->repo->getName() : 'unknown';
+       }
+
+       /**
+        * Returns the repository
+        *
+        * @return FileRepo|false
+        */
+       function getRepo() {
+               return $this->repo;
+       }
+
+       /**
+        * Returns true if the image is an old version
+        * STUB
+        *
+        * @return bool
+        */
+       function isOld() {
+               return false;
+       }
+
+       /**
+        * 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;
+       }
+
+       /**
+        * Was this file ever deleted from the wiki?
+        *
+        * @return bool
+        */
+       function wasDeleted() {
+               $title = $this->getTitle();
+               return $title && $title->isDeletedQuick();
+       }
+
+       /**
+        * Move file to the new title
+        *
+        * Move current, old version and all thumbnails
+        * to the new filename. Old file is deleted.
+        *
+        * Cache purging is done; checks for validity
+        * and logging are caller's responsibility
+        *
+        * @param $target Title New file name
+        * @return FileRepoStatus object.
+        */
+        function move( $target ) {
+               $this->readOnlyError();
+        }
+
+       /**
+        * Delete all versions of the file.
+        *
+        * Moves the files into an archive directory (or deletes them)
+        * and removes the database rows.
+        *
+        * Cache purging is done; logging is caller's responsibility.
+        *
+        * @param $reason String
+        * @param $suppress Boolean: hide content from sysops?
+        * @return true on success, false on some kind of failure
+        * STUB
+        * Overridden by LocalFile
+        */
+       function delete( $reason, $suppress = false ) {
+               $this->readOnlyError();
+       }
+
+       /**
+        * Restore all or specified deleted revisions to the given file.
+        * Permissions and logging are left to the caller.
+        *
+        * May throw database exceptions on error.
+        *
+        * @param $versions array set of record ids of deleted items to restore,
+        *                    or empty to restore all revisions.
+        * @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 ) {
+               $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
+        * question only has a single page.
+        *
+        * @return Bool
+        */
+       function isMultipage() {
+               return $this->getHandler() && $this->handler->isMultiPage( $this );
+       }
+
+       /**
+        * 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 ) ) {
+                       if ( $this->getHandler() && $this->handler->isMultiPage( $this ) ) {
+                               $this->pageCount = $this->handler->pageCount( $this );
+                       } else {
+                               $this->pageCount = false;
+                       }
+               }
+               return $this->pageCount;
+       }
+
+       /**
+        * 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
+               if ( $srcWidth == 0 ) {
+                       return 0;
+               } else {
+                       return round( $srcHeight * $dstWidth / $srcWidth );
+               }
+       }
+
+       /**
+        * Get an image size array like that returned by getImageSize(), or false if it
+        * can't be determined.
+        *
+        * @param $fileName String: The filename
+        * @return Array
+        */
+       function getImageSize( $fileName ) {
+               if ( !$this->getHandler() ) {
+                       return false;
+               }
+               return $this->handler->getImageSize( $this, $fileName );
+       }
+
+       /**
+        * Get the URL of the image description page. May return false if it is
+        * unknown or not applicable.
+        *
+        * @return string
+        */
+       function getDescriptionUrl() {
+               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 || !$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(),
+                                                                       $this->getName() );
+                               $obj = $wgMemc->get($key);
+                               if ($obj) {
+                                       wfDebug("success!\n");
+                                       return $obj;
+                               }
+                               wfDebug("miss\n");
+                       }
+                       wfDebug( "Fetching shared description from $renderUrl\n" );
+                       $res = Http::get( $renderUrl );
+                       if ( $res && $this->repo->descriptionCacheExpiry > 0 ) {
+                               $wgMemc->set( $key, $res, $this->repo->descriptionCacheExpiry );
+                       }
+                       return $res;
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * Get discription of file revision
+        * STUB
+        *
+        * @return string
+        */
+       function getDescription() {
+               return null;
+       }
+
+       /**
+        * Get the 14-character timestamp of the file upload, or false if
+        * it doesn't exist
+        *
+        * @return string
+        */
+       function getTimestamp() {
+               $path = $this->getPath();
+               if ( !file_exists( $path ) ) {
+                       return false;
+               }
+               return wfTimestamp( TS_MW, filemtime( $path ) );
+       }
+
+       /**
+        * Get the SHA-1 base 36 hash of the file
+        *
+        * @return string
+        */
+       function getSha1() {
+               return self::sha1Base36( $this->getPath() );
+       }
+
+       /**
+        * Get the deletion archive key, <sha1>.<ext>
+        *
+        * @return string
+        */
+       function getStorageKey() {
+               $hash = $this->getSha1();
+               if ( !$hash ) {
+                       return false;
+               }
+               $ext = $this->getExtension();
+               $dotExt = $ext === '' ? '' : ".$ext";
+               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 $field Integer
+        * @param $user User object to check, or null to use $wgUser
+        * @return Boolean
+        */
+       function userCan( $field, User $user = null ) {
+               return true;
+       }
+
+       /**
+        * Get an associative array containing information about a file in the local filesystem.
+        *
+        * @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__ );
+               wfDebug( __METHOD__.": Getting file info for $path\n" );
+               $info = array(
+                       'fileExists' => file_exists( $path ) && !is_dir( $path )
+               );
+               $gis = false;
+               if ( $info['fileExists'] ) {
+                       $magic = MimeMagic::singleton();
+
+                       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'] );
+
+                       # Get size in bytes
+                       $info['size'] = filesize( $path );
+
+                       # Height, width and metadata
+                       $handler = MediaHandler::getHandler( $info['mime'] );
+                       if ( $handler ) {
+                               $tempImage = (object)array();
+                               $info['metadata'] = $handler->getMetadata( $tempImage, $path );
+                               $gis = $handler->getImageSize( $tempImage, $path, $info['metadata'] );
+                       } else {
+                               $gis = false;
+                               $info['metadata'] = '';
+                       }
+                       $info['sha1'] = self::sha1Base36( $path );
+
+                       wfDebug(__METHOD__.": $path loaded, {$info['size']} bytes, {$info['mime']}.\n");
+               } else {
+                       $info['mime'] = null;
+                       $info['media_type'] = MEDIATYPE_UNKNOWN;
+                       $info['metadata'] = '';
+                       $info['sha1'] = '';
+                       wfDebug(__METHOD__.": $path NOT FOUND!\n");
+               }
+               if( $gis ) {
+                       # NOTE: $gis[2] contains a code for the image type. This is no longer used.
+                       $info['width'] = $gis[0];
+                       $info['height'] = $gis[1];
+                       if ( isset( $gis['bits'] ) ) {
+                               $info['bits'] = $gis['bits'];
+                       } else {
+                               $info['bits'] = 0;
+                       }
+               } else {
+                       $info['width'] = 0;
+                       $info['height'] = 0;
+                       $info['bits'] = 0;
+               }
+               wfProfileOut( __METHOD__ );
+               return $info;
+       }
+
+       /**
+        * Get a SHA-1 hash of a file in the local filesystem, in base-36 lower case
+        * encoding, zero padded to 31 digits.
+        *
+        * 160 log 2 / log 36 = 30.95, so the 160-bit hash fills 31 digits in base 36
+        * fairly neatly.
+        *
+        * @param $path string
+        *
+        * @return false|string False on failure
+        */
+       static function sha1Base36( $path ) {
+               wfSuppressWarnings();
+               $hash = sha1_file( $path );
+               wfRestoreWarnings();
+               if ( $hash === false ) {
+                       return false;
+               } else {
+                       return wfBaseConvert( $hash, 16, 36, 31 );
+               }
+       }
+
+       /**
+        * @return string
+        */
+       function getLongDesc() {
+               $handler = $this->getHandler();
+               if ( $handler ) {
+                       return $handler->getLongDesc( $this );
+               } else {
+                       return MediaHandler::getGeneralLongDesc( $this );
+               }
+       }
+
+       /**
+        * @return string
+        */
+       function getShortDesc() {
+               $handler = $this->getHandler();
+               if ( $handler ) {
+                       return $handler->getShortDesc( $this );
+               } else {
+                       return MediaHandler::getGeneralShortDesc( $this );
+               }
+       }
+
+       /**
+        * @return string
+        */
+       function getDimensionsString() {
+               $handler = $this->getHandler();
+               if ( $handler ) {
+                       return $handler->getDimensionsString( $this );
+               } else {
+                       return '';
+               }
+       }
+
+       /**
+        * @return
+        */
+       function getRedirected() {
+               return $this->redirected;
+       }
+
+       /**
+        * @return Title
+        */
+       function getRedirectedTitle() {
+               if ( $this->redirected ) {
+                       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" );
+               }
+       }
+}
diff --git a/includes/filerepo/file/ForeignAPIFile.php b/includes/filerepo/file/ForeignAPIFile.php
new file mode 100644 (file)
index 0000000..9cd798d
--- /dev/null
@@ -0,0 +1,242 @@
+<?php
+/**
+ * Foreign file accessible through api.php requests.
+ *
+ * @file
+ * @ingroup FileRepo
+ */
+
+/**
+ * Foreign file accessible through api.php requests.
+ * Very hacky and inefficient, do not use :D
+ *
+ * @ingroup FileRepo
+ */
+class ForeignAPIFile extends File {
+
+       private $mExists;
+
+       protected $repoClass = 'ForeignApiRepo';
+
+       /**
+        * @param $title
+        * @param $repo ForeignApiRepo
+        * @param $info
+        * @param bool $exists
+        */
+       function __construct( $title, $repo, $info, $exists = false ) {
+               parent::__construct( $title, $repo );
+
+               $this->mInfo = $info;
+               $this->mExists = $exists;
+
+               $this->assertRepoDefined();
+       }
+
+       /**
+        * @param $title Title
+        * @param $repo ForeignApiRepo
+        * @return ForeignAPIFile|null
+        */
+       static function newFromTitle( Title $title, $repo ) {
+               $data = $repo->fetchImageQuery( array(
+                       'titles' => 'File:' . $title->getDBKey(),
+                       'iiprop' => self::getProps(),
+                       'prop'   => 'imageinfo',
+                       'iimetadataversion' => MediaHandler::getMetadataVersion()
+               ) );
+
+               $info = $repo->getImageInfo( $data );
+
+               if( $info ) {
+                       $lastRedirect = isset( $data['query']['redirects'] )
+                               ? count( $data['query']['redirects'] ) - 1
+                               : -1;
+                       if( $lastRedirect >= 0 ) {
+                               $newtitle = Title::newFromText( $data['query']['redirects'][$lastRedirect]['to']);
+                               $img = new self( $newtitle, $repo, $info, true );
+                               if( $img ) {
+                                       $img->redirectedFrom( $title->getDBkey() );
+                               }
+                       } else {
+                               $img = new self( $title, $repo, $info, true );
+                       }
+                       return $img;
+               } else {
+                       return null;
+               }
+       }
+
+       /**
+        * Get the property string for iiprop and aiprop
+        */
+       static function getProps() {
+               return 'timestamp|user|comment|url|size|sha1|metadata|mime';
+       }
+
+       // Dummy functions...
+       public function exists() {
+               return $this->mExists;
+       }
+
+       public function getPath() {
+               return false;
+       }
+
+       function transform( $params, $flags = 0 ) {
+               if( !$this->canRender() ) {
+                       // show icon
+                       return parent::transform( $params, $flags );
+               }
+
+               // Note, the this->canRender() check above implies
+               // that we have a handler, and it can do makeParamString.
+               $otherParams = $this->handler->makeParamString( $params );
+
+               $thumbUrl = $this->repo->getThumbUrlFromCache(
+                       $this->getName(),
+                       isset( $params['width'] ) ? $params['width'] : -1,
+                       isset( $params['height'] ) ? $params['height'] : -1,
+                       $otherParams );
+               return $this->handler->getTransform( $this, 'bogus', $thumbUrl, $params );
+       }
+
+       // Info we can get from API...
+       public function getWidth( $page = 1 ) {
+               return isset( $this->mInfo['width'] ) ? intval( $this->mInfo['width'] ) : 0;
+       }
+
+       /**
+        * @param $page int
+        * @return int
+        */
+       public function getHeight( $page = 1 ) {
+               return isset( $this->mInfo['height'] ) ? intval( $this->mInfo['height'] ) : 0;
+       }
+
+       public function getMetadata() {
+               if ( isset( $this->mInfo['metadata'] ) ) {
+                       return serialize( self::parseMetadata( $this->mInfo['metadata'] ) );
+               }
+               return null;
+       }
+
+       public static function parseMetadata( $metadata ) {
+               if( !is_array( $metadata ) ) {
+                       return $metadata;
+               }
+               $ret = array();
+               foreach( $metadata as $meta ) {
+                       $ret[ $meta['name'] ] = self::parseMetadata( $meta['value'] );
+               }
+               return $ret;
+       }
+
+       public function getSize() {
+               return isset( $this->mInfo['size'] ) ? intval( $this->mInfo['size'] ) : null;
+       }
+
+       public function getUrl() {
+               return isset( $this->mInfo['url'] ) ? strval( $this->mInfo['url'] ) : null;
+       }
+
+       public function getUser( $method='text' ) {
+               return isset( $this->mInfo['user'] ) ? strval( $this->mInfo['user'] ) : null;
+       }
+
+       public function getDescription() {
+               return isset( $this->mInfo['comment'] ) ? strval( $this->mInfo['comment'] ) : null;
+       }
+
+       function getSha1() {
+               return isset( $this->mInfo['sha1'] ) ?
+                       wfBaseConvert( strval( $this->mInfo['sha1'] ), 16, 36, 31 ) :
+                       null;
+       }
+
+       function getTimestamp() {
+               return wfTimestamp( TS_MW,
+                       isset( $this->mInfo['timestamp'] ) ?
+                       strval( $this->mInfo['timestamp'] ) :
+                       null
+               );
+       }
+
+       function getMimeType() {
+               if( !isset( $this->mInfo['mime'] ) ) {
+                       $magic = MimeMagic::singleton();
+                       $this->mInfo['mime'] = $magic->guessTypesForExtension( $this->getExtension() );
+               }
+               return $this->mInfo['mime'];
+       }
+
+       /// @todo FIXME: May guess wrong on file types that can be eg audio or video
+       function getMediaType() {
+               $magic = MimeMagic::singleton();
+               return $magic->getMediaType( null, $this->getMimeType() );
+       }
+
+       function getDescriptionUrl() {
+               return isset( $this->mInfo['descriptionurl'] )
+                       ? $this->mInfo['descriptionurl']
+                       : false;
+       }
+
+       /**
+        * Only useful if we're locally caching thumbs anyway...
+        */
+       function getThumbPath( $suffix = '' ) {
+               if ( $this->repo->canCacheThumbs() ) {
+                       $path = $this->repo->getZonePath('thumb') . '/' . $this->getHashPath( $this->getName() );
+                       if ( $suffix ) {
+                               $path = $path . $suffix . '/';
+                       }
+                       return $path;
+               } else {
+                       return null;
+               }
+       }
+
+       function getThumbnails() {
+               $files = array();
+               $dir = $this->getThumbPath( $this->getName() );
+               if ( is_dir( $dir ) ) {
+                       $handle = opendir( $dir );
+                       if ( $handle ) {
+                               while ( false !== ( $file = readdir($handle) ) ) {
+                                       if ( $file[0] != '.'  ) {
+                                               $files[] = $file;
+                                       }
+                               }
+                               closedir( $handle );
+                       }
+               }
+               return $files;
+       }
+
+       function purgeCache() {
+               $this->purgeThumbnails();
+               $this->purgeDescriptionPage();
+       }
+
+       function purgeDescriptionPage() {
+               global $wgMemc, $wgContLang;
+               $url = $this->repo->getDescriptionRenderUrl( $this->getName(), $wgContLang->getCode() );
+               $key = $this->repo->getLocalCacheKey( 'RemoteFileDescription', 'url', md5($url) );
+               $wgMemc->delete( $key );
+       }
+
+       function purgeThumbnails() {
+               global $wgMemc;
+               $key = $this->repo->getLocalCacheKey( 'ForeignAPIRepo', 'ThumbUrl', $this->getName() );
+               $wgMemc->delete( $key );
+               $files = $this->getThumbnails();
+               $dir = $this->getThumbPath( $this->getName() );
+               foreach ( $files as $file ) {
+                       unlink( $dir . $file );
+               }
+               if ( is_dir( $dir ) ) {
+                       rmdir( $dir ); // Might have already gone away, spews errors if we don't.
+               }
+       }
+}
diff --git a/includes/filerepo/file/ForeignDBFile.php b/includes/filerepo/file/ForeignDBFile.php
new file mode 100644 (file)
index 0000000..09bee39
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+/**
+ * Foreign file with an accessible MediaWiki database
+ *
+ * @file
+ * @ingroup FileRepo
+ */
+
+/**
+ * Foreign file with an accessible MediaWiki database
+ *
+ * @ingroup FileRepo
+ */
+class ForeignDBFile extends LocalFile {
+
+       /**
+        * @param $title
+        * @param $repo
+        * @param $unused
+        * @return ForeignDBFile
+        */
+       static function newFromTitle( $title, $repo, $unused = null ) {
+               return new self( $title, $repo );
+       }
+
+       /**
+        * Create a ForeignDBFile from a title
+        * Do not call this except from inside a repo class.
+        *
+        * @param $row
+        * @param $repo
+        *
+        * @return ForeignDBFile
+        */
+       static function newFromRow( $row, $repo ) {
+               $title = Title::makeTitle( NS_FILE, $row->img_name );
+               $file = new self( $title, $repo );
+               $file->loadFromRow( $row );
+               return $file;
+       }
+
+       function publish( $srcPath, $flags = 0 ) {
+               $this->readOnlyError();
+       }
+
+       function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
+               $watch = false, $timestamp = false ) {
+               $this->readOnlyError();
+       }
+
+       function restore( $versions = array(), $unsuppress = false ) {
+               $this->readOnlyError();
+       }
+
+       function delete( $reason, $suppress = false ) {
+               $this->readOnlyError();
+       }
+
+       function move( $target ) {
+               $this->readOnlyError();
+       }
+
+       /**
+        * @return string
+        */
+       function getDescriptionUrl() {
+               // Restore remote behaviour
+               return File::getDescriptionUrl();
+       }
+
+       /**
+        * @return string
+        */
+       function getDescriptionText() {
+               // Restore remote behaviour
+               return File::getDescriptionText();
+       }
+}
diff --git a/includes/filerepo/file/LocalFile.php b/includes/filerepo/file/LocalFile.php
new file mode 100644 (file)
index 0000000..d11030f
--- /dev/null
@@ -0,0 +1,2323 @@
+<?php
+/**
+ * Local file in the wiki's own database
+ *
+ * @file
+ * @ingroup FileRepo
+ */
+
+/**
+ * Bump this number when serialized cache records may be incompatible.
+ */
+define( 'MW_FILE_VERSION', 8 );
+
+/**
+ * Class to represent a local file in the wiki's own database
+ *
+ * Provides methods to retrieve paths (physical, logical, URL),
+ * to generate image thumbnails or for uploading.
+ *
+ * Note that only the repo object knows what its file class is called. You should
+ * never name a file class explictly outside of the repo class. Instead use the
+ * repo's factory functions to generate file objects, for example:
+ *
+ * RepoGroup::singleton()->getLocalRepo()->newFile($title);
+ *
+ * The convenience functions wfLocalFile() and wfFindFile() should be sufficient
+ * in most cases.
+ *
+ * @ingroup FileRepo
+ */
+class LocalFile extends File {
+       /**#@+
+        * @private
+        */
+       var
+               $fileExists,       # does the file exist on disk? (loadFromXxx)
+               $historyLine,      # Number of line to return by nextHistoryLine() (constructor)
+               $historyRes,       # result of the query for the file's history (nextHistoryLine)
+               $width,            # \
+               $height,           #  |
+               $bits,             #   --- returned by getimagesize (loadFromXxx)
+               $attr,             # /
+               $media_type,       # MEDIATYPE_xxx (bitmap, drawing, audio...)
+               $mime,             # MIME type, determined by MimeMagic::guessMimeType
+               $major_mime,       # Major mime type
+               $minor_mime,       # Minor mime type
+               $size,             # Size in bytes (loadFromXxx)
+               $metadata,         # Handler-specific metadata
+               $timestamp,        # Upload timestamp
+               $sha1,             # SHA-1 base 36 content hash
+               $user, $user_text, # User, who uploaded the file
+               $description,      # Description of current revision of the file
+               $dataLoaded,       # Whether or not all this has been loaded from the database (loadFromXxx)
+               $upgraded,         # Whether the row was upgraded on load
+               $locked,           # True if the image row is locked
+               $missing,          # True if file is not present in file system. Not to be cached in memcached
+               $deleted;          # Bitfield akin to rev_deleted
+
+       /**#@-*/
+
+       protected $repoClass = 'LocalRepo';
+
+       /**
+        * Create a LocalFile from a title
+        * Do not call this except from inside a repo class.
+        *
+        * Note: $unused param is only here to avoid an E_STRICT
+        *
+        * @param $title
+        * @param $repo
+        * @param $unused
+        *
+        * @return LocalFile
+        */
+       static function newFromTitle( $title, $repo, $unused = null ) {
+               return new self( $title, $repo );
+       }
+
+       /**
+        * Create a LocalFile from a title
+        * Do not call this except from inside a repo class.
+        *
+        * @param $row
+        * @param $repo
+        *
+        * @return LocalFile
+        */
+       static function newFromRow( $row, $repo ) {
+               $title = Title::makeTitle( NS_FILE, $row->img_name );
+               $file = new self( $title, $repo );
+               $file->loadFromRow( $row );
+
+               return $file;
+       }
+
+       /**
+        * Create a LocalFile from a SHA-1 key
+        * Do not call this except from inside a repo class.
+        *
+        * @param $sha1 string base-36 SHA-1
+        * @param $repo LocalRepo
+        * @param string|bool $timestamp MW_timestamp (optional)
+        *
+        * @return bool|LocalFile
+        */
+       static function newFromKey( $sha1, $repo, $timestamp = false ) {
+               $dbr = $repo->getSlaveDB();
+
+               $conds = array( 'img_sha1' => $sha1 );
+               if ( $timestamp ) {
+                       $conds['img_timestamp'] = $dbr->timestamp( $timestamp );
+               }
+
+               $row = $dbr->selectRow( 'image', self::selectFields(), $conds, __METHOD__ );
+               if ( $row ) {
+                       return self::newFromRow( $row, $repo );
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * Fields in the image table
+        */
+       static function selectFields() {
+               return array(
+                       'img_name',
+                       'img_size',
+                       'img_width',
+                       'img_height',
+                       'img_metadata',
+                       'img_bits',
+                       'img_media_type',
+                       'img_major_mime',
+                       'img_minor_mime',
+                       'img_description',
+                       'img_user',
+                       'img_user_text',
+                       'img_timestamp',
+                       'img_sha1',
+               );
+       }
+
+       /**
+        * Constructor.
+        * Do not call this except from inside a repo class.
+        */
+       function __construct( $title, $repo ) {
+               parent::__construct( $title, $repo );
+
+               $this->metadata = '';
+               $this->historyLine = 0;
+               $this->historyRes = null;
+               $this->dataLoaded = false;
+
+               $this->assertRepoDefined();
+               $this->assertTitleDefined();
+       }
+
+       /**
+        * Get the memcached key for the main data for this file, or false if
+        * there is no access to the shared cache.
+        */
+       function getCacheKey() {
+               $hashedName = md5( $this->getName() );
+
+               return $this->repo->getSharedCacheKey( 'file', $hashedName );
+       }
+
+       /**
+        * Try to load file metadata from memcached. Returns true on success.
+        */
+       function loadFromCache() {
+               global $wgMemc;
+
+               wfProfileIn( __METHOD__ );
+               $this->dataLoaded = false;
+               $key = $this->getCacheKey();
+
+               if ( !$key ) {
+                       wfProfileOut( __METHOD__ );
+                       return false;
+               }
+
+               $cachedValues = $wgMemc->get( $key );
+
+               // Check if the key existed and belongs to this version of MediaWiki
+               if ( isset( $cachedValues['version'] ) && ( $cachedValues['version'] == MW_FILE_VERSION ) ) {
+                       wfDebug( "Pulling file metadata from cache key $key\n" );
+                       $this->fileExists = $cachedValues['fileExists'];
+                       if ( $this->fileExists ) {
+                               $this->setProps( $cachedValues );
+                       }
+                       $this->dataLoaded = true;
+               }
+
+               if ( $this->dataLoaded ) {
+                       wfIncrStats( 'image_cache_hit' );
+               } else {
+                       wfIncrStats( 'image_cache_miss' );
+               }
+
+               wfProfileOut( __METHOD__ );
+               return $this->dataLoaded;
+       }
+
+       /**
+        * Save the file metadata to memcached
+        */
+       function saveToCache() {
+               global $wgMemc;
+
+               $this->load();
+               $key = $this->getCacheKey();
+
+               if ( !$key ) {
+                       return;
+               }
+
+               $fields = $this->getCacheFields( '' );
+               $cache = array( 'version' => MW_FILE_VERSION );
+               $cache['fileExists'] = $this->fileExists;
+
+               if ( $this->fileExists ) {
+                       foreach ( $fields as $field ) {
+                               $cache[$field] = $this->$field;
+                       }
+               }
+
+               $wgMemc->set( $key, $cache, 60 * 60 * 24 * 7 ); // A week
+       }
+
+       /**
+        * Load metadata from the file itself
+        */
+       function loadFromFile() {
+               $this->setProps( self::getPropsFromPath( $this->getPath() ) );
+       }
+
+       function getCacheFields( $prefix = 'img_' ) {
+               static $fields = array( 'size', 'width', 'height', 'bits', 'media_type',
+                       'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1', 'user', 'user_text', 'description' );
+               static $results = array();
+
+               if ( $prefix == '' ) {
+                       return $fields;
+               }
+
+               if ( !isset( $results[$prefix] ) ) {
+                       $prefixedFields = array();
+                       foreach ( $fields as $field ) {
+                               $prefixedFields[] = $prefix . $field;
+                       }
+                       $results[$prefix] = $prefixedFields;
+               }
+
+               return $results[$prefix];
+       }
+
+       /**
+        * Load file metadata from the DB
+        */
+       function loadFromDB() {
+               # Polymorphic function name to distinguish foreign and local fetches
+               $fname = get_class( $this ) . '::' . __FUNCTION__;
+               wfProfileIn( $fname );
+
+               # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
+               $this->dataLoaded = true;
+
+               $dbr = $this->repo->getMasterDB();
+
+               $row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ),
+                       array( 'img_name' => $this->getName() ), $fname );
+
+               if ( $row ) {
+                       $this->loadFromRow( $row );
+               } else {
+                       $this->fileExists = false;
+               }
+
+               wfProfileOut( $fname );
+       }
+
+       /**
+        * Decode a row from the database (either object or array) to an array
+        * with timestamps and MIME types decoded, and the field prefix removed.
+        */
+       function decodeRow( $row, $prefix = 'img_' ) {
+               $array = (array)$row;
+               $prefixLength = strlen( $prefix );
+
+               // Sanity check prefix once
+               if ( substr( key( $array ), 0, $prefixLength ) !== $prefix ) {
+                       throw new MWException( __METHOD__ .  ': incorrect $prefix parameter' );
+               }
+
+               $decoded = array();
+
+               foreach ( $array as $name => $value ) {
+                       $decoded[substr( $name, $prefixLength )] = $value;
+               }
+
+               $decoded['timestamp'] = wfTimestamp( TS_MW, $decoded['timestamp'] );
+
+               if ( empty( $decoded['major_mime'] ) ) {
+                       $decoded['mime'] = 'unknown/unknown';
+               } else {
+                       if ( !$decoded['minor_mime'] ) {
+                               $decoded['minor_mime'] = 'unknown';
+                       }
+                       $decoded['mime'] = $decoded['major_mime'] . '/' . $decoded['minor_mime'];
+               }
+
+               # Trim zero padding from char/binary field
+               $decoded['sha1'] = rtrim( $decoded['sha1'], "\0" );
+
+               return $decoded;
+       }
+
+       /**
+        * Load file metadata from a DB result row
+        */
+       function loadFromRow( $row, $prefix = 'img_' ) {
+               $this->dataLoaded = true;
+               $array = $this->decodeRow( $row, $prefix );
+
+               foreach ( $array as $name => $value ) {
+                       $this->$name = $value;
+               }
+
+               $this->fileExists = true;
+               $this->maybeUpgradeRow();
+       }
+
+       /**
+        * Load file metadata from cache or DB, unless already loaded
+        */
+       function load() {
+               if ( !$this->dataLoaded ) {
+                       if ( !$this->loadFromCache() ) {
+                               $this->loadFromDB();
+                               $this->saveToCache();
+                       }
+                       $this->dataLoaded = true;
+               }
+       }
+
+       /**
+        * Upgrade a row if it needs it
+        */
+       function maybeUpgradeRow() {
+               global $wgUpdateCompatibleMetadata;
+               if ( wfReadOnly() ) {
+                       return;
+               }
+
+               if ( is_null( $this->media_type ) ||
+                       $this->mime == 'image/svg'
+               ) {
+                       $this->upgradeRow();
+                       $this->upgraded = true;
+               } else {
+                       $handler = $this->getHandler();
+                       if ( $handler ) {
+                               $validity = $handler->isMetadataValid( $this, $this->metadata );
+                               if ( $validity === MediaHandler::METADATA_BAD
+                                       || ( $validity === MediaHandler::METADATA_COMPATIBLE && $wgUpdateCompatibleMetadata )
+                               ) {
+                                       $this->upgradeRow();
+                                       $this->upgraded = true;
+                               }
+                       }
+               }
+       }
+
+       function getUpgraded() {
+               return $this->upgraded;
+       }
+
+       /**
+        * Fix assorted version-related problems with the image row by reloading it from the file
+        */
+       function upgradeRow() {
+               wfProfileIn( __METHOD__ );
+
+               $this->loadFromFile();
+
+               # Don't destroy file info of missing files
+               if ( !$this->fileExists ) {
+                       wfDebug( __METHOD__ . ": file does not exist, aborting\n" );
+                       wfProfileOut( __METHOD__ );
+                       return;
+               }
+
+               $dbw = $this->repo->getMasterDB();
+               list( $major, $minor ) = self::splitMime( $this->mime );
+
+               if ( wfReadOnly() ) {
+                       wfProfileOut( __METHOD__ );
+                       return;
+               }
+               wfDebug( __METHOD__ . ': upgrading ' . $this->getName() . " to the current schema\n" );
+
+               $dbw->update( 'image',
+                       array(
+                               'img_width' => $this->width,
+                               'img_height' => $this->height,
+                               'img_bits' => $this->bits,
+                               'img_media_type' => $this->media_type,
+                               'img_major_mime' => $major,
+                               'img_minor_mime' => $minor,
+                               'img_metadata' => $this->metadata,
+                               'img_sha1' => $this->sha1,
+                       ), array( 'img_name' => $this->getName() ),
+                       __METHOD__
+               );
+
+               $this->saveToCache();
+               wfProfileOut( __METHOD__ );
+       }
+
+       /**
+        * Set properties in this object to be equal to those given in the
+        * associative array $info. Only cacheable fields can be set.
+        *
+        * If 'mime' is given, it will be split into major_mime/minor_mime.
+        * If major_mime/minor_mime are given, $this->mime will also be set.
+        */
+       function setProps( $info ) {
+               $this->dataLoaded = true;
+               $fields = $this->getCacheFields( '' );
+               $fields[] = 'fileExists';
+
+               foreach ( $fields as $field ) {
+                       if ( isset( $info[$field] ) ) {
+                               $this->$field = $info[$field];
+                       }
+               }
+
+               // Fix up mime fields
+               if ( isset( $info['major_mime'] ) ) {
+                       $this->mime = "{$info['major_mime']}/{$info['minor_mime']}";
+               } elseif ( isset( $info['mime'] ) ) {
+                       $this->mime = $info['mime'];
+                       list( $this->major_mime, $this->minor_mime ) = self::splitMime( $this->mime );
+               }
+       }
+
+       /** splitMime inherited */
+       /** getName inherited */
+       /** getTitle inherited */
+       /** getURL inherited */
+       /** getViewURL inherited */
+       /** getPath inherited */
+       /** isVisible inhereted */
+
+       function isMissing() {
+               if ( $this->missing === null ) {
+                       list( $fileExists ) = $this->repo->fileExistsBatch( array( $this->getVirtualUrl() ), FileRepo::FILES_ONLY );
+                       $this->missing = !$fileExists;
+               }
+               return $this->missing;
+       }
+
+       /**
+        * Return the width of the image
+        *
+        * Returns false on error
+        */
+       public function getWidth( $page = 1 ) {
+               $this->load();
+
+               if ( $this->isMultipage() ) {
+                       $dim = $this->getHandler()->getPageDimensions( $this, $page );
+                       if ( $dim ) {
+                               return $dim['width'];
+                       } else {
+                               return false;
+                       }
+               } else {
+                       return $this->width;
+               }
+       }
+
+       /**
+        * Return the height of the image
+        *
+        * Returns false on error
+        */
+       public function getHeight( $page = 1 ) {
+               $this->load();
+
+               if ( $this->isMultipage() ) {
+                       $dim = $this->getHandler()->getPageDimensions( $this, $page );
+                       if ( $dim ) {
+                               return $dim['height'];
+                       } else {
+                               return false;
+                       }
+               } else {
+                       return $this->height;
+               }
+       }
+
+       /**
+        * Returns ID or name of user who uploaded the file
+        *
+        * @param $type string 'text' or 'id'
+        */
+       function getUser( $type = 'text' ) {
+               $this->load();
+
+               if ( $type == 'text' ) {
+                       return $this->user_text;
+               } elseif ( $type == 'id' ) {
+                       return $this->user;
+               }
+       }
+
+       /**
+        * Get handler-specific metadata
+        */
+       function getMetadata() {
+               $this->load();
+               return $this->metadata;
+       }
+
+       function getBitDepth() {
+               $this->load();
+               return $this->bits;
+       }
+
+       /**
+        * Return the size of the image file, in bytes
+        */
+       public function getSize() {
+               $this->load();
+               return $this->size;
+       }
+
+       /**
+        * Returns the mime type of the file.
+        */
+       function getMimeType() {
+               $this->load();
+               return $this->mime;
+       }
+
+       /**
+        * Return the type of the media in the file.
+        * Use the value returned by this function with the MEDIATYPE_xxx constants.
+        */
+       function getMediaType() {
+               $this->load();
+               return $this->media_type;
+       }
+
+       /** canRender inherited */
+       /** mustRender inherited */
+       /** allowInlineDisplay inherited */
+       /** isSafeFile inherited */
+       /** isTrustedFile inherited */
+
+       /**
+        * Returns true if the file exists on disk.
+        * @return boolean Whether file exist on disk.
+        */
+       public function exists() {
+               $this->load();
+               return $this->fileExists;
+       }
+
+       /** getTransformScript inherited */
+       /** getUnscaledThumb inherited */
+       /** thumbName inherited */
+       /** createThumb inherited */
+       /** transform inherited */
+
+       /**
+        * Fix thumbnail files from 1.4 or before, with extreme prejudice
+        */
+       function migrateThumbFile( $thumbName ) {
+               $thumbDir = $this->getThumbPath();
+               $thumbPath = "$thumbDir/$thumbName";
+
+               if ( is_dir( $thumbPath ) ) {
+                       // Directory where file should be
+                       // This happened occasionally due to broken migration code in 1.5
+                       // Rename to broken-*
+                       for ( $i = 0; $i < 100 ; $i++ ) {
+                               $broken = $this->repo->getZonePath( 'public' ) . "/broken-$i-$thumbName";
+                               if ( !file_exists( $broken ) ) {
+                                       rename( $thumbPath, $broken );
+                                       break;
+                               }
+                       }
+                       // Doesn't exist anymore
+                       clearstatcache();
+               }
+
+               if ( is_file( $thumbDir ) ) {
+                       // File where directory should be
+                       unlink( $thumbDir );
+                       // Doesn't exist anymore
+                       clearstatcache();
+               }
+       }
+
+       /** getHandler inherited */
+       /** iconThumb inherited */
+       /** getLastError inherited */
+
+       /**
+        * Get all thumbnail names previously generated for this file
+        * @param $archiveName string|false Name of an archive file
+        * @return array first element is the base dir, then files in that base dir.
+        */
+       function getThumbnails( $archiveName = false ) {
+               $this->load();
+
+               if ( $archiveName ) {
+                       $dir = $this->getArchiveThumbPath( $archiveName );
+               } else {
+                       $dir = $this->getThumbPath();
+               }
+               $files = array();
+               $files[] = $dir;
+
+               if ( is_dir( $dir ) ) {
+                       $handle = opendir( $dir );
+
+                       if ( $handle ) {
+                               while ( false !== ( $file = readdir( $handle ) ) ) {
+                                       if ( $file { 0 } != '.' ) {
+                                               $files[] = $file;
+                                       }
+                               }
+
+                               closedir( $handle );
+                       }
+               }
+
+               return $files;
+       }
+
+       /**
+        * Refresh metadata in memcached, but don't touch thumbnails or squid
+        */
+       function purgeMetadataCache() {
+               $this->loadFromDB();
+               $this->saveToCache();
+               $this->purgeHistory();
+       }
+
+       /**
+        * Purge the shared history (OldLocalFile) cache
+        */
+       function purgeHistory() {
+               global $wgMemc;
+
+               $hashedName = md5( $this->getName() );
+               $oldKey = $this->repo->getSharedCacheKey( 'oldfile', $hashedName );
+
+               // Must purge thumbnails for old versions too! bug 30192
+               foreach( $this->getHistory() as $oldFile ) {
+                       $oldFile->purgeThumbnails();
+               }
+
+               if ( $oldKey ) {
+                       $wgMemc->delete( $oldKey );
+               }
+       }
+
+       /**
+        * Delete all previously generated thumbnails, refresh metadata in memcached and purge the squid
+        */
+       function purgeCache() {
+               // Refresh metadata cache
+               $this->purgeMetadataCache();
+
+               // Delete thumbnails
+               $this->purgeThumbnails();
+
+               // Purge squid cache for this file
+               SquidUpdate::purge( array( $this->getURL() ) );
+       }
+
+       /**
+        * Delete cached transformed files for an archived version only.
+        * @param $archiveName string name of the archived file
+        */
+       function purgeOldThumbnails( $archiveName ) {
+               global $wgUseSquid;
+               // Get a list of old thumbnails and URLs
+               $files = $this->getThumbnails( $archiveName );
+               $dir = array_shift( $files );
+               $this->purgeThumbList( $dir, $files );
+
+               // Directory should be empty, delete it too. This will probably suck on
+               // something like NFS or if the directory isn't actually empty, so hide
+               // the warnings :D
+               wfSuppressWarnings();
+               if( !rmdir( $dir ) ) {
+                       wfDebug( __METHOD__ . ": unable to remove archive directory: $dir\n" );
+               }
+               wfRestoreWarnings();
+
+               // Purge any custom thumbnail caches
+               wfRunHooks( 'LocalFilePurgeThumbnails', array( $this, $archiveName ) );
+
+               // Purge the squid
+               if ( $wgUseSquid ) {
+                       $urls = array();
+                       foreach( $files as $file ) {
+                               $urls[] = $this->getArchiveThumbUrl( $archiveName, $file );
+                       }
+                       SquidUpdate::purge( $urls );
+               }
+       }
+
+
+       /**
+        * Delete cached transformed files for the current version only.
+        */
+       function purgeThumbnails() {
+               global $wgUseSquid;
+
+               // Delete thumbnails
+               $files = $this->getThumbnails();
+               $dir = array_shift( $files );
+               $this->purgeThumbList( $dir, $files );
+
+               // Purge any custom thumbnail caches
+               wfRunHooks( 'LocalFilePurgeThumbnails', array( $this, false ) );
+
+               // Purge the squid
+               if ( $wgUseSquid ) {
+                       $urls = array();
+                       foreach( $files as $file ) {
+                               $urls[] = $this->getThumbUrl( $file );
+                       }
+                       SquidUpdate::purge( $urls );
+               }
+       }
+
+       /**
+        * Delete a list of thumbnails visible at urls
+        * @param $dir string base dir of the files.
+        * @param $files array of strings: relative filenames (to $dir)
+        */
+       protected function purgeThumbList($dir, $files) {
+               wfDebug( __METHOD__ . ": " . var_export( $files, true ) . "\n" );
+               foreach ( $files as $file ) {
+                       # Check that the base file name is part of the thumb name
+                       # This is a basic sanity check to avoid erasing unrelated directories
+                       if ( strpos( $file, $this->getName() ) !== false ) {
+                               wfSuppressWarnings();
+                               unlink( "$dir/$file" );
+                               wfRestoreWarnings();
+                       }
+               }
+       }
+
+       /** purgeDescription inherited */
+       /** purgeEverything inherited */
+
+       function getHistory( $limit = null, $start = null, $end = null, $inc = true ) {
+               $dbr = $this->repo->getSlaveDB();
+               $tables = array( 'oldimage' );
+               $fields = OldLocalFile::selectFields();
+               $conds = $opts = $join_conds = array();
+               $eq = $inc ? '=' : '';
+               $conds[] = "oi_name = " . $dbr->addQuotes( $this->title->getDBkey() );
+
+               if ( $start ) {
+                       $conds[] = "oi_timestamp <$eq " . $dbr->addQuotes( $dbr->timestamp( $start ) );
+               }
+
+               if ( $end ) {
+                       $conds[] = "oi_timestamp >$eq " . $dbr->addQuotes( $dbr->timestamp( $end ) );
+               }
+
+               if ( $limit ) {
+                       $opts['LIMIT'] = $limit;
+               }
+
+               // Search backwards for time > x queries
+               $order = ( !$start && $end !== null ) ? 'ASC' : 'DESC';
+               $opts['ORDER BY'] = "oi_timestamp $order";
+               $opts['USE INDEX'] = array( 'oldimage' => 'oi_name_timestamp' );
+
+               wfRunHooks( 'LocalFile::getHistory', array( &$this, &$tables, &$fields,
+                       &$conds, &$opts, &$join_conds ) );
+
+               $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $opts, $join_conds );
+               $r = array();
+
+               foreach ( $res as $row ) {
+                       if ( $this->repo->oldFileFromRowFactory ) {
+                               $r[] = call_user_func( $this->repo->oldFileFromRowFactory, $row, $this->repo );
+                       } else {
+                               $r[] = OldLocalFile::newFromRow( $row, $this->repo );
+                       }
+               }
+
+               if ( $order == 'ASC' ) {
+                       $r = array_reverse( $r ); // make sure it ends up descending
+               }
+
+               return $r;
+       }
+
+       /**
+        * Return the history of this file, line by line.
+        * starts with current version, then old versions.
+        * uses $this->historyLine to check which line to return:
+        *  0      return line for current version
+        *  1      query for old versions, return first one
+        *  2, ... return next old version from above query
+        */
+       public function nextHistoryLine() {
+               # Polymorphic function name to distinguish foreign and local fetches
+               $fname = get_class( $this ) . '::' . __FUNCTION__;
+
+               $dbr = $this->repo->getSlaveDB();
+
+               if ( $this->historyLine == 0 ) {// called for the first time, return line from cur
+                       $this->historyRes = $dbr->select( 'image',
+                               array(
+                                       '*',
+                                       "'' AS oi_archive_name",
+                                       '0 as oi_deleted',
+                                       'img_sha1'
+                               ),
+                               array( 'img_name' => $this->title->getDBkey() ),
+                               $fname
+                       );
+
+                       if ( 0 == $dbr->numRows( $this->historyRes ) ) {
+                               $this->historyRes = null;
+                               return false;
+                       }
+               } elseif ( $this->historyLine == 1 ) {
+                       $this->historyRes = $dbr->select( 'oldimage', '*',
+                               array( 'oi_name' => $this->title->getDBkey() ),
+                               $fname,
+                               array( 'ORDER BY' => 'oi_timestamp DESC' )
+                       );
+               }
+               $this->historyLine ++;
+
+               return $dbr->fetchObject( $this->historyRes );
+       }
+
+       /**
+        * Reset the history pointer to the first element of the history
+        */
+       public function resetHistory() {
+               $this->historyLine = 0;
+
+               if ( !is_null( $this->historyRes ) ) {
+                       $this->historyRes = null;
+               }
+       }
+
+       /** getHashPath inherited */
+       /** getRel inherited */
+       /** getUrlRel inherited */
+       /** getArchiveRel inherited */
+       /** getArchivePath inherited */
+       /** getThumbPath inherited */
+       /** getArchiveUrl inherited */
+       /** getThumbUrl inherited */
+       /** getArchiveVirtualUrl inherited */
+       /** getThumbVirtualUrl inherited */
+       /** isHashed inherited */
+
+       /**
+        * Upload a file and record it in the DB
+        * @param $srcPath String: source path or virtual URL
+        * @param $comment String: upload description
+        * @param $pageText String: text to use for the new description page,
+        *                  if a new description page is created
+        * @param $flags Integer: flags for publish()
+        * @param $props Array: File properties, if known. This can be used to reduce the
+        *               upload time when uploading virtual URLs for which the file info
+        *               is already known
+        * @param $timestamp String: timestamp for img_timestamp, or false to use the current time
+        * @param $user Mixed: User object or null to use $wgUser
+        *
+        * @return FileRepoStatus object. On success, the value member contains the
+        *     archive name, or an empty string if it was a new file.
+        */
+       function upload( $srcPath, $comment, $pageText, $flags = 0, $props = false, $timestamp = false, $user = null ) {
+               global $wgContLang;
+               // truncate nicely or the DB will do it for us
+               // non-nicely (dangling multi-byte chars, non-truncated
+               // version in cache).
+               $comment = $wgContLang->truncate( $comment, 255 );
+               $this->lock();
+               $status = $this->publish( $srcPath, $flags );
+
+               if ( $status->ok ) {
+                       if ( !$this->recordUpload2( $status->value, $comment, $pageText, $props, $timestamp, $user ) ) {
+                               $status->fatal( 'filenotfound', $srcPath );
+                       }
+               }
+
+               $this->unlock();
+
+               return $status;
+       }
+
+       /**
+        * Record a file upload in the upload log and the image table
+        */
+       function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
+               $watch = false, $timestamp = false )
+       {
+               $pageText = SpecialUpload::getInitialPageText( $desc, $license, $copyStatus, $source );
+
+               if ( !$this->recordUpload2( $oldver, $desc, $pageText ) ) {
+                       return false;
+               }
+
+               if ( $watch ) {
+                       global $wgUser;
+                       $wgUser->addWatch( $this->getTitle() );
+               }
+               return true;
+       }
+
+       /**
+        * Record a file upload in the upload log and the image table
+        */
+       function recordUpload2(
+               $oldver, $comment, $pageText, $props = false, $timestamp = false, $user = null
+       ) {
+               if ( is_null( $user ) ) {
+                       global $wgUser;
+                       $user = $wgUser;
+               }
+
+               $dbw = $this->repo->getMasterDB();
+               $dbw->begin();
+
+               if ( !$props ) {
+                       $props = $this->repo->getFileProps( $this->getVirtualUrl() );
+               }
+
+               if ( $timestamp === false ) {
+                       $timestamp = $dbw->timestamp();
+               }
+
+               $props['description'] = $comment;
+               $props['user'] = $user->getId();
+               $props['user_text'] = $user->getName();
+               $props['timestamp'] = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
+               $this->setProps( $props );
+
+               # Delete thumbnails
+               $this->purgeThumbnails();
+
+               # The file is already on its final location, remove it from the squid cache
+               SquidUpdate::purge( array( $this->getURL() ) );
+
+               # Fail now if the file isn't there
+               if ( !$this->fileExists ) {
+                       wfDebug( __METHOD__ . ": File " . $this->getRel() . " went missing!\n" );
+                       return false;
+               }
+
+               $reupload = false;
+
+               # Test to see if the row exists using INSERT IGNORE
+               # This avoids race conditions by locking the row until the commit, and also
+               # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
+               $dbw->insert( 'image',
+                       array(
+                               'img_name' => $this->getName(),
+                               'img_size' => $this->size,
+                               'img_width' => intval( $this->width ),
+                               'img_height' => intval( $this->height ),
+                               'img_bits' => $this->bits,
+                               'img_media_type' => $this->media_type,
+                               'img_major_mime' => $this->major_mime,
+                               'img_minor_mime' => $this->minor_mime,
+                               'img_timestamp' => $timestamp,
+                               'img_description' => $comment,
+                               'img_user' => $user->getId(),
+                               'img_user_text' => $user->getName(),
+                               'img_metadata' => $this->metadata,
+                               'img_sha1' => $this->sha1
+                       ),
+                       __METHOD__,
+                       'IGNORE'
+               );
+
+               if ( $dbw->affectedRows() == 0 ) {
+                       $reupload = true;
+
+                       # Collision, this is an update of a file
+                       # Insert previous contents into oldimage
+                       $dbw->insertSelect( 'oldimage', 'image',
+                               array(
+                                       'oi_name' => 'img_name',
+                                       'oi_archive_name' => $dbw->addQuotes( $oldver ),
+                                       'oi_size' => 'img_size',
+                                       'oi_width' => 'img_width',
+                                       'oi_height' => 'img_height',
+                                       'oi_bits' => 'img_bits',
+                                       'oi_timestamp' => 'img_timestamp',
+                                       'oi_description' => 'img_description',
+                                       'oi_user' => 'img_user',
+                                       'oi_user_text' => 'img_user_text',
+                                       'oi_metadata' => 'img_metadata',
+                                       'oi_media_type' => 'img_media_type',
+                                       'oi_major_mime' => 'img_major_mime',
+                                       'oi_minor_mime' => 'img_minor_mime',
+                                       'oi_sha1' => 'img_sha1'
+                               ), array( 'img_name' => $this->getName() ), __METHOD__
+                       );
+
+                       # Update the current image row
+                       $dbw->update( 'image',
+                               array( /* SET */
+                                       'img_size' => $this->size,
+                                       'img_width' => intval( $this->width ),
+                                       'img_height' => intval( $this->height ),
+                                       'img_bits' => $this->bits,
+                                       'img_media_type' => $this->media_type,
+                                       'img_major_mime' => $this->major_mime,
+                                       'img_minor_mime' => $this->minor_mime,
+                                       'img_timestamp' => $timestamp,
+                                       'img_description' => $comment,
+                                       'img_user' => $user->getId(),
+                                       'img_user_text' => $user->getName(),
+                                       'img_metadata' => $this->metadata,
+                                       'img_sha1' => $this->sha1
+                               ), array( /* WHERE */
+                                       'img_name' => $this->getName()
+                               ), __METHOD__
+                       );
+               } else {
+                       # This is a new file
+                       # Update the image count
+                       $dbw->begin( __METHOD__ );
+                       $dbw->update(
+                               'site_stats',
+                               array( 'ss_images = ss_images+1' ),
+                               '*',
+                               __METHOD__
+                       );
+                       $dbw->commit( __METHOD__ );
+               }
+
+               $descTitle = $this->getTitle();
+               $wikiPage = new WikiFilePage( $descTitle );
+               $wikiPage->setFile( $this );
+
+               # Add the log entry
+               $log = new LogPage( 'upload' );
+               $action = $reupload ? 'overwrite' : 'upload';
+               $log->addEntry( $action, $descTitle, $comment, array(), $user );
+
+               if ( $descTitle->exists() ) {
+                       # Create a null revision
+                       $latest = $descTitle->getLatestRevID();
+                       $nullRevision = Revision::newNullRevision(
+                               $dbw,
+                               $descTitle->getArticleId(),
+                               $log->getRcComment(),
+                               false
+                       );
+                       if (!is_null($nullRevision)) {
+                               $nullRevision->insertOn( $dbw );
+
+                               wfRunHooks( 'NewRevisionFromEditComplete', array( $wikiPage, $nullRevision, $latest, $user ) );
+                               $wikiPage->updateRevisionOn( $dbw, $nullRevision );
+                       }
+                       # Invalidate the cache for the description page
+                       $descTitle->invalidateCache();
+                       $descTitle->purgeSquid();
+               } else {
+                       # New file; create the description page.
+                       # There's already a log entry, so don't make a second RC entry
+                       # Squid and file cache for the description page are purged by doEdit.
+                       $wikiPage->doEdit( $pageText, $comment, EDIT_NEW | EDIT_SUPPRESS_RC );
+               }
+
+               # Commit the transaction now, in case something goes wrong later
+               # The most important thing is that files don't get lost, especially archives
+               $dbw->commit();
+
+               # Save to cache and purge the squid
+               # We shall not saveToCache before the commit since otherwise
+               # in case of a rollback there is an usable file from memcached
+               # which in fact doesn't really exist (bug 24978)
+               $this->saveToCache();
+
+               # Hooks, hooks, the magic of hooks...
+               wfRunHooks( 'FileUpload', array( $this, $reupload, $descTitle->exists() ) );
+
+               # Invalidate cache for all pages using this file
+               $update = new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' );
+               $update->doUpdate();
+
+               # Invalidate cache for all pages that redirects on this page
+               $redirs = $this->getTitle()->getRedirectsHere();
+
+               foreach ( $redirs as $redir ) {
+                       $update = new HTMLCacheUpdate( $redir, 'imagelinks' );
+                       $update->doUpdate();
+               }
+
+               return true;
+       }
+
+       /**
+        * Move or copy a file to its public location. If a file exists at the
+        * 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 $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 FileRepoStatus object. On success, the value member contains the
+        *     archive name, or an empty string if it was a new file.
+        */
+       function publish( $srcPath, $flags = 0 ) {
+               return $this->publishTo( $srcPath, $this->getRel(), $flags );
+       }
+
+       /**
+        * Move or copy a file to a specified location. 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 $srcPath String: local filesystem path to the source image
+        * @param $dstRel String: target relative path
+        * @param $flags Integer: a bitwise combination of:
+        *     File::DELETE_SOURCE      Delete the source file, i.e. move rather than copy
+        * @return FileRepoStatus object. On success, the value member contains the
+        *     archive name, or an empty string if it was a new file.
+        */
+       function publishTo( $srcPath, $dstRel, $flags = 0 ) {
+               $this->lock();
+
+               $archiveName = wfTimestamp( TS_MW ) . '!'. $this->getName();
+               $archiveRel = 'archive/' . $this->getHashPath() . $archiveName;
+               $flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0;
+               $status = $this->repo->publish( $srcPath, $dstRel, $archiveRel, $flags );
+
+               if ( $status->value == 'new' ) {
+                       $status->value = '';
+               } else {
+                       $status->value = $archiveName;
+               }
+
+               $this->unlock();
+
+               return $status;
+       }
+
+       /** getLinksTo inherited */
+       /** getExifData inherited */
+       /** isLocal inherited */
+       /** wasDeleted inherited */
+
+       /**
+        * Move file to the new title
+        *
+        * Move current, old version and all thumbnails
+        * to the new filename. Old file is deleted.
+        *
+        * Cache purging is done; checks for validity
+        * and logging are caller's responsibility
+        *
+        * @param $target Title New file name
+        * @return FileRepoStatus object.
+        */
+       function move( $target ) {
+               wfDebugLog( 'imagemove', "Got request to move {$this->name} to " . $target->getText() );
+               $this->lock();
+
+               $batch = new LocalFileMoveBatch( $this, $target );
+               $batch->addCurrent();
+               $batch->addOlds();
+
+               $status = $batch->execute();
+               wfDebugLog( 'imagemove', "Finished moving {$this->name}" );
+
+               $this->purgeEverything();
+               $this->unlock();
+
+               if ( $status->isOk() ) {
+                       // Now switch the object
+                       $this->title = $target;
+                       // Force regeneration of the name and hashpath
+                       unset( $this->name );
+                       unset( $this->hashPath );
+                       // Purge the new image
+                       $this->purgeEverything();
+               }
+
+               return $status;
+       }
+
+       /**
+        * Delete all versions of the file.
+        *
+        * Moves the files into an archive directory (or deletes them)
+        * and removes the database rows.
+        *
+        * Cache purging is done; logging is caller's responsibility.
+        *
+        * @param $reason
+        * @param $suppress
+        * @return FileRepoStatus object.
+        */
+       function delete( $reason, $suppress = false ) {
+               $this->lock();
+
+               $batch = new LocalFileDeleteBatch( $this, $reason, $suppress );
+               $batch->addCurrent();
+
+               # Get old version relative paths
+               $dbw = $this->repo->getMasterDB();
+               $result = $dbw->select( 'oldimage',
+                       array( 'oi_archive_name' ),
+                       array( 'oi_name' => $this->getName() ) );
+               foreach ( $result as $row ) {
+                       $batch->addOld( $row->oi_archive_name );
+                       $this->purgeOldThumbnails( $row->oi_archive_name );
+               }
+               $status = $batch->execute();
+
+               if ( $status->ok ) {
+                       // Update site_stats
+                       $site_stats = $dbw->tableName( 'site_stats' );
+                       $dbw->query( "UPDATE $site_stats SET ss_images=ss_images-1", __METHOD__ );
+                       $this->purgeEverything();
+               }
+
+               $this->unlock();
+
+               return $status;
+       }
+
+       /**
+        * Delete an old version of the file.
+        *
+        * Moves the file into an archive directory (or deletes it)
+        * and removes the database row.
+        *
+        * Cache purging is done; logging is caller's responsibility.
+        *
+        * @param $archiveName String
+        * @param $reason String
+        * @param $suppress Boolean
+        * @throws MWException or FSException on database or file store failure
+        * @return FileRepoStatus object.
+        */
+       function deleteOld( $archiveName, $reason, $suppress = false ) {
+               $this->lock();
+
+               $batch = new LocalFileDeleteBatch( $this, $reason, $suppress );
+               $batch->addOld( $archiveName );
+               $this->purgeOldThumbnails( $archiveName );
+               $status = $batch->execute();
+
+               $this->unlock();
+
+               if ( $status->ok ) {
+                       $this->purgeDescription();
+                       $this->purgeHistory();
+               }
+
+               return $status;
+       }
+
+       /**
+        * Restore all or specified deleted revisions to the given file.
+        * Permissions and logging are left to the caller.
+        *
+        * May throw database exceptions on error.
+        *
+        * @param $versions set of record ids of deleted items to restore,
+        *                    or empty to restore all revisions.
+        * @param $unsuppress Boolean
+        * @return FileRepoStatus
+        */
+       function restore( $versions = array(), $unsuppress = false ) {
+               $batch = new LocalFileRestoreBatch( $this, $unsuppress );
+
+               if ( !$versions ) {
+                       $batch->addAll();
+               } else {
+                       $batch->addIds( $versions );
+               }
+
+               $status = $batch->execute();
+
+               if ( !$status->isGood() ) {
+                       return $status;
+               }
+
+               $cleanupStatus = $batch->cleanup();
+               $cleanupStatus->successCount = 0;
+               $cleanupStatus->failCount = 0;
+               $status->merge( $cleanupStatus );
+
+               return $status;
+       }
+
+       /** isMultipage inherited */
+       /** pageCount inherited */
+       /** scaleHeight inherited */
+       /** getImageSize inherited */
+
+       /**
+        * Get the URL of the file description page.
+        */
+       function getDescriptionUrl() {
+               return $this->title->getLocalUrl();
+       }
+
+       /**
+        * Get the HTML text of the description page
+        * This is not used by ImagePage for local files, since (among other things)
+        * it skips the parser cache.
+        */
+       function getDescriptionText() {
+               global $wgParser;
+               $revision = Revision::newFromTitle( $this->title );
+               if ( !$revision ) return false;
+               $text = $revision->getText();
+               if ( !$text ) return false;
+               $pout = $wgParser->parse( $text, $this->title, new ParserOptions() );
+               return $pout->getText();
+       }
+
+       function getDescription() {
+               $this->load();
+               return $this->description;
+       }
+
+       function getTimestamp() {
+               $this->load();
+               return $this->timestamp;
+       }
+
+       function getSha1() {
+               $this->load();
+               // Initialise now if necessary
+               if ( $this->sha1 == '' && $this->fileExists ) {
+                       $this->sha1 = File::sha1Base36( $this->getPath() );
+                       if ( !wfReadOnly() && strval( $this->sha1 ) != '' ) {
+                               $dbw = $this->repo->getMasterDB();
+                               $dbw->update( 'image',
+                                       array( 'img_sha1' => $this->sha1 ),
+                                       array( 'img_name' => $this->getName() ),
+                                       __METHOD__ );
+                               $this->saveToCache();
+                       }
+               }
+
+               return $this->sha1;
+       }
+
+       /**
+        * Start a transaction and lock the image for update
+        * Increments a reference counter if the lock is already held
+        * @return boolean True if the image exists, false otherwise
+        */
+       function lock() {
+               $dbw = $this->repo->getMasterDB();
+
+               if ( !$this->locked ) {
+                       $dbw->begin();
+                       $this->locked++;
+               }
+
+               return $dbw->selectField( 'image', '1', array( 'img_name' => $this->getName() ), __METHOD__ );
+       }
+
+       /**
+        * Decrement the lock reference count. If the reference count is reduced to zero, commits
+        * the transaction and thereby releases the image lock.
+        */
+       function unlock() {
+               if ( $this->locked ) {
+                       --$this->locked;
+                       if ( !$this->locked ) {
+                               $dbw = $this->repo->getMasterDB();
+                               $dbw->commit();
+                       }
+               }
+       }
+
+       /**
+        * Roll back the DB transaction and mark the image unlocked
+        */
+       function unlockAndRollback() {
+               $this->locked = false;
+               $dbw = $this->repo->getMasterDB();
+               $dbw->rollback();
+       }
+} // LocalFile class
+
+# ------------------------------------------------------------------------------
+
+/**
+ * Helper class for file deletion
+ * @ingroup FileRepo
+ */
+class LocalFileDeleteBatch {
+
+       /**
+        * @var LocalFile
+        */
+       var $file;
+
+       var $reason, $srcRels = array(), $archiveUrls = array(), $deletionBatch, $suppress;
+       var $status;
+
+       function __construct( File $file, $reason = '', $suppress = false ) {
+               $this->file = $file;
+               $this->reason = $reason;
+               $this->suppress = $suppress;
+               $this->status = $file->repo->newGood();
+       }
+
+       function addCurrent() {
+               $this->srcRels['.'] = $this->file->getRel();
+       }
+
+       function addOld( $oldName ) {
+               $this->srcRels[$oldName] = $this->file->getArchiveRel( $oldName );
+               $this->archiveUrls[] = $this->file->getArchiveUrl( $oldName );
+       }
+
+       function getOldRels() {
+               if ( !isset( $this->srcRels['.'] ) ) {
+                       $oldRels =& $this->srcRels;
+                       $deleteCurrent = false;
+               } else {
+                       $oldRels = $this->srcRels;
+                       unset( $oldRels['.'] );
+                       $deleteCurrent = true;
+               }
+
+               return array( $oldRels, $deleteCurrent );
+       }
+
+       protected function getHashes() {
+               $hashes = array();
+               list( $oldRels, $deleteCurrent ) = $this->getOldRels();
+
+               if ( $deleteCurrent ) {
+                       $hashes['.'] = $this->file->getSha1();
+               }
+
+               if ( count( $oldRels ) ) {
+                       $dbw = $this->file->repo->getMasterDB();
+                       $res = $dbw->select(
+                               'oldimage',
+                               array( 'oi_archive_name', 'oi_sha1' ),
+                               'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')',
+                               __METHOD__
+                       );
+
+                       foreach ( $res as $row ) {
+                               if ( rtrim( $row->oi_sha1, "\0" ) === '' ) {
+                                       // Get the hash from the file
+                                       $oldUrl = $this->file->getArchiveVirtualUrl( $row->oi_archive_name );
+                                       $props = $this->file->repo->getFileProps( $oldUrl );
+
+                                       if ( $props['fileExists'] ) {
+                                               // Upgrade the oldimage row
+                                               $dbw->update( 'oldimage',
+                                                       array( 'oi_sha1' => $props['sha1'] ),
+                                                       array( 'oi_name' => $this->file->getName(), 'oi_archive_name' => $row->oi_archive_name ),
+                                                       __METHOD__ );
+                                               $hashes[$row->oi_archive_name] = $props['sha1'];
+                                       } else {
+                                               $hashes[$row->oi_archive_name] = false;
+                                       }
+                               } else {
+                                       $hashes[$row->oi_archive_name] = $row->oi_sha1;
+                               }
+                       }
+               }
+
+               $missing = array_diff_key( $this->srcRels, $hashes );
+
+               foreach ( $missing as $name => $rel ) {
+                       $this->status->error( 'filedelete-old-unregistered', $name );
+               }
+
+               foreach ( $hashes as $name => $hash ) {
+                       if ( !$hash ) {
+                               $this->status->error( 'filedelete-missing', $this->srcRels[$name] );
+                               unset( $hashes[$name] );
+                       }
+               }
+
+               return $hashes;
+       }
+
+       function doDBInserts() {
+               global $wgUser;
+
+               $dbw = $this->file->repo->getMasterDB();
+               $encTimestamp = $dbw->addQuotes( $dbw->timestamp() );
+               $encUserId = $dbw->addQuotes( $wgUser->getId() );
+               $encReason = $dbw->addQuotes( $this->reason );
+               $encGroup = $dbw->addQuotes( 'deleted' );
+               $ext = $this->file->getExtension();
+               $dotExt = $ext === '' ? '' : ".$ext";
+               $encExt = $dbw->addQuotes( $dotExt );
+               list( $oldRels, $deleteCurrent ) = $this->getOldRels();
+
+               // Bitfields to further suppress the content
+               if ( $this->suppress ) {
+                       $bitfield = 0;
+                       // This should be 15...
+                       $bitfield |= Revision::DELETED_TEXT;
+                       $bitfield |= Revision::DELETED_COMMENT;
+                       $bitfield |= Revision::DELETED_USER;
+                       $bitfield |= Revision::DELETED_RESTRICTED;
+               } else {
+                       $bitfield = 'oi_deleted';
+               }
+
+               if ( $deleteCurrent ) {
+                       $concat = $dbw->buildConcat( array( "img_sha1", $encExt ) );
+                       $where = array( 'img_name' => $this->file->getName() );
+                       $dbw->insertSelect( 'filearchive', 'image',
+                               array(
+                                       'fa_storage_group' => $encGroup,
+                                       'fa_storage_key'   => "CASE WHEN img_sha1='' THEN '' ELSE $concat END",
+                                       'fa_deleted_user'      => $encUserId,
+                                       'fa_deleted_timestamp' => $encTimestamp,
+                                       'fa_deleted_reason'    => $encReason,
+                                       'fa_deleted'               => $this->suppress ? $bitfield : 0,
+
+                                       'fa_name'         => 'img_name',
+                                       'fa_archive_name' => 'NULL',
+                                       'fa_size'         => 'img_size',
+                                       'fa_width'        => 'img_width',
+                                       'fa_height'       => 'img_height',
+                                       'fa_metadata'     => 'img_metadata',
+                                       'fa_bits'         => 'img_bits',
+                                       'fa_media_type'   => 'img_media_type',
+                                       'fa_major_mime'   => 'img_major_mime',
+                                       'fa_minor_mime'   => 'img_minor_mime',
+                                       'fa_description'  => 'img_description',
+                                       'fa_user'         => 'img_user',
+                                       'fa_user_text'    => 'img_user_text',
+                                       'fa_timestamp'    => 'img_timestamp'
+                               ), $where, __METHOD__ );
+               }
+
+               if ( count( $oldRels ) ) {
+                       $concat = $dbw->buildConcat( array( "oi_sha1", $encExt ) );
+                       $where = array(
+                               'oi_name' => $this->file->getName(),
+                               'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')' );
+                       $dbw->insertSelect( 'filearchive', 'oldimage',
+                               array(
+                                       'fa_storage_group' => $encGroup,
+                                       'fa_storage_key'   => "CASE WHEN oi_sha1='' THEN '' ELSE $concat END",
+                                       'fa_deleted_user'      => $encUserId,
+                                       'fa_deleted_timestamp' => $encTimestamp,
+                                       'fa_deleted_reason'    => $encReason,
+                                       'fa_deleted'               => $this->suppress ? $bitfield : 'oi_deleted',
+
+                                       'fa_name'         => 'oi_name',
+                                       'fa_archive_name' => 'oi_archive_name',
+                                       'fa_size'         => 'oi_size',
+                                       'fa_width'        => 'oi_width',
+                                       'fa_height'       => 'oi_height',
+                                       'fa_metadata'     => 'oi_metadata',
+                                       'fa_bits'         => 'oi_bits',
+                                       'fa_media_type'   => 'oi_media_type',
+                                       'fa_major_mime'   => 'oi_major_mime',
+                                       'fa_minor_mime'   => 'oi_minor_mime',
+                                       'fa_description'  => 'oi_description',
+                                       'fa_user'         => 'oi_user',
+                                       'fa_user_text'    => 'oi_user_text',
+                                       'fa_timestamp'    => 'oi_timestamp',
+                                       'fa_deleted'      => $bitfield
+                               ), $where, __METHOD__ );
+               }
+       }
+
+       function doDBDeletes() {
+               $dbw = $this->file->repo->getMasterDB();
+               list( $oldRels, $deleteCurrent ) = $this->getOldRels();
+
+               if ( count( $oldRels ) ) {
+                       $dbw->delete( 'oldimage',
+                               array(
+                                       'oi_name' => $this->file->getName(),
+                                       'oi_archive_name' => array_keys( $oldRels )
+                               ), __METHOD__ );
+               }
+
+               if ( $deleteCurrent ) {
+                       $dbw->delete( 'image', array( 'img_name' => $this->file->getName() ), __METHOD__ );
+               }
+       }
+
+       /**
+        * Run the transaction
+        */
+       function execute() {
+               global $wgUseSquid;
+               wfProfileIn( __METHOD__ );
+
+               $this->file->lock();
+               // Leave private files alone
+               $privateFiles = array();
+               list( $oldRels, $deleteCurrent ) = $this->getOldRels();
+               $dbw = $this->file->repo->getMasterDB();
+
+               if ( !empty( $oldRels ) ) {
+                       $res = $dbw->select( 'oldimage',
+                               array( 'oi_archive_name' ),
+                               array( 'oi_name' => $this->file->getName(),
+                                       'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')',
+                                       $dbw->bitAnd( 'oi_deleted', File::DELETED_FILE ) => File::DELETED_FILE ),
+                               __METHOD__ );
+
+                       foreach ( $res as $row ) {
+                               $privateFiles[$row->oi_archive_name] = 1;
+                       }
+               }
+               // Prepare deletion batch
+               $hashes = $this->getHashes();
+               $this->deletionBatch = array();
+               $ext = $this->file->getExtension();
+               $dotExt = $ext === '' ? '' : ".$ext";
+
+               foreach ( $this->srcRels as $name => $srcRel ) {
+                       // Skip files that have no hash (missing source).
+                       // Keep private files where they are.
+                       if ( isset( $hashes[$name] ) && !array_key_exists( $name, $privateFiles ) ) {
+                               $hash = $hashes[$name];
+                               $key = $hash . $dotExt;
+                               $dstRel = $this->file->repo->getDeletedHashPath( $key ) . $key;
+                               $this->deletionBatch[$name] = array( $srcRel, $dstRel );
+                       }
+               }
+
+               // Lock the filearchive rows so that the files don't get deleted by a cleanup operation
+               // We acquire this lock by running the inserts now, before the file operations.
+               //
+               // This potentially has poor lock contention characteristics -- an alternative
+               // scheme would be to insert stub filearchive entries with no fa_name and commit
+               // them in a separate transaction, then run the file ops, then update the fa_name fields.
+               $this->doDBInserts();
+
+               // Removes non-existent file from the batch, so we don't get errors.
+               $this->deletionBatch = $this->removeNonexistentFiles( $this->deletionBatch );
+
+               // Execute the file deletion batch
+               $status = $this->file->repo->deleteBatch( $this->deletionBatch );
+
+               if ( !$status->isGood() ) {
+                       $this->status->merge( $status );
+               }
+
+               if ( !$this->status->ok ) {
+                       // Critical file deletion error
+                       // Roll back inserts, release lock and abort
+                       // TODO: delete the defunct filearchive rows if we are using a non-transactional DB
+                       $this->file->unlockAndRollback();
+                       wfProfileOut( __METHOD__ );
+                       return $this->status;
+               }
+
+               // Purge squid
+               if ( $wgUseSquid ) {
+                       $urls = array();
+
+                       foreach ( $this->srcRels as $srcRel ) {
+                               $urlRel = str_replace( '%2F', '/', rawurlencode( $srcRel ) );
+                               $urls[] = $this->file->repo->getZoneUrl( 'public' ) . '/' . $urlRel;
+                       }
+                       SquidUpdate::purge( $urls );
+               }
+
+               // Delete image/oldimage rows
+               $this->doDBDeletes();
+
+               // Commit and return
+               $this->file->unlock();
+               wfProfileOut( __METHOD__ );
+
+               return $this->status;
+       }
+
+       /**
+        * Removes non-existent files from a deletion batch.
+        */
+       function removeNonexistentFiles( $batch ) {
+               $files = $newBatch = array();
+
+               foreach ( $batch as $batchItem ) {
+                       list( $src, $dest ) = $batchItem;
+                       $files[$src] = $this->file->repo->getVirtualUrl( 'public' ) . '/' . rawurlencode( $src );
+               }
+
+               $result = $this->file->repo->fileExistsBatch( $files, FSRepo::FILES_ONLY );
+
+               foreach ( $batch as $batchItem ) {
+                       if ( $result[$batchItem[0]] ) {
+                               $newBatch[] = $batchItem;
+                       }
+               }
+
+               return $newBatch;
+       }
+}
+
+# ------------------------------------------------------------------------------
+
+/**
+ * Helper class for file undeletion
+ * @ingroup FileRepo
+ */
+class LocalFileRestoreBatch {
+       /**
+        * @var LocalFile
+        */
+       var $file;
+
+       var $cleanupBatch, $ids, $all, $unsuppress = false;
+
+       function __construct( File $file, $unsuppress = false ) {
+               $this->file = $file;
+               $this->cleanupBatch = $this->ids = array();
+               $this->ids = array();
+               $this->unsuppress = $unsuppress;
+       }
+
+       /**
+        * Add a file by ID
+        */
+       function addId( $fa_id ) {
+               $this->ids[] = $fa_id;
+       }
+
+       /**
+        * Add a whole lot of files by ID
+        */
+       function addIds( $ids ) {
+               $this->ids = array_merge( $this->ids, $ids );
+       }
+
+       /**
+        * Add all revisions of the file
+        */
+       function addAll() {
+               $this->all = true;
+       }
+
+       /**
+        * Run the transaction, except the cleanup batch.
+        * The cleanup batch should be run in a separate transaction, because it locks different
+        * rows and there's no need to keep the image row locked while it's acquiring those locks
+        * The caller may have its own transaction open.
+        * So we save the batch and let the caller call cleanup()
+        */
+       function execute() {
+               global $wgLang;
+
+               if ( !$this->all && !$this->ids ) {
+                       // Do nothing
+                       return $this->file->repo->newGood();
+               }
+
+               $exists = $this->file->lock();
+               $dbw = $this->file->repo->getMasterDB();
+               $status = $this->file->repo->newGood();
+
+               // Fetch all or selected archived revisions for the file,
+               // sorted from the most recent to the oldest.
+               $conditions = array( 'fa_name' => $this->file->getName() );
+
+               if ( !$this->all ) {
+                       $conditions[] = 'fa_id IN (' . $dbw->makeList( $this->ids ) . ')';
+               }
+
+               $result = $dbw->select( 'filearchive', '*',
+                       $conditions,
+                       __METHOD__,
+                       array( 'ORDER BY' => 'fa_timestamp DESC' )
+               );
+
+               $idsPresent = array();
+               $storeBatch = array();
+               $insertBatch = array();
+               $insertCurrent = false;
+               $deleteIds = array();
+               $first = true;
+               $archiveNames = array();
+
+               foreach ( $result as $row ) {
+                       $idsPresent[] = $row->fa_id;
+
+                       if ( $row->fa_name != $this->file->getName() ) {
+                               $status->error( 'undelete-filename-mismatch', $wgLang->timeanddate( $row->fa_timestamp ) );
+                               $status->failCount++;
+                               continue;
+                       }
+
+                       if ( $row->fa_storage_key == '' ) {
+                               // Revision was missing pre-deletion
+                               $status->error( 'undelete-bad-store-key', $wgLang->timeanddate( $row->fa_timestamp ) );
+                               $status->failCount++;
+                               continue;
+                       }
+
+                       $deletedRel = $this->file->repo->getDeletedHashPath( $row->fa_storage_key ) . $row->fa_storage_key;
+                       $deletedUrl = $this->file->repo->getVirtualUrl() . '/deleted/' . $deletedRel;
+
+                       $sha1 = substr( $row->fa_storage_key, 0, strcspn( $row->fa_storage_key, '.' ) );
+
+                       # Fix leading zero
+                       if ( strlen( $sha1 ) == 32 && $sha1[0] == '0' ) {
+                               $sha1 = substr( $sha1, 1 );
+                       }
+
+                       if ( is_null( $row->fa_major_mime ) || $row->fa_major_mime == 'unknown'
+                               || is_null( $row->fa_minor_mime ) || $row->fa_minor_mime == 'unknown'
+                               || is_null( $row->fa_media_type ) || $row->fa_media_type == 'UNKNOWN'
+                               || is_null( $row->fa_metadata ) ) {
+                               // Refresh our metadata
+                               // Required for a new current revision; nice for older ones too. :)
+                               $props = RepoGroup::singleton()->getFileProps( $deletedUrl );
+                       } else {
+                               $props = array(
+                                       'minor_mime' => $row->fa_minor_mime,
+                                       'major_mime' => $row->fa_major_mime,
+                                       'media_type' => $row->fa_media_type,
+                                       'metadata'   => $row->fa_metadata
+                               );
+                       }
+
+                       if ( $first && !$exists ) {
+                               // This revision will be published as the new current version
+                               $destRel = $this->file->getRel();
+                               $insertCurrent = array(
+                                       'img_name'        => $row->fa_name,
+                                       'img_size'        => $row->fa_size,
+                                       'img_width'       => $row->fa_width,
+                                       'img_height'      => $row->fa_height,
+                                       'img_metadata'    => $props['metadata'],
+                                       'img_bits'        => $row->fa_bits,
+                                       'img_media_type'  => $props['media_type'],
+                                       'img_major_mime'  => $props['major_mime'],
+                                       'img_minor_mime'  => $props['minor_mime'],
+                                       'img_description' => $row->fa_description,
+                                       'img_user'        => $row->fa_user,
+                                       'img_user_text'   => $row->fa_user_text,
+                                       'img_timestamp'   => $row->fa_timestamp,
+                                       'img_sha1'        => $sha1
+                               );
+
+                               // The live (current) version cannot be hidden!
+                               if ( !$this->unsuppress && $row->fa_deleted ) {
+                                       $storeBatch[] = array( $deletedUrl, 'public', $destRel );
+                                       $this->cleanupBatch[] = $row->fa_storage_key;
+                               }
+                       } else {
+                               $archiveName = $row->fa_archive_name;
+
+                               if ( $archiveName == '' ) {
+                                       // This was originally a current version; we
+                                       // have to devise a new archive name for it.
+                                       // Format is <timestamp of archiving>!<name>
+                                       $timestamp = wfTimestamp( TS_UNIX, $row->fa_deleted_timestamp );
+
+                                       do {
+                                               $archiveName = wfTimestamp( TS_MW, $timestamp ) . '!' . $row->fa_name;
+                                               $timestamp++;
+                                       } while ( isset( $archiveNames[$archiveName] ) );
+                               }
+
+                               $archiveNames[$archiveName] = true;
+                               $destRel = $this->file->getArchiveRel( $archiveName );
+                               $insertBatch[] = array(
+                                       'oi_name'         => $row->fa_name,
+                                       'oi_archive_name' => $archiveName,
+                                       'oi_size'         => $row->fa_size,
+                                       'oi_width'        => $row->fa_width,
+                                       'oi_height'       => $row->fa_height,
+                                       'oi_bits'         => $row->fa_bits,
+                                       'oi_description'  => $row->fa_description,
+                                       'oi_user'         => $row->fa_user,
+                                       'oi_user_text'    => $row->fa_user_text,
+                                       'oi_timestamp'    => $row->fa_timestamp,
+                                       'oi_metadata'     => $props['metadata'],
+                                       'oi_media_type'   => $props['media_type'],
+                                       'oi_major_mime'   => $props['major_mime'],
+                                       'oi_minor_mime'   => $props['minor_mime'],
+                                       'oi_deleted'      => $this->unsuppress ? 0 : $row->fa_deleted,
+                                       'oi_sha1'         => $sha1 );
+                       }
+
+                       $deleteIds[] = $row->fa_id;
+
+                       if ( !$this->unsuppress && $row->fa_deleted & File::DELETED_FILE ) {
+                               // private files can stay where they are
+                               $status->successCount++;
+                       } else {
+                               $storeBatch[] = array( $deletedUrl, 'public', $destRel );
+                               $this->cleanupBatch[] = $row->fa_storage_key;
+                       }
+
+                       $first = false;
+               }
+
+               unset( $result );
+
+               // Add a warning to the status object for missing IDs
+               $missingIds = array_diff( $this->ids, $idsPresent );
+
+               foreach ( $missingIds as $id ) {
+                       $status->error( 'undelete-missing-filearchive', $id );
+               }
+
+               // Remove missing files from batch, so we don't get errors when undeleting them
+               $storeBatch = $this->removeNonexistentFiles( $storeBatch );
+
+               // Run the store batch
+               // Use the OVERWRITE_SAME flag to smooth over a common error
+               $storeStatus = $this->file->repo->storeBatch( $storeBatch, FileRepo::OVERWRITE_SAME );
+               $status->merge( $storeStatus );
+
+               if ( !$status->isGood() ) {
+                       // Even if some files could be copied, fail entirely as that is the
+                       // easiest thing to do without data loss
+                       $this->cleanupFailedBatch( $storeStatus, $storeBatch );
+                       $status->ok = false;
+                       $this->file->unlock();
+
+                       return $status;
+               }
+
+               // Run the DB updates
+               // Because we have locked the image row, key conflicts should be rare.
+               // If they do occur, we can roll back the transaction at this time with
+               // no data loss, but leaving unregistered files scattered throughout the
+               // public zone.
+               // This is not ideal, which is why it's important to lock the image row.
+               if ( $insertCurrent ) {
+                       $dbw->insert( 'image', $insertCurrent, __METHOD__ );
+               }
+
+               if ( $insertBatch ) {
+                       $dbw->insert( 'oldimage', $insertBatch, __METHOD__ );
+               }
+
+               if ( $deleteIds ) {
+                       $dbw->delete( 'filearchive',
+                               array( 'fa_id IN (' . $dbw->makeList( $deleteIds ) . ')' ),
+                               __METHOD__ );
+               }
+
+               // If store batch is empty (all files are missing), deletion is to be considered successful
+               if ( $status->successCount > 0 || !$storeBatch ) {
+                       if ( !$exists ) {
+                               wfDebug( __METHOD__ . " restored {$status->successCount} items, creating a new current\n" );
+
+                               // Update site_stats
+                               $site_stats = $dbw->tableName( 'site_stats' );
+                               $dbw->query( "UPDATE $site_stats SET ss_images=ss_images+1", __METHOD__ );
+
+                               $this->file->purgeEverything();
+                       } else {
+                               wfDebug( __METHOD__ . " restored {$status->successCount} as archived versions\n" );
+                               $this->file->purgeDescription();
+                               $this->file->purgeHistory();
+                       }
+               }
+
+               $this->file->unlock();
+
+               return $status;
+       }
+
+       /**
+        * Removes non-existent files from a store batch.
+        */
+       function removeNonexistentFiles( $triplets ) {
+               $files = $filteredTriplets = array();
+               foreach ( $triplets as $file )
+                       $files[$file[0]] = $file[0];
+
+               $result = $this->file->repo->fileExistsBatch( $files, FSRepo::FILES_ONLY );
+
+               foreach ( $triplets as $file ) {
+                       if ( $result[$file[0]] ) {
+                               $filteredTriplets[] = $file;
+                       }
+               }
+
+               return $filteredTriplets;
+       }
+
+       /**
+        * Removes non-existent files from a cleanup batch.
+        */
+       function removeNonexistentFromCleanup( $batch ) {
+               $files = $newBatch = array();
+               $repo = $this->file->repo;
+
+               foreach ( $batch as $file ) {
+                       $files[$file] = $repo->getVirtualUrl( 'deleted' ) . '/' .
+                               rawurlencode( $repo->getDeletedHashPath( $file ) . $file );
+               }
+
+               $result = $repo->fileExistsBatch( $files, FSRepo::FILES_ONLY );
+
+               foreach ( $batch as $file ) {
+                       if ( $result[$file] ) {
+                               $newBatch[] = $file;
+                       }
+               }
+
+               return $newBatch;
+       }
+
+       /**
+        * Delete unused files in the deleted zone.
+        * This should be called from outside the transaction in which execute() was called.
+        */
+       function cleanup() {
+               if ( !$this->cleanupBatch ) {
+                       return $this->file->repo->newGood();
+               }
+
+               $this->cleanupBatch = $this->removeNonexistentFromCleanup( $this->cleanupBatch );
+
+               $status = $this->file->repo->cleanupDeletedBatch( $this->cleanupBatch );
+
+               return $status;
+       }
+
+       /**
+        * Cleanup a failed batch. The batch was only partially successful, so
+        * rollback by removing all items that were succesfully copied.
+        *
+        * @param Status $storeStatus
+        * @param array $storeBatch
+        */
+       function cleanupFailedBatch( $storeStatus, $storeBatch ) {
+               $cleanupBatch = array();
+
+               foreach ( $storeStatus->success as $i => $success ) {
+                       // Check if this item of the batch was successfully copied
+                       if ( $success ) {
+                               // Item was successfully copied and needs to be removed again
+                               // Extract ($dstZone, $dstRel) from the batch
+                               $cleanupBatch[] = array( $storeBatch[$i][1], $storeBatch[$i][2] );
+                       }
+               }
+               $this->file->repo->cleanupBatch( $cleanupBatch );
+       }
+}
+
+# ------------------------------------------------------------------------------
+
+/**
+ * Helper class for file movement
+ * @ingroup FileRepo
+ */
+class LocalFileMoveBatch {
+
+       /**
+        * @var File
+        */
+       var $file;
+
+       /**
+        * @var Title
+        */
+       var $target;
+
+       var $cur, $olds, $oldCount, $archive, $db;
+
+       function __construct( File $file, Title $target ) {
+               $this->file = $file;
+               $this->target = $target;
+               $this->oldHash = $this->file->repo->getHashPath( $this->file->getName() );
+               $this->newHash = $this->file->repo->getHashPath( $this->target->getDBkey() );
+               $this->oldName = $this->file->getName();
+               $this->newName = $this->file->repo->getNameFromTitle( $this->target );
+               $this->oldRel = $this->oldHash . $this->oldName;
+               $this->newRel = $this->newHash . $this->newName;
+               $this->db = $file->repo->getMasterDb();
+       }
+
+       /**
+        * Add the current image to the batch
+        */
+       function addCurrent() {
+               $this->cur = array( $this->oldRel, $this->newRel );
+       }
+
+       /**
+        * Add the old versions of the image to the batch
+        */
+       function addOlds() {
+               $archiveBase = 'archive';
+               $this->olds = array();
+               $this->oldCount = 0;
+
+               $result = $this->db->select( 'oldimage',
+                       array( 'oi_archive_name', 'oi_deleted' ),
+                       array( 'oi_name' => $this->oldName ),
+                       __METHOD__
+               );
+
+               foreach ( $result as $row ) {
+                       $oldName = $row->oi_archive_name;
+                       $bits = explode( '!', $oldName, 2 );
+
+                       if ( count( $bits ) != 2 ) {
+                               wfDebug( "Old file name missing !: '$oldName' \n" );
+                               continue;
+                       }
+
+                       list( $timestamp, $filename ) = $bits;
+
+                       if ( $this->oldName != $filename ) {
+                               wfDebug( "Old file name doesn't match: '$oldName' \n" );
+                               continue;
+                       }
+
+                       $this->oldCount++;
+
+                       // Do we want to add those to oldCount?
+                       if ( $row->oi_deleted & File::DELETED_FILE ) {
+                               continue;
+                       }
+
+                       $this->olds[] = array(
+                               "{$archiveBase}/{$this->oldHash}{$oldName}",
+                               "{$archiveBase}/{$this->newHash}{$timestamp}!{$this->newName}"
+                       );
+               }
+       }
+
+       /**
+        * Perform the move.
+        */
+       function execute() {
+               $repo = $this->file->repo;
+               $status = $repo->newGood();
+               $triplets = $this->getMoveTriplets();
+
+               $triplets = $this->removeNonexistentFiles( $triplets );
+
+               // Copy the files into their new location
+               $statusMove = $repo->storeBatch( $triplets );
+               wfDebugLog( 'imagemove', "Moved files for {$this->file->getName()}: {$statusMove->successCount} successes, {$statusMove->failCount} failures" );
+               if ( !$statusMove->isGood() ) {
+                       wfDebugLog( 'imagemove', "Error in moving files: " . $statusMove->getWikiText() );
+                       $this->cleanupTarget( $triplets );
+                       $statusMove->ok = false;
+                       return $statusMove;
+               }
+
+               $this->db->begin();
+               $statusDb = $this->doDBUpdates();
+               wfDebugLog( 'imagemove', "Renamed {$this->file->getName()} in database: {$statusDb->successCount} successes, {$statusDb->failCount} failures" );
+               if ( !$statusDb->isGood() ) {
+                       $this->db->rollback();
+                       // Something went wrong with the DB updates, so remove the target files
+                       $this->cleanupTarget( $triplets );
+                       $statusDb->ok = false;
+                       return $statusDb;
+               }
+               $this->db->commit();
+
+               // Everything went ok, remove the source files
+               $this->cleanupSource( $triplets );
+
+               $status->merge( $statusDb );
+               $status->merge( $statusMove );
+
+               return $status;
+       }
+
+       /**
+        * Do the database updates and return a new FileRepoStatus indicating how
+        * many rows where updated.
+        *
+        * @return FileRepoStatus
+        */
+       function doDBUpdates() {
+               $repo = $this->file->repo;
+               $status = $repo->newGood();
+               $dbw = $this->db;
+
+               // Update current image
+               $dbw->update(
+                       'image',
+                       array( 'img_name' => $this->newName ),
+                       array( 'img_name' => $this->oldName ),
+                       __METHOD__
+               );
+
+               if ( $dbw->affectedRows() ) {
+                       $status->successCount++;
+               } else {
+                       $status->failCount++;
+                       $status->fatal( 'imageinvalidfilename' );
+                       return $status;
+               }
+
+               // Update old images
+               $dbw->update(
+                       'oldimage',
+                       array(
+                               'oi_name' => $this->newName,
+                               'oi_archive_name = ' . $dbw->strreplace( 'oi_archive_name', $dbw->addQuotes( $this->oldName ), $dbw->addQuotes( $this->newName ) ),
+                       ),
+                       array( 'oi_name' => $this->oldName ),
+                       __METHOD__
+               );
+
+               $affected = $dbw->affectedRows();
+               $total = $this->oldCount;
+               $status->successCount += $affected;
+               $status->failCount += $total - $affected;
+               if ( $status->failCount ) {
+                       $status->error( 'imageinvalidfilename' );
+               }
+
+               return $status;
+       }
+
+       /**
+        * Generate triplets for FSRepo::storeBatch().
+        */
+       function getMoveTriplets() {
+               $moves = array_merge( array( $this->cur ), $this->olds );
+               $triplets = array();    // The format is: (srcUrl, destZone, destUrl)
+
+               foreach ( $moves as $move ) {
+                       // $move: (oldRelativePath, newRelativePath)
+                       $srcUrl = $this->file->repo->getVirtualUrl() . '/public/' . rawurlencode( $move[0] );
+                       $triplets[] = array( $srcUrl, 'public', $move[1] );
+                       wfDebugLog( 'imagemove', "Generated move triplet for {$this->file->getName()}: {$srcUrl} :: public :: {$move[1]}" );
+               }
+
+               return $triplets;
+       }
+
+       /**
+        * Removes non-existent files from move batch.
+        */
+       function removeNonexistentFiles( $triplets ) {
+               $files = array();
+
+               foreach ( $triplets as $file ) {
+                       $files[$file[0]] = $file[0];
+               }
+
+               $result = $this->file->repo->fileExistsBatch( $files, FSRepo::FILES_ONLY );
+               $filteredTriplets = array();
+
+               foreach ( $triplets as $file ) {
+                       if ( $result[$file[0]] ) {
+                               $filteredTriplets[] = $file;
+                       } else {
+                               wfDebugLog( 'imagemove', "File {$file[0]} does not exist" );
+                       }
+               }
+
+               return $filteredTriplets;
+       }
+
+       /**
+        * Cleanup a partially moved array of triplets by deleting the target
+        * files. Called if something went wrong half way.
+        */
+       function cleanupTarget( $triplets ) {
+               // Create dest pairs from the triplets
+               $pairs = array();
+               foreach ( $triplets as $triplet ) {
+                       $pairs[] = array( $triplet[1], $triplet[2] );
+               }
+
+               $this->file->repo->cleanupBatch( $pairs );
+       }
+
+       /**
+        * Cleanup a fully moved array of triplets by deleting the source files.
+        * Called at the end of the move process if everything else went ok.
+        */
+       function cleanupSource( $triplets ) {
+               // Create source file names from the triplets
+               $files = array();
+               foreach ( $triplets as $triplet ) {
+                       $files[] = $triplet[0];
+               }
+
+               $this->file->repo->cleanupBatch( $files );
+       }
+}
diff --git a/includes/filerepo/file/OldLocalFile.php b/includes/filerepo/file/OldLocalFile.php
new file mode 100644 (file)
index 0000000..a22da16
--- /dev/null
@@ -0,0 +1,295 @@
+<?php
+/**
+ * Old file in the oldimage table
+ *
+ * @file
+ * @ingroup FileRepo
+ */
+
+/**
+ * Class to represent a file in the oldimage table
+ *
+ * @ingroup FileRepo
+ */
+class OldLocalFile extends LocalFile {
+       var $requestedTime, $archive_name;
+
+       const CACHE_VERSION = 1;
+       const MAX_CACHE_ROWS = 20;
+
+       static function newFromTitle( $title, $repo, $time = null ) {
+               # The null default value is only here to avoid an E_STRICT
+               if ( $time === null ) {
+                       throw new MWException( __METHOD__.' got null for $time parameter' );
+               }
+               return new self( $title, $repo, $time, null );
+       }
+
+       static function newFromArchiveName( $title, $repo, $archiveName ) {
+               return new self( $title, $repo, null, $archiveName );
+       }
+
+       static function newFromRow( $row, $repo ) {
+               $title = Title::makeTitle( NS_FILE, $row->oi_name );
+               $file = new self( $title, $repo, null, $row->oi_archive_name );
+               $file->loadFromRow( $row, 'oi_' );
+               return $file;
+       }
+
+       /**
+        * Create a OldLocalFile from a SHA-1 key
+        * Do not call this except from inside a repo class.
+        *
+        * @param $sha1 string base-36 SHA-1
+        * @param $repo LocalRepo
+        * @param string|bool $timestamp MW_timestamp (optional)
+        *
+        * @return bool|OldLocalFile
+        */
+       static function newFromKey( $sha1, $repo, $timestamp = false ) {
+               $dbr = $repo->getSlaveDB();
+
+               $conds = array( 'oi_sha1' => $sha1 );
+               if ( $timestamp ) {
+                       $conds['oi_timestamp'] = $dbr->timestamp( $timestamp );
+               }
+
+               $row = $dbr->selectRow( 'oldimage', self::selectFields(), $conds, __METHOD__ );
+               if ( $row ) {
+                       return self::newFromRow( $row, $repo );
+               } else {
+                       return false;
+               }
+       }
+       
+       /**
+        * Fields in the oldimage table
+        */
+       static function selectFields() {
+               return array(
+                       'oi_name',
+                       'oi_archive_name',
+                       'oi_size',
+                       'oi_width',
+                       'oi_height',
+                       'oi_metadata',
+                       'oi_bits',
+                       'oi_media_type',
+                       'oi_major_mime',
+                       'oi_minor_mime',
+                       'oi_description',
+                       'oi_user',
+                       'oi_user_text',
+                       'oi_timestamp',
+                       'oi_deleted',
+                       'oi_sha1',
+               );
+       }
+
+       /**
+        * @param $title Title
+        * @param $repo FileRepo
+        * @param $time String: timestamp or null to load by archive name
+        * @param $archiveName String: archive name or null to load by timestamp
+        */
+       function __construct( $title, $repo, $time, $archiveName ) {
+               parent::__construct( $title, $repo );
+               $this->requestedTime = $time;
+               $this->archive_name = $archiveName;
+               if ( is_null( $time ) && is_null( $archiveName ) ) {
+                       throw new MWException( __METHOD__.': must specify at least one of $time or $archiveName' );
+               }
+       }
+
+       function getCacheKey() {
+               return false;
+       }
+
+       function getArchiveName() {
+               if ( !isset( $this->archive_name ) ) {
+                       $this->load();
+               }
+               return $this->archive_name;
+       }
+
+       function isOld() {
+               return true;
+       }
+
+       function isVisible() {
+               return $this->exists() && !$this->isDeleted(File::DELETED_FILE);
+       }
+
+       function loadFromDB() {
+               wfProfileIn( __METHOD__ );
+               $this->dataLoaded = true;
+               $dbr = $this->repo->getSlaveDB();
+               $conds = array( 'oi_name' => $this->getName() );
+               if ( is_null( $this->requestedTime ) ) {
+                       $conds['oi_archive_name'] = $this->archive_name;
+               } else {
+                       $conds[] = 'oi_timestamp = ' . $dbr->addQuotes( $dbr->timestamp( $this->requestedTime ) );
+               }
+               $row = $dbr->selectRow( 'oldimage', $this->getCacheFields( 'oi_' ),
+                       $conds, __METHOD__, array( 'ORDER BY' => 'oi_timestamp DESC' ) );
+               if ( $row ) {
+                       $this->loadFromRow( $row, 'oi_' );
+               } else {
+                       $this->fileExists = false;
+               }
+               wfProfileOut( __METHOD__ );
+       }
+
+       function getCacheFields( $prefix = 'img_' ) {
+               $fields = parent::getCacheFields( $prefix );
+               $fields[] = $prefix . 'archive_name';
+               $fields[] = $prefix . 'deleted';
+               return $fields;
+       }
+
+       function getRel() {
+               return 'archive/' . $this->getHashPath() . $this->getArchiveName();
+       }
+
+       function getUrlRel() {
+               return 'archive/' . $this->getHashPath() . rawurlencode( $this->getArchiveName() );
+       }
+
+       function upgradeRow() {
+               wfProfileIn( __METHOD__ );
+               $this->loadFromFile();
+
+               # Don't destroy file info of missing files
+               if ( !$this->fileExists ) {
+                       wfDebug( __METHOD__.": file does not exist, aborting\n" );
+                       wfProfileOut( __METHOD__ );
+                       return;
+               }
+
+               $dbw = $this->repo->getMasterDB();
+               list( $major, $minor ) = self::splitMime( $this->mime );
+
+               wfDebug(__METHOD__.': upgrading '.$this->archive_name." to the current schema\n");
+               $dbw->update( 'oldimage',
+                       array(
+                               'oi_width' => $this->width,
+                               'oi_height' => $this->height,
+                               'oi_bits' => $this->bits,
+                               'oi_media_type' => $this->media_type,
+                               'oi_major_mime' => $major,
+                               'oi_minor_mime' => $minor,
+                               'oi_metadata' => $this->metadata,
+                               'oi_sha1' => $this->sha1,
+                       ), array(
+                               'oi_name' => $this->getName(),
+                               'oi_archive_name' => $this->archive_name ),
+                       __METHOD__
+               );
+               wfProfileOut( __METHOD__ );
+       }
+
+       /**
+        * @param $field Integer: one of DELETED_* bitfield constants
+        *               for file or revision rows
+        * @return bool
+        */
+       function isDeleted( $field ) {
+               $this->load();
+               return ($this->deleted & $field) == $field;
+       }
+
+       /**
+        * Returns bitfield value
+        * @return int
+        */
+       function getVisibility() {
+               $this->load();
+               return (int)$this->deleted;
+       }
+
+       /**
+        * Determine if the current user is allowed to view a particular
+        * field of this image file, if it's marked as deleted.
+        *
+        * @param $field Integer
+        * @param $user User object to check, or null to use $wgUser
+        * @return bool
+        */
+       function userCan( $field, User $user = null ) {
+               $this->load();
+               return Revision::userCanBitfield( $this->deleted, $field, $user );
+       }
+       
+       /**
+        * Upload a file directly into archive. Generally for Special:Import.
+        * 
+        * @param $srcPath string File system path of the source file
+        * @param $archiveName string Full archive name of the file, in the form 
+        *      $timestamp!$filename, where $filename must match $this->getName()
+        *
+        * @return FileRepoStatus
+        */
+       function uploadOld( $srcPath, $archiveName, $timestamp, $comment, $user, $flags = 0 ) {
+               $this->lock();
+               
+               $dstRel = 'archive/' . $this->getHashPath() . $archiveName;
+               $status = $this->publishTo( $srcPath, $dstRel,
+                       $flags & File::DELETE_SOURCE ? FileRepo::DELETE_SOURCE : 0
+               );
+               
+               if ( $status->isGood() ) {
+                       if ( !$this->recordOldUpload( $srcPath, $archiveName, $timestamp, $comment, $user ) ) {
+                               $status->fatal( 'filenotfound', $srcPath );
+                       }
+               }
+               
+               $this->unlock();
+               
+               return $status;
+       }
+       
+       /**
+        * Record a file upload in the oldimage table, without adding log entries.
+        * 
+        * @param $srcPath string File system path to the source file
+        * @param $archiveName string The archive name of the file
+        * @param $comment string Upload comment
+        * @param $user User User who did this upload
+        * @return bool
+        */
+       function recordOldUpload( $srcPath, $archiveName, $timestamp, $comment, $user ) {
+               $dbw = $this->repo->getMasterDB();
+               $dbw->begin();
+
+               $dstPath = $this->repo->getZonePath( 'public' ) . '/' . $this->getRel();
+               $props = self::getPropsFromPath( $dstPath );
+               if ( !$props['fileExists'] ) {
+                       return false;
+               }
+
+               $dbw->insert( 'oldimage',
+                       array(
+                               'oi_name'         => $this->getName(),
+                               'oi_archive_name' => $archiveName,
+                               'oi_size'         => $props['size'],
+                               'oi_width'        => intval( $props['width'] ),
+                               'oi_height'       => intval( $props['height'] ),
+                               'oi_bits'         => $props['bits'],
+                               'oi_timestamp'    => $dbw->timestamp( $timestamp ),
+                               'oi_description'  => $comment,
+                               'oi_user'         => $user->getId(),
+                               'oi_user_text'    => $user->getName(),
+                               'oi_metadata'     => $props['metadata'],
+                               'oi_media_type'   => $props['media_type'],
+                               'oi_major_mime'   => $props['major_mime'],
+                               'oi_minor_mime'   => $props['minor_mime'],
+                               'oi_sha1'         => $props['sha1'],
+                       ), __METHOD__
+               );
+
+               $dbw->commit();
+
+               return true;
+       }
+       
+}
diff --git a/includes/filerepo/file/UnregisteredLocalFile.php b/includes/filerepo/file/UnregisteredLocalFile.php
new file mode 100644 (file)
index 0000000..6a0e097
--- /dev/null
@@ -0,0 +1,143 @@
+<?php
+/**
+ * File without associated database record
+ *
+ * @file
+ * @ingroup FileRepo
+ */
+
+/**
+ * A file object referring to either a standalone local file, or a file in a
+ * local repository with no database, for example an FSRepo repository.
+ *
+ * Read-only.
+ *
+ * TODO: Currently it doesn't really work in the repository role, there are
+ * lots of functions missing. It is used by the WebStore extension in the
+ * standalone role.
+ *
+ * @ingroup FileRepo
+ */
+class UnregisteredLocalFile extends File {
+       var $title, $path, $mime, $dims;
+
+       /**
+        * @var MediaHandler
+        */
+       var $handler;
+
+       /**
+        * @param $path
+        * @param $mime
+        * @return UnregisteredLocalFile
+        */
+       static function newFromPath( $path, $mime ) {
+               return new self( false, false, $path, $mime );
+       }
+
+       /**
+        * @param $title
+        * @param $repo
+        * @return UnregisteredLocalFile
+        */
+       static function newFromTitle( $title, $repo ) {
+               return new self( $title, $repo, false, false );
+       }
+
+       /**
+        * Create an UnregisteredLocalFile based on a path or a (title,repo) pair.
+        * A FileRepo object is not required here, unlike most other File classes.
+        * 
+        * @throws MWException
+        * @param $title Title|false
+        * @param $repo FSRepo
+        * @param $path string
+        * @param $mime string
+        */
+       function __construct( $title = false, $repo = false, $path = false, $mime = false ) {
+               if ( !( $title && $repo ) && !$path ) {
+                       throw new MWException( __METHOD__.': not enough parameters, must specify title and repo, or a full path' );
+               }
+               if ( $title instanceof Title ) {
+                       $this->title = File::normalizeTitle( $title, 'exception' );
+                       $this->name = $repo->getNameFromTitle( $title );
+               } else {
+                       $this->name = basename( $path );
+                       $this->title = File::normalizeTitle( $this->name, 'exception' );
+               }
+               $this->repo = $repo;
+               if ( $path ) {
+                       $this->path = $path;
+               } else {
+                       $this->path = $repo->getRootDirectory() . '/' .
+                               $repo->getHashPath( $this->name ) . $this->name;
+               }
+               if ( $mime ) {
+                       $this->mime = $mime;
+               }
+               $this->dims = array();
+       }
+
+       private function cachePageDimensions( $page = 1 ) {
+               if ( !isset( $this->dims[$page] ) ) {
+                       if ( !$this->getHandler() ) {
+                               return false;
+                       }
+                       $this->dims[$page] = $this->handler->getPageDimensions( $this, $page );
+               }
+               return $this->dims[$page];
+       }
+
+       function getWidth( $page = 1 ) {
+               $dim = $this->cachePageDimensions( $page );
+               return $dim['width'];
+       }
+
+       function getHeight( $page = 1 ) {
+               $dim = $this->cachePageDimensions( $page );
+               return $dim['height'];
+       }
+
+       function getMimeType() {
+               if ( !isset( $this->mime ) ) {
+                       $magic = MimeMagic::singleton();
+                       $this->mime = $magic->guessMimeType( $this->getPath() );
+               }
+               return $this->mime;
+       }
+
+       function getImageSize( $filename ) {
+               if ( !$this->getHandler() ) {
+                       return false;
+               }
+               return $this->handler->getImageSize( $this, $this->getPath() );
+       }
+
+       function getMetadata() {
+               if ( !isset( $this->metadata ) ) {
+                       if ( !$this->getHandler() ) {
+                               $this->metadata = false;
+                       } else {
+                               $this->metadata = $this->handler->getMetadata( $this, $this->getPath() );
+                       }
+               }
+               return $this->metadata;
+       }
+
+       function getURL() {
+               if ( $this->repo ) {
+                       return $this->repo->getZoneUrl( 'public' ) . '/' .
+                               $this->repo->getHashPath( $this->name ) . rawurlencode( $this->name );
+               } else {
+                       return false;
+               }
+       }
+
+       function getSize() {
+               if ( file_exists( $this->path ) ) {
+                       return filesize( $this->path );
+               } else {
+                       return false;
+               }
+       }
+}