MediaWiki  REL1_22
FileRepo.php
Go to the documentation of this file.
00001 <?php
00037 class FileRepo {
00038     const DELETE_SOURCE = 1;
00039     const OVERWRITE = 2;
00040     const OVERWRITE_SAME = 4;
00041     const SKIP_LOCKING = 8;
00042 
00044     protected $backend;
00046     protected $zones = array();
00047 
00048     var $thumbScriptUrl, $transformVia404;
00049     var $descBaseUrl, $scriptDirUrl, $scriptExtension, $articleUrl;
00050     var $fetchDescription, $initialCapital;
00051     var $pathDisclosureProtection = 'simple'; // 'paranoid'
00052     var $descriptionCacheExpiry, $url, $thumbUrl;
00053     var $hashLevels, $deletedHashLevels;
00054     protected $abbrvThreshold;
00055 
00060     var $fileFactory = array( 'UnregisteredLocalFile', 'newFromTitle' );
00061     var $oldFileFactory = false;
00062     var $fileFactoryKey = false, $oldFileFactoryKey = false;
00063 
00068     public function __construct( array $info = null ) {
00069         // Verify required settings presence
00070         if (
00071             $info === null
00072             || !array_key_exists( 'name', $info )
00073             || !array_key_exists( 'backend', $info )
00074         ) {
00075             throw new MWException( __CLASS__ . " requires an array of options having both 'name' and 'backend' keys.\n" );
00076         }
00077 
00078         // Required settings
00079         $this->name = $info['name'];
00080         if ( $info['backend'] instanceof FileBackend ) {
00081             $this->backend = $info['backend']; // useful for testing
00082         } else {
00083             $this->backend = FileBackendGroup::singleton()->get( $info['backend'] );
00084         }
00085 
00086         // Optional settings that can have no value
00087         $optionalSettings = array(
00088             'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription',
00089             'thumbScriptUrl', 'pathDisclosureProtection', 'descriptionCacheExpiry',
00090             'scriptExtension'
00091         );
00092         foreach ( $optionalSettings as $var ) {
00093             if ( isset( $info[$var] ) ) {
00094                 $this->$var = $info[$var];
00095             }
00096         }
00097 
00098         // Optional settings that have a default
00099         $this->initialCapital = isset( $info['initialCapital'] )
00100             ? $info['initialCapital']
00101             : MWNamespace::isCapitalized( NS_FILE );
00102         $this->url = isset( $info['url'] )
00103             ? $info['url']
00104             : false; // a subclass may set the URL (e.g. ForeignAPIRepo)
00105         if ( isset( $info['thumbUrl'] ) ) {
00106             $this->thumbUrl = $info['thumbUrl'];
00107         } else {
00108             $this->thumbUrl = $this->url ? "{$this->url}/thumb" : false;
00109         }
00110         $this->hashLevels = isset( $info['hashLevels'] )
00111             ? $info['hashLevels']
00112             : 2;
00113         $this->deletedHashLevels = isset( $info['deletedHashLevels'] )
00114             ? $info['deletedHashLevels']
00115             : $this->hashLevels;
00116         $this->transformVia404 = !empty( $info['transformVia404'] );
00117         $this->abbrvThreshold = isset( $info['abbrvThreshold'] )
00118             ? $info['abbrvThreshold']
00119             : 255;
00120         $this->isPrivate = !empty( $info['isPrivate'] );
00121         // Give defaults for the basic zones...
00122         $this->zones = isset( $info['zones'] ) ? $info['zones'] : array();
00123         foreach ( array( 'public', 'thumb', 'transcoded', 'temp', 'deleted' ) as $zone ) {
00124             if ( !isset( $this->zones[$zone]['container'] ) ) {
00125                 $this->zones[$zone]['container'] = "{$this->name}-{$zone}";
00126             }
00127             if ( !isset( $this->zones[$zone]['directory'] ) ) {
00128                 $this->zones[$zone]['directory'] = '';
00129             }
00130             if ( !isset( $this->zones[$zone]['urlsByExt'] ) ) {
00131                 $this->zones[$zone]['urlsByExt'] = array();
00132             }
00133         }
00134     }
00135 
00141     public function getBackend() {
00142         return $this->backend;
00143     }
00144 
00151     public function getReadOnlyReason() {
00152         return $this->backend->getReadOnlyReason();
00153     }
00154 
00162     protected function initZones( $doZones = array() ) {
00163         $status = $this->newGood();
00164         foreach ( (array)$doZones as $zone ) {
00165             $root = $this->getZonePath( $zone );
00166             if ( $root === null ) {
00167                 throw new MWException( "No '$zone' zone defined in the {$this->name} repo." );
00168             }
00169         }
00170         return $status;
00171     }
00172 
00179     public static function isVirtualUrl( $url ) {
00180         return substr( $url, 0, 9 ) == 'mwrepo://';
00181     }
00182 
00191     public function getVirtualUrl( $suffix = false ) {
00192         $path = 'mwrepo://' . $this->name;
00193         if ( $suffix !== false ) {
00194             $path .= '/' . rawurlencode( $suffix );
00195         }
00196         return $path;
00197     }
00198 
00206     public function getZoneUrl( $zone, $ext = null ) {
00207         if ( in_array( $zone, array( 'public', 'temp', 'thumb', 'transcoded' ) ) ) { // standard public zones
00208             if ( $ext !== null && isset( $this->zones[$zone]['urlsByExt'][$ext] ) ) {
00209                 return $this->zones[$zone]['urlsByExt'][$ext]; // custom URL for extension/zone
00210             } elseif ( isset( $this->zones[$zone]['url'] ) ) {
00211                 return $this->zones[$zone]['url']; // custom URL for zone
00212             }
00213         }
00214         switch ( $zone ) {
00215             case 'public':
00216                 return $this->url;
00217             case 'temp':
00218                 return "{$this->url}/temp";
00219             case 'deleted':
00220                 return false; // no public URL
00221             case 'thumb':
00222                 return $this->thumbUrl;
00223             case 'transcoded':
00224                 return "{$this->url}/transcoded";
00225             default:
00226                 return false;
00227         }
00228     }
00229 
00243     public function getZoneHandlerUrl( $zone ) {
00244         if ( isset( $this->zones[$zone]['handlerUrl'] )
00245             && in_array( $zone, array( 'public', 'temp', 'thumb', 'transcoded' ) ) )
00246         {
00247             return $this->zones[$zone]['handlerUrl'];
00248         }
00249         return false;
00250     }
00251 
00260     public function resolveVirtualUrl( $url ) {
00261         if ( substr( $url, 0, 9 ) != 'mwrepo://' ) {
00262             throw new MWException( __METHOD__ . ': unknown protocol' );
00263         }
00264         $bits = explode( '/', substr( $url, 9 ), 3 );
00265         if ( count( $bits ) != 3 ) {
00266             throw new MWException( __METHOD__ . ": invalid mwrepo URL: $url" );
00267         }
00268         list( $repo, $zone, $rel ) = $bits;
00269         if ( $repo !== $this->name ) {
00270             throw new MWException( __METHOD__ . ": fetching from a foreign repo is not supported" );
00271         }
00272         $base = $this->getZonePath( $zone );
00273         if ( !$base ) {
00274             throw new MWException( __METHOD__ . ": invalid zone: $zone" );
00275         }
00276         return $base . '/' . rawurldecode( $rel );
00277     }
00278 
00285     protected function getZoneLocation( $zone ) {
00286         if ( !isset( $this->zones[$zone] ) ) {
00287             return array( null, null ); // bogus
00288         }
00289         return array( $this->zones[$zone]['container'], $this->zones[$zone]['directory'] );
00290     }
00291 
00298     public function getZonePath( $zone ) {
00299         list( $container, $base ) = $this->getZoneLocation( $zone );
00300         if ( $container === null || $base === null ) {
00301             return null;
00302         }
00303         $backendName = $this->backend->getName();
00304         if ( $base != '' ) { // may not be set
00305             $base = "/{$base}";
00306         }
00307         return "mwstore://$backendName/{$container}{$base}";
00308     }
00309 
00321     public function newFile( $title, $time = false ) {
00322         $title = File::normalizeTitle( $title );
00323         if ( !$title ) {
00324             return null;
00325         }
00326         if ( $time ) {
00327             if ( $this->oldFileFactory ) {
00328                 return call_user_func( $this->oldFileFactory, $title, $this, $time );
00329             } else {
00330                 return false;
00331             }
00332         } else {
00333             return call_user_func( $this->fileFactory, $title, $this );
00334         }
00335     }
00336 
00355     public function findFile( $title, $options = array() ) {
00356         $title = File::normalizeTitle( $title );
00357         if ( !$title ) {
00358             return false;
00359         }
00360         $time = isset( $options['time'] ) ? $options['time'] : false;
00361         # First try the current version of the file to see if it precedes the timestamp
00362         $img = $this->newFile( $title );
00363         if ( !$img ) {
00364             return false;
00365         }
00366         if ( $img->exists() && ( !$time || $img->getTimestamp() == $time ) ) {
00367             return $img;
00368         }
00369         # Now try an old version of the file
00370         if ( $time !== false ) {
00371             $img = $this->newFile( $title, $time );
00372             if ( $img && $img->exists() ) {
00373                 if ( !$img->isDeleted( File::DELETED_FILE ) ) {
00374                     return $img; // always OK
00375                 } elseif ( !empty( $options['private'] ) && $img->userCan( File::DELETED_FILE ) ) {
00376                     return $img;
00377                 }
00378             }
00379         }
00380 
00381         # Now try redirects
00382         if ( !empty( $options['ignoreRedirect'] ) ) {
00383             return false;
00384         }
00385         $redir = $this->checkRedirect( $title );
00386         if ( $redir && $title->getNamespace() == NS_FILE ) {
00387             $img = $this->newFile( $redir );
00388             if ( !$img ) {
00389                 return false;
00390             }
00391             if ( $img->exists() ) {
00392                 $img->redirectedFrom( $title->getDBkey() );
00393                 return $img;
00394             }
00395         }
00396         return false;
00397     }
00398 
00410     public function findFiles( array $items ) {
00411         $result = array();
00412         foreach ( $items as $item ) {
00413             if ( is_array( $item ) ) {
00414                 $title = $item['title'];
00415                 $options = $item;
00416                 unset( $options['title'] );
00417             } else {
00418                 $title = $item;
00419                 $options = array();
00420             }
00421             $file = $this->findFile( $title, $options );
00422             if ( $file ) {
00423                 $result[$file->getTitle()->getDBkey()] = $file;
00424             }
00425         }
00426         return $result;
00427     }
00428 
00438     public function findFileFromKey( $sha1, $options = array() ) {
00439         $time = isset( $options['time'] ) ? $options['time'] : false;
00440         # First try to find a matching current version of a file...
00441         if ( $this->fileFactoryKey ) {
00442             $img = call_user_func( $this->fileFactoryKey, $sha1, $this, $time );
00443         } else {
00444             return false; // find-by-sha1 not supported
00445         }
00446         if ( $img && $img->exists() ) {
00447             return $img;
00448         }
00449         # Now try to find a matching old version of a file...
00450         if ( $time !== false && $this->oldFileFactoryKey ) { // find-by-sha1 supported?
00451             $img = call_user_func( $this->oldFileFactoryKey, $sha1, $this, $time );
00452             if ( $img && $img->exists() ) {
00453                 if ( !$img->isDeleted( File::DELETED_FILE ) ) {
00454                     return $img; // always OK
00455                 } elseif ( !empty( $options['private'] ) && $img->userCan( File::DELETED_FILE ) ) {
00456                     return $img;
00457                 }
00458             }
00459         }
00460         return false;
00461     }
00462 
00471     public function findBySha1( $hash ) {
00472         return array();
00473     }
00474 
00482     public function findBySha1s( array $hashes ) {
00483         $result = array();
00484         foreach ( $hashes as $hash ) {
00485             $files = $this->findBySha1( $hash );
00486             if ( count( $files ) ) {
00487                 $result[$hash] = $files;
00488             }
00489         }
00490         return $result;
00491     }
00492 
00501     public function findFilesByPrefix( $prefix, $limit ) {
00502         return array();
00503     }
00504 
00511     public function getRootUrl() {
00512         return $this->getZoneUrl( 'public' );
00513     }
00514 
00520     public function getThumbScriptUrl() {
00521         return $this->thumbScriptUrl;
00522     }
00523 
00529     public function canTransformVia404() {
00530         return $this->transformVia404;
00531     }
00532 
00539     public function getNameFromTitle( Title $title ) {
00540         global $wgContLang;
00541         if ( $this->initialCapital != MWNamespace::isCapitalized( NS_FILE ) ) {
00542             $name = $title->getUserCaseDBKey();
00543             if ( $this->initialCapital ) {
00544                 $name = $wgContLang->ucfirst( $name );
00545             }
00546         } else {
00547             $name = $title->getDBkey();
00548         }
00549         return $name;
00550     }
00551 
00557     public function getRootDirectory() {
00558         return $this->getZonePath( 'public' );
00559     }
00560 
00568     public function getHashPath( $name ) {
00569         return self::getHashPathForLevel( $name, $this->hashLevels );
00570     }
00571 
00579     public function getTempHashPath( $suffix ) {
00580         $parts = explode( '!', $suffix, 2 ); // format is <timestamp>!<name> or just <name>
00581         $name = isset( $parts[1] ) ? $parts[1] : $suffix; // hash path is not based on timestamp
00582         return self::getHashPathForLevel( $name, $this->hashLevels );
00583     }
00584 
00590     protected static function getHashPathForLevel( $name, $levels ) {
00591         if ( $levels == 0 ) {
00592             return '';
00593         } else {
00594             $hash = md5( $name );
00595             $path = '';
00596             for ( $i = 1; $i <= $levels; $i++ ) {
00597                 $path .= substr( $hash, 0, $i ) . '/';
00598             }
00599             return $path;
00600         }
00601     }
00602 
00608     public function getHashLevels() {
00609         return $this->hashLevels;
00610     }
00611 
00617     public function getName() {
00618         return $this->name;
00619     }
00620 
00628     public function makeUrl( $query = '', $entry = 'index' ) {
00629         if ( isset( $this->scriptDirUrl ) ) {
00630             $ext = isset( $this->scriptExtension ) ? $this->scriptExtension : '.php';
00631             return wfAppendQuery( "{$this->scriptDirUrl}/{$entry}{$ext}", $query );
00632         }
00633         return false;
00634     }
00635 
00648     public function getDescriptionUrl( $name ) {
00649         $encName = wfUrlencode( $name );
00650         if ( !is_null( $this->descBaseUrl ) ) {
00651             # "http://example.com/wiki/Image:"
00652             return $this->descBaseUrl . $encName;
00653         }
00654         if ( !is_null( $this->articleUrl ) ) {
00655             # "http://example.com/wiki/$1"
00656             #
00657             # We use "Image:" as the canonical namespace for
00658             # compatibility across all MediaWiki versions.
00659             return str_replace( '$1',
00660                 "Image:$encName", $this->articleUrl );
00661         }
00662         if ( !is_null( $this->scriptDirUrl ) ) {
00663             # "http://example.com/w"
00664             #
00665             # We use "Image:" as the canonical namespace for
00666             # compatibility across all MediaWiki versions,
00667             # and just sort of hope index.php is right. ;)
00668             return $this->makeUrl( "title=Image:$encName" );
00669         }
00670         return false;
00671     }
00672 
00683     public function getDescriptionRenderUrl( $name, $lang = null ) {
00684         $query = 'action=render';
00685         if ( !is_null( $lang ) ) {
00686             $query .= '&uselang=' . $lang;
00687         }
00688         if ( isset( $this->scriptDirUrl ) ) {
00689             return $this->makeUrl(
00690                 'title=' .
00691                 wfUrlencode( 'Image:' . $name ) .
00692                 "&$query" );
00693         } else {
00694             $descUrl = $this->getDescriptionUrl( $name );
00695             if ( $descUrl ) {
00696                 return wfAppendQuery( $descUrl, $query );
00697             } else {
00698                 return false;
00699             }
00700         }
00701     }
00702 
00708     public function getDescriptionStylesheetUrl() {
00709         if ( isset( $this->scriptDirUrl ) ) {
00710             return $this->makeUrl( 'title=MediaWiki:Filepage.css&' .
00711                 wfArrayToCgi( Skin::getDynamicStylesheetQuery() ) );
00712         }
00713         return false;
00714     }
00715 
00730     public function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) {
00731         $this->assertWritableRepo(); // fail out if read-only
00732 
00733         $status = $this->storeBatch( array( array( $srcPath, $dstZone, $dstRel ) ), $flags );
00734         if ( $status->successCount == 0 ) {
00735             $status->ok = false;
00736         }
00737 
00738         return $status;
00739     }
00740 
00754     public function storeBatch( array $triplets, $flags = 0 ) {
00755         $this->assertWritableRepo(); // fail out if read-only
00756 
00757         $status = $this->newGood();
00758         $backend = $this->backend; // convenience
00759 
00760         $operations = array();
00761         $sourceFSFilesToDelete = array(); // cleanup for disk source files
00762         // Validate each triplet and get the store operation...
00763         foreach ( $triplets as $triplet ) {
00764             list( $srcPath, $dstZone, $dstRel ) = $triplet;
00765             wfDebug( __METHOD__
00766                 . "( \$src='$srcPath', \$dstZone='$dstZone', \$dstRel='$dstRel' )\n"
00767             );
00768 
00769             // Resolve destination path
00770             $root = $this->getZonePath( $dstZone );
00771             if ( !$root ) {
00772                 throw new MWException( "Invalid zone: $dstZone" );
00773             }
00774             if ( !$this->validateFilename( $dstRel ) ) {
00775                 throw new MWException( 'Validation error in $dstRel' );
00776             }
00777             $dstPath = "$root/$dstRel";
00778             $dstDir = dirname( $dstPath );
00779             // Create destination directories for this triplet
00780             if ( !$this->initDirectory( $dstDir )->isOK() ) {
00781                 return $this->newFatal( 'directorycreateerror', $dstDir );
00782             }
00783 
00784             // Resolve source to a storage path if virtual
00785             $srcPath = $this->resolveToStoragePath( $srcPath );
00786 
00787             // Get the appropriate file operation
00788             if ( FileBackend::isStoragePath( $srcPath ) ) {
00789                 $opName = ( $flags & self::DELETE_SOURCE ) ? 'move' : 'copy';
00790             } else {
00791                 $opName = 'store';
00792                 if ( $flags & self::DELETE_SOURCE ) {
00793                     $sourceFSFilesToDelete[] = $srcPath;
00794                 }
00795             }
00796             $operations[] = array(
00797                 'op' => $opName,
00798                 'src' => $srcPath,
00799                 'dst' => $dstPath,
00800                 'overwrite' => $flags & self::OVERWRITE,
00801                 'overwriteSame' => $flags & self::OVERWRITE_SAME,
00802             );
00803         }
00804 
00805         // Execute the store operation for each triplet
00806         $opts = array( 'force' => true );
00807         if ( $flags & self::SKIP_LOCKING ) {
00808             $opts['nonLocking'] = true;
00809         }
00810         $status->merge( $backend->doOperations( $operations, $opts ) );
00811         // Cleanup for disk source files...
00812         foreach ( $sourceFSFilesToDelete as $file ) {
00813             wfSuppressWarnings();
00814             unlink( $file ); // FS cleanup
00815             wfRestoreWarnings();
00816         }
00817 
00818         return $status;
00819     }
00820 
00831     public function cleanupBatch( array $files, $flags = 0 ) {
00832         $this->assertWritableRepo(); // fail out if read-only
00833 
00834         $status = $this->newGood();
00835 
00836         $operations = array();
00837         foreach ( $files as $path ) {
00838             if ( is_array( $path ) ) {
00839                 // This is a pair, extract it
00840                 list( $zone, $rel ) = $path;
00841                 $path = $this->getZonePath( $zone ) . "/$rel";
00842             } else {
00843                 // Resolve source to a storage path if virtual
00844                 $path = $this->resolveToStoragePath( $path );
00845             }
00846             $operations[] = array( 'op' => 'delete', 'src' => $path );
00847         }
00848         // Actually delete files from storage...
00849         $opts = array( 'force' => true );
00850         if ( $flags & self::SKIP_LOCKING ) {
00851             $opts['nonLocking'] = true;
00852         }
00853         $status->merge( $this->backend->doOperations( $operations, $opts ) );
00854 
00855         return $status;
00856     }
00857 
00869     final public function quickImport( $src, $dst, $disposition = null ) {
00870         return $this->quickImportBatch( array( array( $src, $dst, $disposition ) ) );
00871     }
00872 
00881     final public function quickPurge( $path ) {
00882         return $this->quickPurgeBatch( array( $path ) );
00883     }
00884 
00892     public function quickCleanDir( $dir ) {
00893         $status = $this->newGood();
00894         $status->merge( $this->backend->clean(
00895             array( 'dir' => $this->resolveToStoragePath( $dir ) ) ) );
00896 
00897         return $status;
00898     }
00899 
00912     public function quickImportBatch( array $triples ) {
00913         $status = $this->newGood();
00914         $operations = array();
00915         foreach ( $triples as $triple ) {
00916             list( $src, $dst ) = $triple;
00917             $src = $this->resolveToStoragePath( $src );
00918             $dst = $this->resolveToStoragePath( $dst );
00919             $operations[] = array(
00920                 'op' => FileBackend::isStoragePath( $src ) ? 'copy' : 'store',
00921                 'src' => $src,
00922                 'dst' => $dst,
00923                 'disposition' => isset( $triple[2] ) ? $triple[2] : null
00924             );
00925             $status->merge( $this->initDirectory( dirname( $dst ) ) );
00926         }
00927         $status->merge( $this->backend->doQuickOperations( $operations ) );
00928 
00929         return $status;
00930     }
00931 
00940     public function quickPurgeBatch( array $paths ) {
00941         $status = $this->newGood();
00942         $operations = array();
00943         foreach ( $paths as $path ) {
00944             $operations[] = array(
00945                 'op' => 'delete',
00946                 'src' => $this->resolveToStoragePath( $path ),
00947                 'ignoreMissingSource' => true
00948             );
00949         }
00950         $status->merge( $this->backend->doQuickOperations( $operations ) );
00951 
00952         return $status;
00953     }
00954 
00965     public function storeTemp( $originalName, $srcPath ) {
00966         $this->assertWritableRepo(); // fail out if read-only
00967 
00968         $date = MWTimestamp::getInstance()->format( 'YmdHis' );
00969         $hashPath = $this->getHashPath( $originalName );
00970         $dstUrlRel = $hashPath . $date . '!' . rawurlencode( $originalName );
00971         $virtualUrl = $this->getVirtualUrl( 'temp' ) . '/' . $dstUrlRel;
00972 
00973         $result = $this->quickImport( $srcPath, $virtualUrl );
00974         $result->value = $virtualUrl;
00975 
00976         return $result;
00977     }
00978 
00985     public function freeTemp( $virtualUrl ) {
00986         $this->assertWritableRepo(); // fail out if read-only
00987 
00988         $temp = $this->getVirtualUrl( 'temp' );
00989         if ( substr( $virtualUrl, 0, strlen( $temp ) ) != $temp ) {
00990             wfDebug( __METHOD__ . ": Invalid temp virtual URL\n" );
00991             return false;
00992         }
00993 
00994         return $this->quickPurge( $virtualUrl )->isOK();
00995     }
00996 
01006     public function concatenate( array $srcPaths, $dstPath, $flags = 0 ) {
01007         $this->assertWritableRepo(); // fail out if read-only
01008 
01009         $status = $this->newGood();
01010 
01011         $sources = array();
01012         foreach ( $srcPaths as $srcPath ) {
01013             // Resolve source to a storage path if virtual
01014             $source = $this->resolveToStoragePath( $srcPath );
01015             $sources[] = $source; // chunk to merge
01016         }
01017 
01018         // Concatenate the chunks into one FS file
01019         $params = array( 'srcs' => $sources, 'dst' => $dstPath );
01020         $status->merge( $this->backend->concatenate( $params ) );
01021         if ( !$status->isOK() ) {
01022             return $status;
01023         }
01024 
01025         // Delete the sources if required
01026         if ( $flags & self::DELETE_SOURCE ) {
01027             $status->merge( $this->quickPurgeBatch( $srcPaths ) );
01028         }
01029 
01030         // Make sure status is OK, despite any quickPurgeBatch() fatals
01031         $status->setResult( true );
01032 
01033         return $status;
01034     }
01035 
01055     public function publish(
01056         $srcPath, $dstRel, $archiveRel, $flags = 0, array $options = array()
01057     ) {
01058         $this->assertWritableRepo(); // fail out if read-only
01059 
01060         $status = $this->publishBatch(
01061             array( array( $srcPath, $dstRel, $archiveRel, $options ) ), $flags );
01062         if ( $status->successCount == 0 ) {
01063             $status->ok = false;
01064         }
01065         if ( isset( $status->value[0] ) ) {
01066             $status->value = $status->value[0];
01067         } else {
01068             $status->value = false;
01069         }
01070 
01071         return $status;
01072     }
01073 
01084     public function publishBatch( array $ntuples, $flags = 0 ) {
01085         $this->assertWritableRepo(); // fail out if read-only
01086 
01087         $backend = $this->backend; // convenience
01088         // Try creating directories
01089         $status = $this->initZones( 'public' );
01090         if ( !$status->isOK() ) {
01091             return $status;
01092         }
01093 
01094         $status = $this->newGood( array() );
01095 
01096         $operations = array();
01097         $sourceFSFilesToDelete = array(); // cleanup for disk source files
01098         // Validate each triplet and get the store operation...
01099         foreach ( $ntuples as $ntuple ) {
01100             list( $srcPath, $dstRel, $archiveRel ) = $ntuple;
01101             $options = isset( $ntuple[3] ) ? $ntuple[3] : array();
01102             // Resolve source to a storage path if virtual
01103             $srcPath = $this->resolveToStoragePath( $srcPath );
01104             if ( !$this->validateFilename( $dstRel ) ) {
01105                 throw new MWException( 'Validation error in $dstRel' );
01106             }
01107             if ( !$this->validateFilename( $archiveRel ) ) {
01108                 throw new MWException( 'Validation error in $archiveRel' );
01109             }
01110 
01111             $publicRoot = $this->getZonePath( 'public' );
01112             $dstPath = "$publicRoot/$dstRel";
01113             $archivePath = "$publicRoot/$archiveRel";
01114 
01115             $dstDir = dirname( $dstPath );
01116             $archiveDir = dirname( $archivePath );
01117             // Abort immediately on directory creation errors since they're likely to be repetitive
01118             if ( !$this->initDirectory( $dstDir )->isOK() ) {
01119                 return $this->newFatal( 'directorycreateerror', $dstDir );
01120             }
01121             if ( !$this->initDirectory( $archiveDir )->isOK() ) {
01122                 return $this->newFatal( 'directorycreateerror', $archiveDir );
01123             }
01124 
01125             // Set any desired headers to be use in GET/HEAD responses
01126             $headers = isset( $options['headers'] ) ? $options['headers'] : array();
01127 
01128             // Archive destination file if it exists.
01129             // This will check if the archive file also exists and fail if does.
01130             // This is a sanity check to avoid data loss. On Windows and Linux,
01131             // copy() will overwrite, so the existence check is vulnerable to
01132             // race conditions unless an functioning LockManager is used.
01133             // LocalFile also uses SELECT FOR UPDATE for synchronization.
01134             $operations[] = array(
01135                 'op' => 'copy',
01136                 'src' => $dstPath,
01137                 'dst' => $archivePath,
01138                 'ignoreMissingSource' => true
01139             );
01140 
01141             // Copy (or move) the source file to the destination
01142             if ( FileBackend::isStoragePath( $srcPath ) ) {
01143                 if ( $flags & self::DELETE_SOURCE ) {
01144                     $operations[] = array(
01145                         'op' => 'move',
01146                         'src' => $srcPath,
01147                         'dst' => $dstPath,
01148                         'overwrite' => true, // replace current
01149                         'headers' => $headers
01150                     );
01151                 } else {
01152                     $operations[] = array(
01153                         'op' => 'copy',
01154                         'src' => $srcPath,
01155                         'dst' => $dstPath,
01156                         'overwrite' => true, // replace current
01157                         'headers' => $headers
01158                     );
01159                 }
01160             } else { // FS source path
01161                 $operations[] = array(
01162                     'op' => 'store',
01163                     'src' => $srcPath,
01164                     'dst' => $dstPath,
01165                     'overwrite' => true, // replace current
01166                     'headers' => $headers
01167                 );
01168                 if ( $flags & self::DELETE_SOURCE ) {
01169                     $sourceFSFilesToDelete[] = $srcPath;
01170                 }
01171             }
01172         }
01173 
01174         // Execute the operations for each triplet
01175         $status->merge( $backend->doOperations( $operations ) );
01176         // Find out which files were archived...
01177         foreach ( $ntuples as $i => $ntuple ) {
01178             list( , , $archiveRel ) = $ntuple;
01179             $archivePath = $this->getZonePath( 'public' ) . "/$archiveRel";
01180             if ( $this->fileExists( $archivePath ) ) {
01181                 $status->value[$i] = 'archived';
01182             } else {
01183                 $status->value[$i] = 'new';
01184             }
01185         }
01186         // Cleanup for disk source files...
01187         foreach ( $sourceFSFilesToDelete as $file ) {
01188             wfSuppressWarnings();
01189             unlink( $file ); // FS cleanup
01190             wfRestoreWarnings();
01191         }
01192 
01193         return $status;
01194     }
01195 
01203     protected function initDirectory( $dir ) {
01204         $path = $this->resolveToStoragePath( $dir );
01205         list( , $container, ) = FileBackend::splitStoragePath( $path );
01206 
01207         $params = array( 'dir' => $path );
01208         if ( $this->isPrivate || $container === $this->zones['deleted']['container'] ) {
01209             # Take all available measures to prevent web accessibility of new deleted
01210             # directories, in case the user has not configured offline storage
01211             $params = array( 'noAccess' => true, 'noListing' => true ) + $params;
01212         }
01213 
01214         return $this->backend->prepare( $params );
01215     }
01216 
01223     public function cleanDir( $dir ) {
01224         $this->assertWritableRepo(); // fail out if read-only
01225 
01226         $status = $this->newGood();
01227         $status->merge( $this->backend->clean(
01228             array( 'dir' => $this->resolveToStoragePath( $dir ) ) ) );
01229 
01230         return $status;
01231     }
01232 
01239     public function fileExists( $file ) {
01240         $result = $this->fileExistsBatch( array( $file ) );
01241         return $result[0];
01242     }
01243 
01250     public function fileExistsBatch( array $files ) {
01251         $result = array();
01252         foreach ( $files as $key => $file ) {
01253             $file = $this->resolveToStoragePath( $file );
01254             $result[$key] = $this->backend->fileExists( array( 'src' => $file ) );
01255         }
01256         return $result;
01257     }
01258 
01269     public function delete( $srcRel, $archiveRel ) {
01270         $this->assertWritableRepo(); // fail out if read-only
01271 
01272         return $this->deleteBatch( array( array( $srcRel, $archiveRel ) ) );
01273     }
01274 
01292     public function deleteBatch( array $sourceDestPairs ) {
01293         $this->assertWritableRepo(); // fail out if read-only
01294 
01295         // Try creating directories
01296         $status = $this->initZones( array( 'public', 'deleted' ) );
01297         if ( !$status->isOK() ) {
01298             return $status;
01299         }
01300 
01301         $status = $this->newGood();
01302 
01303         $backend = $this->backend; // convenience
01304         $operations = array();
01305         // Validate filenames and create archive directories
01306         foreach ( $sourceDestPairs as $pair ) {
01307             list( $srcRel, $archiveRel ) = $pair;
01308             if ( !$this->validateFilename( $srcRel ) ) {
01309                 throw new MWException( __METHOD__ . ':Validation error in $srcRel' );
01310             } elseif ( !$this->validateFilename( $archiveRel ) ) {
01311                 throw new MWException( __METHOD__ . ':Validation error in $archiveRel' );
01312             }
01313 
01314             $publicRoot = $this->getZonePath( 'public' );
01315             $srcPath = "{$publicRoot}/$srcRel";
01316 
01317             $deletedRoot = $this->getZonePath( 'deleted' );
01318             $archivePath = "{$deletedRoot}/{$archiveRel}";
01319             $archiveDir = dirname( $archivePath ); // does not touch FS
01320 
01321             // Create destination directories
01322             if ( !$this->initDirectory( $archiveDir )->isOK() ) {
01323                 return $this->newFatal( 'directorycreateerror', $archiveDir );
01324             }
01325 
01326             $operations[] = array(
01327                 'op' => 'move',
01328                 'src' => $srcPath,
01329                 'dst' => $archivePath,
01330                 // We may have 2+ identical files being deleted,
01331                 // all of which will map to the same destination file
01332                 'overwriteSame' => true // also see bug 31792
01333             );
01334         }
01335 
01336         // Move the files by execute the operations for each pair.
01337         // We're now committed to returning an OK result, which will
01338         // lead to the files being moved in the DB also.
01339         $opts = array( 'force' => true );
01340         $status->merge( $backend->doOperations( $operations, $opts ) );
01341 
01342         return $status;
01343     }
01344 
01350     public function cleanupDeletedBatch( array $storageKeys ) {
01351         $this->assertWritableRepo();
01352     }
01353 
01362     public function getDeletedHashPath( $key ) {
01363         if ( strlen( $key ) < 31 ) {
01364             throw new MWException( "Invalid storage key '$key'." );
01365         }
01366         $path = '';
01367         for ( $i = 0; $i < $this->deletedHashLevels; $i++ ) {
01368             $path .= $key[$i] . '/';
01369         }
01370         return $path;
01371     }
01372 
01381     protected function resolveToStoragePath( $path ) {
01382         if ( $this->isVirtualUrl( $path ) ) {
01383             return $this->resolveVirtualUrl( $path );
01384         }
01385         return $path;
01386     }
01387 
01395     public function getLocalCopy( $virtualUrl ) {
01396         $path = $this->resolveToStoragePath( $virtualUrl );
01397         return $this->backend->getLocalCopy( array( 'src' => $path ) );
01398     }
01399 
01408     public function getLocalReference( $virtualUrl ) {
01409         $path = $this->resolveToStoragePath( $virtualUrl );
01410         return $this->backend->getLocalReference( array( 'src' => $path ) );
01411     }
01412 
01420     public function getFileProps( $virtualUrl ) {
01421         $path = $this->resolveToStoragePath( $virtualUrl );
01422         return $this->backend->getFileProps( array( 'src' => $path ) );
01423     }
01424 
01431     public function getFileTimestamp( $virtualUrl ) {
01432         $path = $this->resolveToStoragePath( $virtualUrl );
01433         return $this->backend->getFileTimestamp( array( 'src' => $path ) );
01434     }
01435 
01442     public function getFileSize( $virtualUrl ) {
01443         $path = $this->resolveToStoragePath( $virtualUrl );
01444         return $this->backend->getFileSize( array( 'src' => $path ) );
01445     }
01446 
01453     public function getFileSha1( $virtualUrl ) {
01454         $path = $this->resolveToStoragePath( $virtualUrl );
01455         return $this->backend->getFileSha1Base36( array( 'src' => $path ) );
01456     }
01457 
01465     public function streamFile( $virtualUrl, $headers = array() ) {
01466         $path = $this->resolveToStoragePath( $virtualUrl );
01467         $params = array( 'src' => $path, 'headers' => $headers );
01468         return $this->backend->streamFile( $params )->isOK();
01469     }
01470 
01479     public function enumFiles( $callback ) {
01480         $this->enumFilesInStorage( $callback );
01481     }
01482 
01490     protected function enumFilesInStorage( $callback ) {
01491         $publicRoot = $this->getZonePath( 'public' );
01492         $numDirs = 1 << ( $this->hashLevels * 4 );
01493         // Use a priori assumptions about directory structure
01494         // to reduce the tree height of the scanning process.
01495         for ( $flatIndex = 0; $flatIndex < $numDirs; $flatIndex++ ) {
01496             $hexString = sprintf( "%0{$this->hashLevels}x", $flatIndex );
01497             $path = $publicRoot;
01498             for ( $hexPos = 0; $hexPos < $this->hashLevels; $hexPos++ ) {
01499                 $path .= '/' . substr( $hexString, 0, $hexPos + 1 );
01500             }
01501             $iterator = $this->backend->getFileList( array( 'dir' => $path ) );
01502             foreach ( $iterator as $name ) {
01503                 // Each item returned is a public file
01504                 call_user_func( $callback, "{$path}/{$name}" );
01505             }
01506         }
01507     }
01508 
01515     public function validateFilename( $filename ) {
01516         if ( strval( $filename ) == '' ) {
01517             return false;
01518         }
01519         return FileBackend::isPathTraversalFree( $filename );
01520     }
01521 
01527     function getErrorCleanupFunction() {
01528         switch ( $this->pathDisclosureProtection ) {
01529             case 'none':
01530             case 'simple': // b/c
01531                 $callback = array( $this, 'passThrough' );
01532                 break;
01533             default: // 'paranoid'
01534                 $callback = array( $this, 'paranoidClean' );
01535         }
01536         return $callback;
01537     }
01538 
01545     function paranoidClean( $param ) {
01546         return '[hidden]';
01547     }
01548 
01555     function passThrough( $param ) {
01556         return $param;
01557     }
01558 
01564     public function newFatal( $message /*, parameters...*/ ) {
01565         $params = func_get_args();
01566         array_unshift( $params, $this );
01567         return call_user_func_array( array( 'FileRepoStatus', 'newFatal' ), $params );
01568     }
01569 
01576     public function newGood( $value = null ) {
01577         return FileRepoStatus::newGood( $this, $value );
01578     }
01579 
01588     public function checkRedirect( Title $title ) {
01589         return false;
01590     }
01591 
01599     public function invalidateImageRedirect( Title $title ) {}
01600 
01606     public function getDisplayName() {
01607         // We don't name our own repo, return nothing
01608         if ( $this->isLocal() ) {
01609             return null;
01610         }
01611         // 'shared-repo-name-wikimediacommons' is used when $wgUseInstantCommons = true
01612         return wfMessageFallback( 'shared-repo-name-' . $this->name, 'shared-repo' )->text();
01613     }
01614 
01622     public function nameForThumb( $name ) {
01623         if ( strlen( $name ) > $this->abbrvThreshold ) {
01624             $ext = FileBackend::extensionFromPath( $name );
01625             $name = ( $ext == '' ) ? 'thumbnail' : "thumbnail.$ext";
01626         }
01627         return $name;
01628     }
01629 
01635     public function isLocal() {
01636         return $this->getName() == 'local';
01637     }
01638 
01647     public function getSharedCacheKey( /*...*/ ) {
01648         return false;
01649     }
01650 
01658     public function getLocalCacheKey( /*...*/ ) {
01659         $args = func_get_args();
01660         array_unshift( $args, 'filerepo', $this->getName() );
01661         return call_user_func_array( 'wfMemcKey', $args );
01662     }
01663 
01672     public function getTempRepo() {
01673         return new TempFileRepo( array(
01674             'name' => "{$this->name}-temp",
01675             'backend' => $this->backend,
01676             'zones' => array(
01677                 'public' => array(
01678                     'container' => $this->zones['temp']['container'],
01679                     'directory' => $this->zones['temp']['directory']
01680                 ),
01681                 'thumb' => array(
01682                     'container' => $this->zones['thumb']['container'],
01683                     'directory' => ( $this->zones['thumb']['directory'] == '' )
01684                         ? 'temp'
01685                         : $this->zones['thumb']['directory'] . '/temp'
01686                 ),
01687                 'transcoded' => array(
01688                     'container' => $this->zones['transcoded']['container'],
01689                     'directory' => ( $this->zones['transcoded']['directory'] == '' )
01690                         ? 'temp'
01691                         : $this->zones['transcoded']['directory'] . '/temp'
01692                 )
01693             ),
01694             'url' => $this->getZoneUrl( 'temp' ),
01695             'thumbUrl' => $this->getZoneUrl( 'thumb' ) . '/temp',
01696             'transcodedUrl' => $this->getZoneUrl( 'transcoded' ) . '/temp',
01697             'hashLevels' => $this->hashLevels // performance
01698         ) );
01699     }
01700 
01707     public function getUploadStash( User $user = null ) {
01708         return new UploadStash( $this, $user );
01709     }
01710 
01718     protected function assertWritableRepo() {}
01719 
01720 
01727     public function getInfo() {
01728         return array(
01729             'name' => $this->getName(),
01730             'displayname' => $this->getDisplayName(),
01731             'rootUrl' => $this->getRootUrl(),
01732             'local' => $this->isLocal(),
01733         );
01734     }
01735 }
01736 
01740 class TempFileRepo extends FileRepo {
01741     public function getTempRepo() {
01742         throw new MWException( "Cannot get a temp repo from a temp repo." );
01743     }
01744 }