* Documentation fix
[lhc/web/wiklou.git] / includes / filerepo / backend / FileBackend.php
index afab3b3..eb9d4ae 100644 (file)
  * @ingroup FileBackend
  * @since 1.19
  */
-abstract class FileBackendBase {
-       protected $name; // unique backend name
-       protected $wikiId; // unique wiki name
-       protected $readOnly; // string
+abstract class FileBackend {
+       protected $name; // string; unique backend name
+       protected $wikiId; // string; unique wiki name
+       protected $readOnly; // string; read-only explanation message
        /** @var LockManager */
        protected $lockManager;
 
@@ -55,24 +55,12 @@ abstract class FileBackendBase {
                $this->wikiId = isset( $config['wikiId'] )
                        ? $config['wikiId']
                        : wfWikiID(); // e.g. "my_wiki-en_"
-               $this->wikiId = $this->resolveWikiId( $this->wikiId );
                $this->lockManager = LockManagerGroup::singleton()->get( $config['lockManager'] );
                $this->readOnly = isset( $config['readOnly'] )
                        ? (string)$config['readOnly']
                        : '';
        }
 
-       /**
-        * Normalize a wiki ID by replacing characters that are
-        * not supported by the backend as part of container names.
-        * 
-        * @param $wikiId string
-        * @return string 
-        */
-       protected function resolveWikiId( $wikiId ) {
-               return $wikiId;
-       }
-
        /**
         * Get the unique backend name.
         * We may have multiple different backends of the same type.
@@ -87,6 +75,7 @@ abstract class FileBackendBase {
        /**
         * This is the main entry point into the backend for write operations.
         * Callers supply an ordered list of operations to perform as a transaction.
+        * Files will be locked, the stat cache cleared, and then the operations attempted.
         * If any serious errors occur, all attempted operations will be rolled back.
         * 
         * $ops is an array of arrays. The outer array holds a list of operations.
@@ -154,9 +143,9 @@ abstract class FileBackendBase {
         *                         This can increase performance for non-critical writes.
         *                         This has no effect unless the 'force' flag is set.
         * 
-        * Remarks:
+        * Remarks on locking:
         * File system paths given to operations should refer to files that are
-        * either locked or otherwise safe from modification from other processes.
+        * already locked or otherwise safe from modification from other processes.
         * Normally these files will be new temp files, which should be adequate.
         * 
         * Return value:
@@ -181,7 +170,7 @@ abstract class FileBackendBase {
        }
 
        /**
-        * @see FileBackendBase::doOperations()
+        * @see FileBackend::doOperations()
         */
        abstract protected function doOperationsInternal( array $ops, array $opts );
 
@@ -190,7 +179,7 @@ abstract class FileBackendBase {
         * If you are doing a batch of operations that should either
         * all succeed or all fail, then use that function instead.
         *
-        * @see FileBackendBase::doOperations()
+        * @see FileBackend::doOperations()
         *
         * @param $op Array Operation
         * @param $opts Array Operation options
@@ -204,7 +193,7 @@ abstract class FileBackendBase {
         * Performs a single create operation.
         * This sets $params['op'] to 'create' and passes it to doOperation().
         *
-        * @see FileBackendBase::doOperation()
+        * @see FileBackend::doOperation()
         *
         * @param $params Array Operation parameters
         * @param $opts Array Operation options
@@ -219,7 +208,7 @@ abstract class FileBackendBase {
         * Performs a single store operation.
         * This sets $params['op'] to 'store' and passes it to doOperation().
         *
-        * @see FileBackendBase::doOperation()
+        * @see FileBackend::doOperation()
         *
         * @param $params Array Operation parameters
         * @param $opts Array Operation options
@@ -234,7 +223,7 @@ abstract class FileBackendBase {
         * Performs a single copy operation.
         * This sets $params['op'] to 'copy' and passes it to doOperation().
         *
-        * @see FileBackendBase::doOperation()
+        * @see FileBackend::doOperation()
         *
         * @param $params Array Operation parameters
         * @param $opts Array Operation options
@@ -249,7 +238,7 @@ abstract class FileBackendBase {
         * Performs a single move operation.
         * This sets $params['op'] to 'move' and passes it to doOperation().
         *
-        * @see FileBackendBase::doOperation()
+        * @see FileBackend::doOperation()
         *
         * @param $params Array Operation parameters
         * @param $opts Array Operation options
@@ -264,7 +253,7 @@ abstract class FileBackendBase {
         * Performs a single delete operation.
         * This sets $params['op'] to 'delete' and passes it to doOperation().
         *
-        * @see FileBackendBase::doOperation()
+        * @see FileBackend::doOperation()
         *
         * @param $params Array Operation parameters
         * @param $opts Array Operation options
@@ -276,7 +265,10 @@ abstract class FileBackendBase {
        }
 
        /**
-        * Concatenate a list of storage files into a single file on the file system
+        * Concatenate a list of storage files into a single file system file.
+        * The target path should refer to a file that is already locked or
+        * otherwise safe from modification from other processes. Normally,
+        * the file will be a new temp file, which should be adequate.
         * $params include:
         *     srcs          : ordered source storage paths (e.g. chunk1, chunk2, ...)
         *     dst           : file system path to 0-byte temp file
@@ -305,7 +297,7 @@ abstract class FileBackendBase {
        }
 
        /**
-        * @see FileBackendBase::prepare()
+        * @see FileBackend::prepare()
         */
        abstract protected function doPrepare( array $params );
 
@@ -336,7 +328,7 @@ abstract class FileBackendBase {
        }
 
        /**
-        * @see FileBackendBase::secure()
+        * @see FileBackend::secure()
         */
        abstract protected function doSecure( array $params );
 
@@ -359,7 +351,7 @@ abstract class FileBackendBase {
        }
 
        /**
-        * @see FileBackendBase::clean()
+        * @see FileBackend::clean()
         */
        abstract protected function doClean( array $params );
 
@@ -483,6 +475,7 @@ abstract class FileBackendBase {
         * Write operations should *never* be done on this file as some backends
         * may do internal tracking or may be instances of FileBackendMultiWrite.
         * In that later case, there are copies of the file that must stay in sync.
+        * Additionally, further calls to this function may return the same file.
         * 
         * $params include:
         *     src    : source storage path
@@ -528,7 +521,7 @@ abstract class FileBackendBase {
         * @param $paths Array Storage paths (optional)
         * @return void
         */
-       abstract public function clearCache( array $paths = null );
+       public function clearCache( array $paths = null ) {}
 
        /**
         * Lock the files at the given storage paths in the backend.
@@ -571,27 +564,140 @@ abstract class FileBackendBase {
        final public function getScopedFileLocks( array $paths, $type, Status $status ) {
                return ScopedLock::factory( $this->lockManager, $paths, $type, $status );
        }
+
+       /**
+        * Check if a given path is a "mwstore://" path.
+        * This does not do any further validation or any existence checks.
+        * 
+        * @param $path string
+        * @return bool
+        */
+       final public static function isStoragePath( $path ) {
+               return ( strpos( $path, 'mwstore://' ) === 0 );
+       }
+
+       /**
+        * Split a storage path into a backend name, a container name, 
+        * and a relative file path. The relative path may be the empty string.
+        * This does not do any path normalization or traversal checks.
+        *
+        * @param $storagePath string
+        * @return Array (backend, container, rel object) or (null, null, null)
+        */
+       final public static function splitStoragePath( $storagePath ) {
+               if ( self::isStoragePath( $storagePath ) ) {
+                       // Remove the "mwstore://" prefix and split the path
+                       $parts = explode( '/', substr( $storagePath, 10 ), 3 );
+                       if ( count( $parts ) >= 2 && $parts[0] != '' && $parts[1] != '' ) {
+                               if ( count( $parts ) == 3 ) {
+                                       return $parts; // e.g. "backend/container/path"
+                               } else {
+                                       return array( $parts[0], $parts[1], '' ); // e.g. "backend/container" 
+                               }
+                       }
+               }
+               return array( null, null, null );
+       }
+
+       /**
+        * Normalize a storage path by cleaning up directory separators.
+        * Returns null if the path is not of the format of a valid storage path.
+        * 
+        * @param $storagePath string
+        * @return string|null 
+        */
+       final public static function normalizeStoragePath( $storagePath ) {
+               list( $backend, $container, $relPath ) = self::splitStoragePath( $storagePath );
+               if ( $relPath !== null ) { // must be for this backend
+                       $relPath = self::normalizeContainerPath( $relPath );
+                       if ( $relPath !== null ) {
+                               return ( $relPath != '' )
+                                       ? "mwstore://{$backend}/{$container}/{$relPath}"
+                                       : "mwstore://{$backend}/{$container}";
+                       }
+               }
+               return null;
+       }
+
+       /**
+        * Validate and normalize a relative storage path.
+        * Null is returned if the path involves directory traversal.
+        * Traversal is insecure for FS backends and broken for others.
+        *
+        * @param $path string Storage path relative to a container
+        * @return string|null
+        */
+       final protected static function normalizeContainerPath( $path ) {
+               // Normalize directory separators
+               $path = strtr( $path, '\\', '/' );
+               // Collapse any consecutive directory separators
+               $path = preg_replace( '![/]{2,}!', '/', $path );
+               // Remove any leading directory separator
+               $path = ltrim( $path, '/' );
+               // Use the same traversal protection as Title::secureAndSplit()
+               if ( strpos( $path, '.' ) !== false ) {
+                       if (
+                               $path === '.' ||
+                               $path === '..' ||
+                               strpos( $path, './' ) === 0 ||
+                               strpos( $path, '../' ) === 0 ||
+                               strpos( $path, '/./' ) !== false ||
+                               strpos( $path, '/../' ) !== false
+                       ) { 
+                               return null;
+                       }
+               }
+               return $path;
+       }
+
+       /**
+        * Get the parent storage directory of a storage path.
+        * This returns a path like "mwstore://backend/container",
+        * "mwstore://backend/container/...", or null if there is no parent.
+        * 
+        * @param $storagePath string
+        * @return string|null
+        */
+       final public static function parentStoragePath( $storagePath ) {
+               $storagePath = dirname( $storagePath );
+               list( $b, $cont, $rel ) = self::splitStoragePath( $storagePath );
+               return ( $rel === null ) ? null : $storagePath;
+       }
+
+       /**
+        * Get the final extension from a storage or FS path
+        * 
+        * @param $path string
+        * @return string
+        */
+       final public static function extensionFromPath( $path ) {
+               $i = strrpos( $path, '.' );
+               return strtolower( $i ? substr( $path, $i + 1 ) : '' );
+       }
 }
 
 /**
  * Base class for all single-write backends.
  * This class defines the methods as abstract that subclasses must implement.
- * Callers outside of FileBackend and its helper classes, such as FileOp,
- * should only call functions that are present in FileBackendBase.
+ * Outside callers should *not* use functions with "Internal" in the name.
  * 
- * The FileBackendBase operations are implemented using primitive functions
+ * The FileBackend operations are implemented using basic functions
  * such as storeInternal(), copyInternal(), deleteInternal() and the like.
  * This class is also responsible for path resolution and sanitization.
  * 
  * @ingroup FileBackend
  * @since 1.19
  */
-abstract class FileBackend extends FileBackendBase {
-       /** @var Array */
+abstract class FileBackendStore extends FileBackend {
+       /** @var Array Map of paths to small (RAM/disk) cache items */
        protected $cache = array(); // (storage path => key => value)
-       protected $maxCacheSize = 75; // integer; max paths with entries
-       /** @var Array */
-       protected $shardViaHashLevels = array(); // (container name => integer)
+       protected $maxCacheSize = 100; // integer; max paths with entries
+       /** @var Array Map of paths to large (RAM/disk) cache items */
+       protected $expCache = array(); // (storage path => key => value)
+       protected $maxExpCacheSize = 10; // integer; max paths with entries
+
+       /** @var Array Map of container names to sharding settings */
+       protected $shardViaHashLevels = array(); // (container name => config array)
 
        protected $maxFileSize = 1000000000; // integer bytes (1GB)
 
@@ -641,7 +747,7 @@ abstract class FileBackend extends FileBackendBase {
        }
 
        /**
-        * @see FileBackend::createInternal()
+        * @see FileBackendStore::createInternal()
         */
        abstract protected function doCreateInternal( array $params );
 
@@ -670,7 +776,7 @@ abstract class FileBackend extends FileBackendBase {
        }
 
        /**
-        * @see FileBackend::storeInternal()
+        * @see FileBackendStore::storeInternal()
         */
        abstract protected function doStoreInternal( array $params );
 
@@ -695,7 +801,7 @@ abstract class FileBackend extends FileBackendBase {
        }
 
        /**
-        * @see FileBackend::copyInternal()
+        * @see FileBackendStore::copyInternal()
         */
        abstract protected function doCopyInternal( array $params );
 
@@ -719,7 +825,7 @@ abstract class FileBackend extends FileBackendBase {
        }
 
        /**
-        * @see FileBackend::deleteInternal()
+        * @see FileBackendStore::deleteInternal()
         */
        abstract protected function doDeleteInternal( array $params );
 
@@ -744,7 +850,7 @@ abstract class FileBackend extends FileBackendBase {
        }
 
        /**
-        * @see FileBackend::moveInternal()
+        * @see FileBackendStore::moveInternal()
         */
        protected function doMoveInternal( array $params ) {
                // Copy source to dest
@@ -758,7 +864,7 @@ abstract class FileBackend extends FileBackendBase {
        }
 
        /**
-        * @see FileBackendBase::concatenate()
+        * @see FileBackend::concatenate()
         */
        final public function concatenate( array $params ) {
                wfProfileIn( __METHOD__ );
@@ -776,7 +882,7 @@ abstract class FileBackend extends FileBackendBase {
        }
 
        /**
-        * @see FileBackend::concatenate()
+        * @see FileBackendStore::concatenate()
         */
        protected function doConcatenate( array $params ) {
                $status = Status::newGood();
@@ -831,7 +937,7 @@ abstract class FileBackend extends FileBackendBase {
        }
 
        /**
-        * @see FileBackendBase::doPrepare()
+        * @see FileBackend::doPrepare()
         */
        final protected function doPrepare( array $params ) {
                wfProfileIn( __METHOD__ );
@@ -859,14 +965,14 @@ abstract class FileBackend extends FileBackendBase {
        }
 
        /**
-        * @see FileBackend::doPrepare()
+        * @see FileBackendStore::doPrepare()
         */
        protected function doPrepareInternal( $container, $dir, array $params ) {
                return Status::newGood();
        }
 
        /**
-        * @see FileBackendBase::doSecure()
+        * @see FileBackend::doSecure()
         */
        final protected function doSecure( array $params ) {
                wfProfileIn( __METHOD__ );
@@ -894,14 +1000,14 @@ abstract class FileBackend extends FileBackendBase {
        }
 
        /**
-        * @see FileBackend::doSecure()
+        * @see FileBackendStore::doSecure()
         */
        protected function doSecureInternal( $container, $dir, array $params ) {
                return Status::newGood();
        }
 
        /**
-        * @see FileBackendBase::doClean()
+        * @see FileBackend::doClean()
         */
        final protected function doClean( array $params ) {
                wfProfileIn( __METHOD__ );
@@ -937,14 +1043,14 @@ abstract class FileBackend extends FileBackendBase {
        }
 
        /**
-        * @see FileBackend::doClean()
+        * @see FileBackendStore::doClean()
         */
        protected function doCleanInternal( $container, $dir, array $params ) {
                return Status::newGood();
        }
 
        /**
-        * @see FileBackendBase::fileExists()
+        * @see FileBackend::fileExists()
         */
        final public function fileExists( array $params ) {
                wfProfileIn( __METHOD__ );
@@ -954,7 +1060,7 @@ abstract class FileBackend extends FileBackendBase {
        }
 
        /**
-        * @see FileBackendBase::getFileTimestamp()
+        * @see FileBackend::getFileTimestamp()
         */
        final public function getFileTimestamp( array $params ) {
                wfProfileIn( __METHOD__ );
@@ -964,7 +1070,7 @@ abstract class FileBackend extends FileBackendBase {
        }
 
        /**
-        * @see FileBackendBase::getFileSize()
+        * @see FileBackend::getFileSize()
         */
        final public function getFileSize( array $params ) {
                wfProfileIn( __METHOD__ );
@@ -974,11 +1080,14 @@ abstract class FileBackend extends FileBackendBase {
        }
 
        /**
-        * @see FileBackendBase::getFileStat()
+        * @see FileBackend::getFileStat()
         */
        final public function getFileStat( array $params ) {
                wfProfileIn( __METHOD__ );
-               $path = $params['src'];
+               $path = self::normalizeStoragePath( $params['src'] );
+               if ( $path === null ) {
+                       return false; // invalid storage path
+               }
                $latest = !empty( $params['latest'] );
                if ( isset( $this->cache[$path]['stat'] ) ) {
                        // If we want the latest data, check that this cached
@@ -999,12 +1108,12 @@ abstract class FileBackend extends FileBackendBase {
        }
 
        /**
-        * @see FileBackend::getFileStat()
+        * @see FileBackendStore::getFileStat()
         */
        abstract protected function doGetFileStat( array $params );
 
        /**
-        * @see FileBackendBase::getFileContents()
+        * @see FileBackend::getFileContents()
         */
        public function getFileContents( array $params ) {
                wfProfileIn( __METHOD__ );
@@ -1021,7 +1130,7 @@ abstract class FileBackend extends FileBackendBase {
        }
 
        /**
-        * @see FileBackendBase::getFileSha1Base36()
+        * @see FileBackend::getFileSha1Base36()
         */
        final public function getFileSha1Base36( array $params ) {
                wfProfileIn( __METHOD__ );
@@ -1040,7 +1149,7 @@ abstract class FileBackend extends FileBackendBase {
        }
 
        /**
-        * @see FileBackend::getFileSha1Base36()
+        * @see FileBackendStore::getFileSha1Base36()
         */
        protected function doGetFileSha1Base36( array $params ) {
                $fsFile = $this->getLocalReference( $params );
@@ -1052,7 +1161,7 @@ abstract class FileBackend extends FileBackendBase {
        }
 
        /**
-        * @see FileBackendBase::getFileProps()
+        * @see FileBackend::getFileProps()
         */
        final public function getFileProps( array $params ) {
                wfProfileIn( __METHOD__ );
@@ -1063,26 +1172,26 @@ abstract class FileBackend extends FileBackendBase {
        }
 
        /**
-        * @see FileBackendBase::getLocalReference()
+        * @see FileBackend::getLocalReference()
         */
        public function getLocalReference( array $params ) {
                wfProfileIn( __METHOD__ );
                $path = $params['src'];
-               if ( isset( $this->cache[$path]['localRef'] ) ) {
+               if ( isset( $this->expCache[$path]['localRef'] ) ) {
                        wfProfileOut( __METHOD__ );
-                       return $this->cache[$path]['localRef'];
+                       return $this->expCache[$path]['localRef'];
                }
                $tmpFile = $this->getLocalCopy( $params );
                if ( $tmpFile ) { // don't cache negatives
-                       $this->trimCache(); // limit memory
-                       $this->cache[$path]['localRef'] = $tmpFile;
+                       $this->trimExpCache(); // limit memory
+                       $this->expCache[$path]['localRef'] = $tmpFile;
                }
                wfProfileOut( __METHOD__ );
                return $tmpFile;
        }
 
        /**
-        * @see FileBackendBase::streamFile()
+        * @see FileBackend::streamFile()
         */
        final public function streamFile( array $params ) {
                wfProfileIn( __METHOD__ );
@@ -1109,7 +1218,7 @@ abstract class FileBackend extends FileBackendBase {
        }
 
        /**
-        * @see FileBackend::streamFile()
+        * @see FileBackendStore::streamFile()
         */
        protected function doStreamFile( array $params ) {
                $status = Status::newGood();
@@ -1125,7 +1234,7 @@ abstract class FileBackend extends FileBackendBase {
        }
 
        /**
-        * @see FileBackendBase::getFileList()
+        * @see FileBackend::getFileList()
         */
        final public function getFileList( array $params ) {
                list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
@@ -1139,18 +1248,20 @@ abstract class FileBackend extends FileBackendBase {
                        wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
                        // File listing spans multiple containers/shards
                        list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
-                       return new FileBackendShardListIterator( $this,
-                               $fullCont, $this->getContainerSuffixes( $shortCont ), $params );
+                       return new FileBackendStoreShardListIterator( $this,
+                               $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
                }
        }
 
        /**
-        * Do not call this function from places outside FileBackend and ContainerFileListIterator
+        * Do not call this function from places outside FileBackend
         *
+        * @see FileBackendStore::getFileList()
+        * 
         * @param $container string Resolved container name
         * @param $dir string Resolved path relative to container
         * @param $params Array
-        * @see FileBackend::getFileList()
+        * @return Traversable|Array|null
         */
        abstract public function getFileListInternal( $container, $dir, array $params );
 
@@ -1203,7 +1314,7 @@ abstract class FileBackend extends FileBackendBase {
        }
 
        /**
-        * @see FileBackendBase::doOperationsInternal()
+        * @see FileBackend::doOperationsInternal()
         */
        protected function doOperationsInternal( array $ops, array $opts ) {
                wfProfileIn( __METHOD__ );
@@ -1248,14 +1359,20 @@ abstract class FileBackend extends FileBackendBase {
        }
 
        /**
-        * @see FileBackendBase::clearCache()
+        * @see FileBackend::clearCache()
         */
        final public function clearCache( array $paths = null ) {
+               if ( is_array( $paths ) ) {
+                       $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
+                       $paths = array_filter( $paths, 'strlen' ); // remove nulls
+               }
                if ( $paths === null ) {
                        $this->cache = array();
+                       $this->expCache = array();
                } else {
                        foreach ( $paths as $path ) {
                                unset( $this->cache[$path] );
+                               unset( $this->expCache[$path] );
                        }
                }
                $this->doClearCache( $paths );
@@ -1264,7 +1381,7 @@ abstract class FileBackend extends FileBackendBase {
        /**
         * Clears any additional stat caches for storage paths
         * 
-        * @see FileBackendBase::clearCache()
+        * @see FileBackend::clearCache()
         * 
         * @param $paths Array Storage paths (optional)
         * @return void
@@ -1272,61 +1389,27 @@ abstract class FileBackend extends FileBackendBase {
        protected function doClearCache( array $paths = null ) {}
 
        /**
-        * Prune the cache if it is too big to add an item
+        * Prune the inexpensive cache if it is too big to add an item
         * 
         * @return void
         */
        protected function trimCache() {
                if ( count( $this->cache ) >= $this->maxCacheSize ) {
                        reset( $this->cache );
-                       $key = key( $this->cache );
-                       unset( $this->cache[$key] );
+                       unset( $this->cache[key( $this->cache )] );
                }
        }
 
        /**
-        * Get the parent storage directory of a storage path.
-        * This returns a path like "mwstore://backend/container",
-        * "mwstore://backend/container/...", or null if there is no parent.
-        * 
-        * @param $storagePath string
-        * @return string|null
-        */
-       final public static function parentStoragePath( $storagePath ) {
-               $storagePath = dirname( $storagePath );
-               list( $b, $cont, $rel ) = self::splitStoragePath( $storagePath );
-               return ( $rel === null ) ? null : $storagePath;
-       }
-
-       /**
-        * Check if a given path is a mwstore:// path.
-        * This does not do any actual validation or existence checks.
+        * Prune the expensive cache if it is too big to add an item
         * 
-        * @param $path string
-        * @return bool
-        */
-       final public static function isStoragePath( $path ) {
-               return ( strpos( $path, 'mwstore://' ) === 0 );
-       }
-
-       /**
-        * Split a storage path (e.g. "mwstore://backend/container/path/to/object")
-        * into a backend name, a container name, and a relative object path.
-        *
-        * @param $storagePath string
-        * @return Array (backend, container, rel object) or (null, null, null)
+        * @return void
         */
-       final public static function splitStoragePath( $storagePath ) {
-               if ( self::isStoragePath( $storagePath ) ) {
-                       // Note: strlen( 'mwstore://' ) = 10
-                       $parts = explode( '/', substr( $storagePath, 10 ), 3 );
-                       if ( count( $parts ) == 3 ) {
-                               return $parts; // e.g. "backend/container/path"
-                       } elseif ( count( $parts ) == 2 ) {
-                               return array( $parts[0], $parts[1], '' ); // e.g. "backend/container" 
-                       }
+       protected function trimExpCache() {
+               if ( count( $this->expCache ) >= $this->maxExpCacheSize ) {
+                       reset( $this->expCache );
+                       unset( $this->expCache[key( $this->expCache )] );
                }
-               return array( null, null, null );
        }
 
        /**
@@ -1339,40 +1422,12 @@ abstract class FileBackend extends FileBackendBase {
        final protected static function isValidContainerName( $container ) {
                // This accounts for Swift and S3 restrictions while leaving room 
                // for things like '.xxx' (hex shard chars) or '.seg' (segments).
+               // This disallows directory separators or traversal characters.
                // Note that matching strings URL encode to the same string;
                // in Swift, the length restriction is *after* URL encoding.
                return preg_match( '/^[a-z0-9][a-z0-9-_]{0,199}$/i', $container );
        }
 
-       /**
-        * Validate and normalize a relative storage path.
-        * Null is returned if the path involves directory traversal.
-        * Traversal is insecure for FS backends and broken for others.
-        *
-        * @param $path string Storage path relative to a container
-        * @return string|null
-        */
-       final protected static function normalizeContainerPath( $path ) {
-               // Normalize directory separators
-               $path = strtr( $path, '\\', '/' );
-               // Collapse consecutive directory separators
-               $path = preg_replace( '![/]{2,}!', '/', $path );
-               // Use the same traversal protection as Title::secureAndSplit()
-               if ( strpos( $path, '.' ) !== false ) {
-                       if (
-                               $path === '.' ||
-                               $path === '..' ||
-                               strpos( $path, './' ) === 0 ||
-                               strpos( $path, '../' ) === 0 ||
-                               strpos( $path, '/./' ) !== false ||
-                               strpos( $path, '/../' ) !== false
-                       ) { 
-                               return null;
-                       }
-               }
-               return $path;
-       }
-
        /**
         * Splits a storage path into an internal container name,
         * an internal relative file name, and a container shard suffix.
@@ -1415,7 +1470,7 @@ abstract class FileBackend extends FileBackendBase {
         * Like resolveStoragePath() except null values are returned if
         * the container is sharded and the shard could not be determined.
         *
-        * @see FileBackend::resolveStoragePath()
+        * @see FileBackendStore::resolveStoragePath()
         *
         * @param $storagePath string
         * @return Array (container, path) or (null, null) if invalid
@@ -1437,40 +1492,53 @@ abstract class FileBackend extends FileBackendBase {
         * @return string|null Returns null if shard could not be determined
         */
        final protected function getContainerShard( $container, $relPath ) {
-               $hashLevels = $this->getContainerHashLevels( $container );
-               if ( $hashLevels === 1 ) { // 16 shards per container
-                       $hashDirRegex = '(?P<shard>[0-9a-f])';
-               } elseif ( $hashLevels === 2 ) { // 256 shards per container
-                       $hashDirRegex = '[0-9a-f]/(?P<shard>[0-9a-f]{2})';
-               } else {
-                       return ''; // no sharding
-               }
-               // Allow certain directories to be above the hash dirs so as
-               // to work with FileRepo (e.g. "archive/a/ab" or "temp/a/ab").
-               // They must be 2+ chars to avoid any hash directory ambiguity.
-               $m = array();
-               if ( preg_match( "!^(?:[^/]{2,}/)*$hashDirRegex(?:/|$)!", $relPath, $m ) ) {
-                       return '.' . $m['shard'];
+               list( $levels, $base, $repeat ) = $this->getContainerHashLevels( $container );
+               if ( $levels == 1 || $levels == 2 ) {
+                       // Hash characters are either base 16 or 36
+                       $char = ( $base == 36 ) ? '[0-9a-z]' : '[0-9a-f]';
+                       // Get a regex that represents the shard portion of paths.
+                       // The concatenation of the captures gives us the shard.
+                       if ( $levels === 1 ) { // 16 or 36 shards per container
+                               $hashDirRegex = '(' . $char . ')';
+                       } else { // 256 or 1296 shards per container
+                               if ( $repeat ) { // verbose hash dir format (e.g. "a/ab/abc")
+                                       $hashDirRegex = $char . '/(' . $char . '{2})';
+                               } else { // short hash dir format (e.g. "a/b/c")
+                                       $hashDirRegex = '(' . $char . ')/(' . $char . ')';
+                               }
+                       }
+                       // Allow certain directories to be above the hash dirs so as
+                       // to work with FileRepo (e.g. "archive/a/ab" or "temp/a/ab").
+                       // They must be 2+ chars to avoid any hash directory ambiguity.
+                       $m = array();
+                       if ( preg_match( "!^(?:[^/]{2,}/)*$hashDirRegex(?:/|$)!", $relPath, $m ) ) {
+                               return '.' . implode( '', array_slice( $m, 1 ) );
+                       }
+                       return null; // failed to match
                }
-               return null; // failed to match
+               return ''; // no sharding
        }
 
        /**
-        * Get the number of hash levels for a container.
+        * Get the sharding config for a container.
         * If greater than 0, then all file storage paths within
         * the container are required to be hashed accordingly.
         *
         * @param $container string
-        * @return integer
+        * @return Array (integer levels, integer base, repeat flag) or (0, 0, false)
         */
        final protected function getContainerHashLevels( $container ) {
                if ( isset( $this->shardViaHashLevels[$container] ) ) {
-                       $hashLevels = (int)$this->shardViaHashLevels[$container];
-                       if ( $hashLevels >= 0 && $hashLevels <= 2 ) {
-                               return $hashLevels;
+                       $config = $this->shardViaHashLevels[$container];
+                       $hashLevels = (int)$config['levels'];
+                       if ( $hashLevels == 0 || $hashLevels == 2 ) {
+                               $hashBase = (int)$config['base'];
+                               if ( $hashBase == 16 || $hashBase == 36 ) {
+                                       return array( $hashLevels, $hashBase, $config['repeat'] );
+                               }
                        }
                }
-               return 0; // no sharding
+               return array( 0, 0, false ); // no sharding
        }
 
        /**
@@ -1481,11 +1549,11 @@ abstract class FileBackend extends FileBackendBase {
         */
        final protected function getContainerSuffixes( $container ) {
                $shards = array();
-               $digits = $this->getContainerHashLevels( $container );
+               list( $digits, $base ) = $this->getContainerHashLevels( $container );
                if ( $digits > 0 ) {
-                       $numShards = 1 << ( $digits * 4 );
+                       $numShards = pow( $base, $digits );
                        for ( $index = 0; $index < $numShards; $index++ ) {
-                               $shards[] = '.' . str_pad( dechex( $index ), $digits, '0', STR_PAD_LEFT );
+                               $shards[] = '.' . wfBaseConvert( $index, 10, $base, $digits );
                        }
                }
                return $shards;
@@ -1530,27 +1598,16 @@ abstract class FileBackend extends FileBackendBase {
        protected function resolveContainerPath( $container, $relStoragePath ) {
                return $relStoragePath;
        }
-
-       /**
-        * Get the final extension from a storage or FS path
-        * 
-        * @param $path string
-        * @return string
-        */
-       final public static function extensionFromPath( $path ) {
-               $i = strrpos( $path, '.' );
-               return strtolower( $i ? substr( $path, $i + 1 ) : '' );
-       }
 }
 
 /**
- * FileBackend helper function to handle file listings that span container shards.
- * Do not use this class from places outside of FileBackend.
+ * FileBackendStore helper function to handle file listings that span container shards.
+ * Do not use this class from places outside of FileBackendStore.
  *
- * @ingroup FileBackend
+ * @ingroup FileBackendStore
  */
-class FileBackendShardListIterator implements Iterator {
-       /* @var FileBackend */
+class FileBackendStoreShardListIterator implements Iterator {
+       /* @var FileBackendStore */
        protected $backend;
        /* @var Array */
        protected $params;
@@ -1565,14 +1622,14 @@ class FileBackendShardListIterator implements Iterator {
        protected $pos = 0; // integer
 
        /**
-        * @param $backend FileBackend
+        * @param $backend FileBackendStore
         * @param $container string Full storage container name
         * @param $dir string Storage directory relative to container
         * @param $suffixes Array List of container shard suffixes
         * @param $params Array
         */
        public function __construct(
-               FileBackend $backend, $container, $dir, array $suffixes, array $params
+               FileBackendStore $backend, $container, $dir, array $suffixes, array $params
        ) {
                $this->backend = $backend;
                $this->container = $container;