Revert r43788 and r43788 (adding findBySha1 functionality). Something is breaking...
[lhc/web/wiklou.git] / includes / filerepo / FSRepo.php
index af5aa63..d561e61 100644 (file)
@@ -3,84 +3,26 @@
 /**
  * A repository for files accessible via the local filesystem. Does not support
  * database access or registration.
- *
- * TODO: split off abstract base FileRepo
+ * @ingroup FileRepo
  */
-
-class FSRepo {
-       const DELETE_SOURCE = 1;
-
-       var $directory, $url, $hashLevels, $thumbScriptUrl, $transformVia404;
-       var $descBaseUrl, $scriptDirUrl, $articleUrl, $fetchDescription;
+class FSRepo extends FileRepo {
+       var $directory, $deletedDir, $url, $deletedHashLevels;
        var $fileFactory = array( 'UnregisteredLocalFile', 'newFromTitle' );
        var $oldFileFactory = false;
+       var $pathDisclosureProtection = 'simple';
 
        function __construct( $info ) {
+               parent::__construct( $info );
+
                // Required settings
-               $this->name = $info['name'];
                $this->directory = $info['directory'];
                $this->url = $info['url'];
-               $this->hashLevels = $info['hashLevels'];
-               $this->transformVia404 = !empty( $info['transformVia404'] );
 
                // Optional settings
-               foreach ( array( 'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription', 
-                       'thumbScriptUrl' ) as $var ) 
-               {
-                       if ( isset( $info[$var] ) ) {
-                               $this->$var = $info[$var];
-                       }
-               }
-       }
-
-       /**
-        * Create a new File object from the local repository
-        * @param mixed $title Title object or string
-        * @param mixed $time Time at which the image is supposed to have existed. 
-        *                    If this is specified, the returned object will be an 
-        *                    instance of the repository's old file class instead of
-        *                    a current file. Repositories not supporting version 
-        *                    control should return false if this parameter is set.
-        */
-       function newFile( $title, $time = false ) {
-               if ( !($title instanceof Title) ) {
-                       $title = Title::makeTitleSafe( NS_IMAGE, $title );
-                       if ( !is_object( $title ) ) {
-                               return null;
-                       }
-               }
-               if ( $time ) {
-                       if ( $this->oldFileFactory ) {
-                               return call_user_func( $this->oldFileFactory, $title, $this, $time );
-                       } else {
-                               return false;
-                       }
-               } else {
-                       return call_user_func( $this->fileFactory, $title, $this );
-               }
-       }
-
-       /**
-        * Find an instance of the named file that existed at the specified time
-        * Returns false if the file did not exist. Repositories not supporting 
-        * version control should return false if the time is specified.
-        *
-        * @param mixed $time 14-character timestamp, or false for the current version
-        */
-       function findFile( $title, $time = false ) {
-               # First try the current version of the file to see if it precedes the timestamp
-               $img = $this->newFile( $title );
-               if ( !$img ) {
-                       return false;
-               }
-               if ( $img->exists() && ( !$time || $img->getTimestamp() <= $time ) ) {
-                       return $img;
-               }
-               # Now try an old version of the file
-               $img = $this->newFile( $title, $time );
-               if ( $img->exists() ) {
-                       return $img;
-               }
+               $this->hashLevels = isset( $info['hashLevels'] ) ? $info['hashLevels'] : 2;
+               $this->deletedHashLevels = isset( $info['deletedHashLevels'] ) ?
+                       $info['deletedHashLevels'] : $this->hashLevels;
+               $this->deletedDir = isset( $info['deletedDir'] ) ? $info['deletedDir'] : false;
        }
 
        /**
@@ -104,20 +46,6 @@ class FSRepo {
                return (bool)$this->hashLevels;
        }
 
-       /**
-        * Get the URL of thumb.php
-        */
-       function getThumbScriptUrl() {
-               return $this->thumbScriptUrl;
-       }
-
-       /**
-        * Returns true if the repository can transform files via a 404 handler
-        */
-       function canTransformVia404() {
-               return $this->transformVia404;
-       }
-
        /**
         * Get the local directory corresponding to one of the three basic zones
         */
@@ -128,7 +56,7 @@ class FSRepo {
                        case 'temp':
                                return "{$this->directory}/temp";
                        case 'deleted':
-                               return $GLOBALS['wgFileStore']['deleted']['directory'];
+                               return $this->deletedDir;
                        default:
                                return false;
                }
@@ -144,7 +72,7 @@ class FSRepo {
                        case 'temp':
                                return "{$this->url}/temp";
                        case 'deleted':
-                               return $GLOBALS['wgFileStore']['deleted']['url'];
+                               return false; // no public URL
                        default:
                                return false;
                }
@@ -152,11 +80,13 @@ class FSRepo {
 
        /**
         * Get a URL referring to this repository, with the private mwrepo protocol.
+        * The suffix, if supplied, is considered to be unencoded, and will be
+        * URL-encoded before being returned.
         */
        function getVirtualUrl( $suffix = false ) {
-               $path = 'mwrepo://';
+               $path = 'mwrepo://' . $this->name;
                if ( $suffix !== false ) {
-                       $path .= '/' . $suffix;
+                       $path .= '/' . rawurlencode( $suffix );
                }
                return $path;
        }
@@ -173,209 +103,364 @@ class FSRepo {
                if ( count( $bits ) != 3 ) {
                        throw new MWException( __METHOD__.": invalid mwrepo URL: $url" );
                }
-               list( $host, $zone, $rel ) = $bits;
-               if ( $host !== '' ) {
+               list( $repo, $zone, $rel ) = $bits;
+               if ( $repo !== $this->name ) {
                        throw new MWException( __METHOD__.": fetching from a foreign repo is not supported" );
                }
                $base = $this->getZonePath( $zone );
                if ( !$base ) {
                        throw new MWException( __METHOD__.": invalid zone: $zone" );
                }
-               return $base . '/' . urldecode( $rel );
+               return $base . '/' . rawurldecode( $rel );
        }
 
        /**
-        * Store a file to a given destination.
+        * Store a batch of files
+        *
+        * @param array $triplets (src,zone,dest) triplets as per store()
+        * @param integer $flags Bitwise combination of the following flags:
+        *     self::DELETE_SOURCE     Delete the source file after upload
+        *     self::OVERWRITE         Overwrite an existing destination file instead of failing
+        *     self::OVERWRITE_SAME    Overwrite the file if the destination exists and has the
+        *                             same contents as the source
         */
-       function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) {
-               $root = $this->getZonePath( $dstZone );
-               if ( !$root ) {
-                       throw new MWException( "Invalid zone: $dstZone" );
+       function storeBatch( $triplets, $flags = 0 ) {
+               if ( !wfMkdirParents( $this->directory ) ) {
+                       return $this->newFatal( 'upload_directory_missing', $this->directory );
+               }
+               if ( !is_writable( $this->directory ) ) {
+                       return $this->newFatal( 'upload_directory_read_only', $this->directory );
                }
-               $dstPath = "$root/$dstRel";
+               $status = $this->newGood();
+               foreach ( $triplets as $i => $triplet ) {
+                       list( $srcPath, $dstZone, $dstRel ) = $triplet;
+
+                       $root = $this->getZonePath( $dstZone );
+                       if ( !$root ) {
+                               throw new MWException( "Invalid zone: $dstZone" );
+                       }
+                       if ( !$this->validateFilename( $dstRel ) ) {
+                               throw new MWException( 'Validation error in $dstRel' );
+                       }
+                       $dstPath = "$root/$dstRel";
+                       $dstDir = dirname( $dstPath );
+
+                       if ( !is_dir( $dstDir ) ) {
+                               if ( !wfMkdirParents( $dstDir ) ) {
+                                       return $this->newFatal( 'directorycreateerror', $dstDir );
+                               }
+                               if ( $dstZone == 'deleted' ) {
+                                       $this->initDeletedDir( $dstDir );
+                               }
+                       }
 
-               if ( !is_dir( dirname( $dstPath ) ) ) {
-                       wfMkdirParents( dirname( $dstPath ) );
+                       if ( self::isVirtualUrl( $srcPath ) ) {
+                               $srcPath = $triplets[$i][0] = $this->resolveVirtualUrl( $srcPath );
+                       }
+                       if ( !is_file( $srcPath ) ) {
+                               // Make a list of files that don't exist for return to the caller
+                               $status->fatal( 'filenotfound', $srcPath );
+                               continue;
+                       }
+                       if ( !( $flags & self::OVERWRITE ) && file_exists( $dstPath ) ) {
+                               if ( $flags & self::OVERWRITE_SAME ) {
+                                       $hashSource = sha1_file( $srcPath );
+                                       $hashDest = sha1_file( $dstPath );
+                                       if ( $hashSource != $hashDest ) {
+                                               $status->fatal( 'fileexistserror', $dstPath );
+                                       }
+                               } else {
+                                       $status->fatal( 'fileexistserror', $dstPath );
+                               }
+                       }
                }
-                       
-               if ( substr( $srcPath, 0, 9 ) == 'mwrepo://' ) {
-                       $srcPath = $this->resolveVirtualUrl( $srcPath );
+
+               $deleteDest = wfIsWindows() && ( $flags & self::OVERWRITE );
+
+               // Abort now on failure
+               if ( !$status->ok ) {
+                       return $status;
                }
 
-               if ( $flags & self::DELETE_SOURCE ) {
-                       if ( !rename( $srcPath, $dstPath ) ) {
-                               return new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $srcPath ), 
-                                       wfEscapeWikiText( $dstPath ) );
+               foreach ( $triplets as $triplet ) {
+                       list( $srcPath, $dstZone, $dstRel ) = $triplet;
+                       $root = $this->getZonePath( $dstZone );
+                       $dstPath = "$root/$dstRel";
+                       $good = true;
+
+                       if ( $flags & self::DELETE_SOURCE ) {
+                               if ( $deleteDest ) {
+                                       unlink( $dstPath );
+                               }
+                               if ( !rename( $srcPath, $dstPath ) ) {
+                                       $status->error( 'filerenameerror', $srcPath, $dstPath );
+                                       $good = false;
+                               }
+                       } else {
+                               if ( !copy( $srcPath, $dstPath ) ) {
+                                       $status->error( 'filecopyerror', $srcPath, $dstPath );
+                                       $good = false;
+                               }
                        }
-               } else {
-                       if ( !copy( $srcPath, $dstPath ) ) {
-                               return new WikiErrorMsg( 'filecopyerror', wfEscapeWikiText( $srcPath ),
-                                       wfEscapeWikiText( $dstPath ) );
+                       if ( $good ) {
+                               chmod( $dstPath, 0644 );
+                               $status->successCount++;
+                       } else {
+                               $status->failCount++;
                        }
                }
-               chmod( $dstPath, 0644 );
-               return true;
+               return $status;
+       }
+
+       /**
+        * Take all available measures to prevent web accessibility of new deleted
+        * directories, in case the user has not configured offline storage
+        */
+       protected function initDeletedDir( $dir ) {
+               // Add a .htaccess file to the root of the deleted zone
+               $root = $this->getZonePath( 'deleted' );
+               if ( !file_exists( "$root/.htaccess" ) ) {
+                       file_put_contents( "$root/.htaccess", "Deny from all\n" );
+               }
+               // Seed new directories with a blank index.html, to prevent crawling
+               file_put_contents( "$dir/index.html", '' );
        }
 
        /**
         * Pick a random name in the temp zone and store a file to it.
-        * Returns the URL, or a WikiError on failure.
-        * @param string $originalName The base name of the file as specified 
+        * @param string $originalName The base name of the file as specified
         *     by the user. The file extension will be maintained.
         * @param string $srcPath The current location of the file.
+        * @return FileRepoStatus object with the URL in the value.
         */
        function storeTemp( $originalName, $srcPath ) {
-               $dstRel = $this->getHashPath( $originalName ) . 
-                       gmdate( "YmdHis" ) . '!' . $originalName;
+               $date = gmdate( "YmdHis" );
+               $hashPath = $this->getHashPath( $originalName );
+               $dstRel = "$hashPath$date!$originalName";
+               $dstUrlRel = $hashPath . $date . '!' . rawurlencode( $originalName );
+
                $result = $this->store( $srcPath, 'temp', $dstRel );
-               if ( WikiError::isError( $result ) ) {
-                       return $result;
-               } else {
-                       return $this->getVirtualUrl( "temp/$dstRel" );
-               }
+               $result->value = $this->getVirtualUrl( 'temp' ) . '/' . $dstUrlRel;
+               return $result;
        }
 
        /**
-        * Copy or move a file either from the local filesystem or from an mwrepo://
-        * virtual URL, into this repository at the specified destination location.
-        *
-        * @param string $srcPath The source path or URL
-        * @param string $dstPath The destination relative path
-        * @param string $archivePath The relative path where the existing file is to
-        *        be archived, if there is one.
-        * @param integer $flags Bitfield, may be FSRepo::DELETE_SOURCE to indicate
-        *        that the source file should be deleted if possible
+        * Remove a temporary file or mark it for garbage collection
+        * @param string $virtualUrl The virtual URL returned by storeTemp
+        * @return boolean True on success, false on failure
         */
-       function publish( $srcPath, $dstPath, $archivePath, $flags = 0 ) {
-               if ( substr( $srcPath, 0, 9 ) == 'mwrepo://' ) {
-                       $srcPath = $this->resolveVirtualUrl( $srcPath );
+       function freeTemp( $virtualUrl ) {
+               $temp = "mwrepo://{$this->name}/temp";
+               if ( substr( $virtualUrl, 0, strlen( $temp ) ) != $temp ) {
+                       wfDebug( __METHOD__.": Invalid virtual URL\n" );
+                       return false;
                }
-               $dstDir = dirname( $dstPath );
-               if ( !is_dir( $dstDir ) ) wfMkdirParents( $dstDir );
-
-               if( is_file( $dstPath ) ) {
-                       $archiveDir = dirname( $archivePath );
-                       if ( !is_dir( $archiveDir ) ) wfMkdirParents( $archiveDir );
-                       wfSuppressWarnings();
-                       $success = rename( $dstPath, $archivePath );
-                       wfRestoreWarnings();
+               $path = $this->resolveVirtualUrl( $virtualUrl );
+               wfSuppressWarnings();
+               $success = unlink( $path );
+               wfRestoreWarnings();
+               return $success;
+       }
 
-                       if( ! $success ) {
-                               return new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $dstPath ),
-                                 wfEscapeWikiText( $archivePath ) );
-                       }
-                       else wfDebug(__METHOD__.": moved file $dstPath to $archivePath\n");
-                       $status = 'archived';
+       /**
+        * Publish a batch of files
+        * @param array $triplets (source,dest,archive) triplets as per publish()
+        * @param integer $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate
+        *        that the source files should be deleted if possible
+        */
+       function publishBatch( $triplets, $flags = 0 ) {
+               // Perform initial checks
+               if ( !wfMkdirParents( $this->directory ) ) {
+                       return $this->newFatal( 'upload_directory_missing', $this->directory );
                }
-               else {
-                       $status = 'new';
+               if ( !is_writable( $this->directory ) ) {
+                       return $this->newFatal( 'upload_directory_read_only', $this->directory );
                }
+               $status = $this->newGood( array() );
+               foreach ( $triplets as $i => $triplet ) {
+                       list( $srcPath, $dstRel, $archiveRel ) = $triplet;
 
-               $error = false;
-               wfSuppressWarnings();
-               if ( $flags & self::DELETE_SOURCE ) {
-                       if ( !rename( $srcPath, $dstPath ) ) {
-                               $error = new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $srcPath ), 
-                               wfEscapeWikiText( $dstPath ) );
+                       if ( substr( $srcPath, 0, 9 ) == 'mwrepo://' ) {
+                               $triplets[$i][0] = $srcPath = $this->resolveVirtualUrl( $srcPath );
+                       }
+                       if ( !$this->validateFilename( $dstRel ) ) {
+                               throw new MWException( 'Validation error in $dstRel' );
+                       }
+                       if ( !$this->validateFilename( $archiveRel ) ) {
+                               throw new MWException( 'Validation error in $archiveRel' );
+                       }
+                       $dstPath = "{$this->directory}/$dstRel";
+                       $archivePath = "{$this->directory}/$archiveRel";
+
+                       $dstDir = dirname( $dstPath );
+                       $archiveDir = dirname( $archivePath );
+                       // Abort immediately on directory creation errors since they're likely to be repetitive
+                       if ( !is_dir( $dstDir ) && !wfMkdirParents( $dstDir ) ) {
+                               return $this->newFatal( 'directorycreateerror', $dstDir );
                        }
-               } else {
-                       if ( !copy( $srcPath, $dstPath ) ) {
-                               $error = new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $srcPath ), 
-                                       wfEscapeWikiText( $dstPath ) );
+                       if ( !is_dir( $archiveDir ) && !wfMkdirParents( $archiveDir ) ) {
+                               return $this->newFatal( 'directorycreateerror', $archiveDir );
+                       }
+                       if ( !is_file( $srcPath ) ) {
+                               // Make a list of files that don't exist for return to the caller
+                               $status->fatal( 'filenotfound', $srcPath );
                        }
                }
-               wfRestoreWarnings();
 
-               if( $error ) {
-                       return $error;
-               } else {
-                       wfDebug(__METHOD__.": wrote tempfile $srcPath to $dstPath\n");
+               if ( !$status->ok ) {
+                       return $status;
                }
 
-               chmod( $dstPath, 0644 );
-               return $status;
-       }
-       
-       /**
-        * Get a relative path including trailing slash, e.g. f/fa/
-        * If the repo is not hashed, returns an empty string
-        */
-       function getHashPath( $name ) {
-               if ( $this->isHashed() ) {
-                       $hash = md5( $name );
-                       $path = '';
-                       for ( $i = 1; $i <= $this->hashLevels; $i++ ) {
-                               $path .= substr( $hash, 0, $i ) . '/';
+               foreach ( $triplets as $i => $triplet ) {
+                       list( $srcPath, $dstRel, $archiveRel ) = $triplet;
+                       $dstPath = "{$this->directory}/$dstRel";
+                       $archivePath = "{$this->directory}/$archiveRel";
+
+                       // Archive destination file if it exists
+                       if( is_file( $dstPath ) ) {
+                               // Check if the archive file exists
+                               // This is a sanity check to avoid data loss. In UNIX, the rename primitive
+                               // unlinks the destination file if it exists. DB-based synchronisation in
+                               // publishBatch's caller should prevent races. In Windows there's no
+                               // problem because the rename primitive fails if the destination exists.
+                               if ( is_file( $archivePath ) ) {
+                                       $success = false;
+                               } else {
+                                       wfSuppressWarnings();
+                                       $success = rename( $dstPath, $archivePath );
+                                       wfRestoreWarnings();
+                               }
+
+                               if( !$success ) {
+                                       $status->error( 'filerenameerror',$dstPath, $archivePath );
+                                       $status->failCount++;
+                                       continue;
+                               } else {
+                                       wfDebug(__METHOD__.": moved file $dstPath to $archivePath\n");
+                               }
+                               $status->value[$i] = 'archived';
+                       } else {
+                               $status->value[$i] = 'new';
                        }
-                       return $path;
-               } else {
-                       return '';
-               }
-       }
 
-       /**
-        * Get the name of this repository, as specified by $info['name]' to the constructor
-        */
-       function getName() {
-               return $this->name;
-       }
+                       $good = true;
+                       wfSuppressWarnings();
+                       if ( $flags & self::DELETE_SOURCE ) {
+                               if ( !rename( $srcPath, $dstPath ) ) {
+                                       $status->error( 'filerenameerror', $srcPath, $dstPath );
+                                       $good = false;
+                               }
+                       } else {
+                               if ( !copy( $srcPath, $dstPath ) ) {
+                                       $status->error( 'filecopyerror', $srcPath, $dstPath );
+                                       $good = false;
+                               }
+                       }
+                       wfRestoreWarnings();
 
-       /**
-        * Get the file description page base URL, or false if there isn't one.
-        * @private
-        */
-       function getDescBaseUrl() {
-               if ( is_null( $this->descBaseUrl ) ) {
-                       if ( !is_null( $this->articleUrl ) ) {
-                               $this->descBaseUrl = str_replace( '$1', 
-                                       urlencode( Namespace::getCanonicalName( NS_IMAGE ) ) . ':', $this->articleUrl );
-                       } elseif ( !is_null( $this->scriptDirUrl ) ) {
-                               $this->descBaseUrl = $this->scriptDirUrl . '/index.php?title=' . 
-                                       urlencode( Namespace::getCanonicalName( NS_IMAGE ) ) . ':';
+                       if ( $good ) {
+                               $status->successCount++;
+                               wfDebug(__METHOD__.": wrote tempfile $srcPath to $dstPath\n");
+                               // Thread-safe override for umask
+                               chmod( $dstPath, 0644 );
                        } else {
-                               $this->descBaseUrl = false;
+                               $status->failCount++;
                        }
                }
-               return $this->descBaseUrl;
+               return $status;
        }
 
        /**
-        * Get the URL of an image description page. May return false if it is
-        * unknown or not applicable. In general this should only be called by the 
-        * File class, since it may return invalid results for certain kinds of 
-        * repositories. Use File::getDescriptionUrl() in user code.
+        * Move a group of files to the deletion archive.
+        * If no valid deletion archive is configured, this may either delete the
+        * file or throw an exception, depending on the preference of the repository.
         *
-        * In particular, it uses the article paths as specified to the repository
-        * constructor, whereas local repositories use the local Title functions.
+        * @param array $sourceDestPairs Array of source/destination pairs. Each element
+        *        is a two-element array containing the source file path relative to the
+        *        public root in the first element, and the archive file path relative
+        *        to the deleted zone root in the second element.
+        * @return FileRepoStatus
         */
-       function getDescriptionUrl( $name ) {
-               $base = $this->getDescBaseUrl();
-               if ( $base ) {
-                       return $base . wfUrlencode( $name );
-               } else {
-                       return false;
+       function deleteBatch( $sourceDestPairs ) {
+               $status = $this->newGood();
+               if ( !$this->deletedDir ) {
+                       throw new MWException( __METHOD__.': no valid deletion archive directory' );
                }
+
+               /**
+                * Validate filenames and create archive directories
+                */
+               foreach ( $sourceDestPairs as $pair ) {
+                       list( $srcRel, $archiveRel ) = $pair;
+                       if ( !$this->validateFilename( $srcRel ) ) {
+                               throw new MWException( __METHOD__.':Validation error in $srcRel' );
+                       }
+                       if ( !$this->validateFilename( $archiveRel ) ) {
+                               throw new MWException( __METHOD__.':Validation error in $archiveRel' );
+                       }
+                       $archivePath = "{$this->deletedDir}/$archiveRel";
+                       $archiveDir = dirname( $archivePath );
+                       if ( !is_dir( $archiveDir ) ) {
+                               if ( !wfMkdirParents( $archiveDir ) ) {
+                                       $status->fatal( 'directorycreateerror', $archiveDir );
+                                       continue;
+                               }
+                               $this->initDeletedDir( $archiveDir );
+                       }
+                       // Check if the archive directory is writable
+                       // This doesn't appear to work on NTFS
+                       if ( !is_writable( $archiveDir ) ) {
+                               $status->fatal( 'filedelete-archive-read-only', $archiveDir );
+                       }
+               }
+               if ( !$status->ok ) {
+                       // Abort early
+                       return $status;
+               }
+
+               /**
+                * Move the files
+                * We're now committed to returning an OK result, which will lead to
+                * the files being moved in the DB also.
+                */
+               foreach ( $sourceDestPairs as $pair ) {
+                       list( $srcRel, $archiveRel ) = $pair;
+                       $srcPath = "{$this->directory}/$srcRel";
+                       $archivePath = "{$this->deletedDir}/$archiveRel";
+                       $good = true;
+                       if ( file_exists( $archivePath ) ) {
+                               # A file with this content hash is already archived
+                               if ( !@unlink( $srcPath ) ) {
+                                       $status->error( 'filedeleteerror', $srcPath );
+                                       $good = false;
+                               }
+                       } else{
+                               if ( !@rename( $srcPath, $archivePath ) ) {
+                                       $status->error( 'filerenameerror', $srcPath, $archivePath );
+                                       $good = false;
+                               } else {
+                                       @chmod( $archivePath, 0644 );
+                               }
+                       }
+                       if ( $good ) {
+                               $status->successCount++;
+                       } else {
+                               $status->failCount++;
+                       }
+               }
+               return $status;
        }
 
        /**
-        * Get the URL of the content-only fragment of the description page. For 
-        * MediaWiki this means action=render. This should only be called by the 
-        * repository's file class, since it may return invalid results. User code 
-        * should use File::getDescriptionText().
+        * Get a relative path for a deletion archive key,
+        * e.g. s/z/a/ for sza251lrxrc1jad41h5mgilp8nysje52.jpg
         */
-       function getDescriptionRenderUrl( $name ) {
-               if ( isset( $this->scriptDirUrl ) ) {
-                       return $this->scriptDirUrl . '/index.php?title=' . 
-                               wfUrlencode( Namespace::getCanonicalName( NS_IMAGE ) . ':' . $name ) .
-                               '&action=render';
-               } else {
-                       $descBase = $this->getDescBaseUrl();
-                       if ( $descBase ) {
-                               return wfAppendQuery( $descBase . wfUrlencode( $name ), 'action=render' );
-                       } else {
-                               return false;
-                       }
+       function getDeletedHashPath( $key ) {
+               $path = '';
+               for ( $i = 0; $i < $this->deletedHashLevels; $i++ ) {
+                       $path .= $key[$i] . '/';
                }
+               return $path;
        }
 
        /**
@@ -401,12 +486,52 @@ class FSRepo {
        }
 
        /**
-        * Call a callaback function for every file in the repository
+        * Call a callback function for every file in the repository
         * May use either the database or the filesystem
         */
        function enumFiles( $callback ) {
                $this->enumFilesInFS( $callback );
        }
-}
 
-?>
+       /**
+        * Get properties of a file with a given virtual URL
+        * The virtual URL must refer to this repo
+        */
+       function getFileProps( $virtualUrl ) {
+               $path = $this->resolveVirtualUrl( $virtualUrl );
+               return File::getPropsFromPath( $path );
+       }
+
+       /**
+        * Path disclosure protection functions
+        *
+        * Get a callback function to use for cleaning error message parameters
+        */
+       function getErrorCleanupFunction() {
+               switch ( $this->pathDisclosureProtection ) {
+                       case 'simple':
+                               $callback = array( $this, 'simpleClean' );
+                               break;
+                       default:
+                               $callback = parent::getErrorCleanupFunction();
+               }
+               return $callback;
+       }
+
+       function simpleClean( $param ) {
+               if ( !isset( $this->simpleCleanPairs ) ) {
+                       global $IP;
+                       $this->simpleCleanPairs = array(
+                               $this->directory => 'public',
+                               "{$this->directory}/temp" => 'temp',
+                               $IP => '$IP',
+                               dirname( __FILE__ ) => '$IP/extensions/WebStore',
+                       );
+                       if ( $this->deletedDir ) {
+                               $this->simpleCleanPairs[$this->deletedDir] = 'deleted';
+                       }
+               }
+               return strtr( $param, $this->simpleCleanPairs );
+       }
+
+}