MediaWiki  master
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', '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' ) ) ) { // 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                         default:
00224                                 return false;
00225                 }
00226         }
00227 
00241         public function getZoneHandlerUrl( $zone ) {
00242                 if ( isset( $this->zones[$zone]['handlerUrl'] )
00243                         && in_array( $zone, array( 'public', 'temp', 'thumb' ) ) )
00244                 {
00245                         return $this->zones[$zone]['handlerUrl'];
00246                 }
00247                 return false;
00248         }
00249 
00258         public function resolveVirtualUrl( $url ) {
00259                 if ( substr( $url, 0, 9 ) != 'mwrepo://' ) {
00260                         throw new MWException( __METHOD__.': unknown protocol' );
00261                 }
00262                 $bits = explode( '/', substr( $url, 9 ), 3 );
00263                 if ( count( $bits ) != 3 ) {
00264                         throw new MWException( __METHOD__.": invalid mwrepo URL: $url" );
00265                 }
00266                 list( $repo, $zone, $rel ) = $bits;
00267                 if ( $repo !== $this->name ) {
00268                         throw new MWException( __METHOD__.": fetching from a foreign repo is not supported" );
00269                 }
00270                 $base = $this->getZonePath( $zone );
00271                 if ( !$base ) {
00272                         throw new MWException( __METHOD__.": invalid zone: $zone" );
00273                 }
00274                 return $base . '/' . rawurldecode( $rel );
00275         }
00276 
00283         protected function getZoneLocation( $zone ) {
00284                 if ( !isset( $this->zones[$zone] ) ) {
00285                         return array( null, null ); // bogus
00286                 }
00287                 return array( $this->zones[$zone]['container'], $this->zones[$zone]['directory'] );
00288         }
00289 
00296         public function getZonePath( $zone ) {
00297                 list( $container, $base ) = $this->getZoneLocation( $zone );
00298                 if ( $container === null || $base === null ) {
00299                         return null;
00300                 }
00301                 $backendName = $this->backend->getName();
00302                 if ( $base != '' ) { // may not be set
00303                         $base = "/{$base}";
00304                 }
00305                 return "mwstore://$backendName/{$container}{$base}";
00306         }
00307 
00319         public function newFile( $title, $time = false ) {
00320                 $title = File::normalizeTitle( $title );
00321                 if ( !$title ) {
00322                         return null;
00323                 }
00324                 if ( $time ) {
00325                         if ( $this->oldFileFactory ) {
00326                                 return call_user_func( $this->oldFileFactory, $title, $this, $time );
00327                         } else {
00328                                 return false;
00329                         }
00330                 } else {
00331                         return call_user_func( $this->fileFactory, $title, $this );
00332                 }
00333         }
00334 
00353         public function findFile( $title, $options = array() ) {
00354                 $title = File::normalizeTitle( $title );
00355                 if ( !$title ) {
00356                         return false;
00357                 }
00358                 $time = isset( $options['time'] ) ? $options['time'] : false;
00359                 # First try the current version of the file to see if it precedes the timestamp
00360                 $img = $this->newFile( $title );
00361                 if ( !$img ) {
00362                         return false;
00363                 }
00364                 if ( $img->exists() && ( !$time || $img->getTimestamp() == $time ) ) {
00365                         return $img;
00366                 }
00367                 # Now try an old version of the file
00368                 if ( $time !== false ) {
00369                         $img = $this->newFile( $title, $time );
00370                         if ( $img && $img->exists() ) {
00371                                 if ( !$img->isDeleted( File::DELETED_FILE ) ) {
00372                                         return $img; // always OK
00373                                 } elseif ( !empty( $options['private'] ) && $img->userCan( File::DELETED_FILE ) ) {
00374                                         return $img;
00375                                 }
00376                         }
00377                 }
00378 
00379                 # Now try redirects
00380                 if ( !empty( $options['ignoreRedirect'] ) ) {
00381                         return false;
00382                 }
00383                 $redir = $this->checkRedirect( $title );
00384                 if ( $redir && $title->getNamespace() == NS_FILE) {
00385                         $img = $this->newFile( $redir );
00386                         if ( !$img ) {
00387                                 return false;
00388                         }
00389                         if ( $img->exists() ) {
00390                                 $img->redirectedFrom( $title->getDBkey() );
00391                                 return $img;
00392                         }
00393                 }
00394                 return false;
00395         }
00396 
00408         public function findFiles( array $items ) {
00409                 $result = array();
00410                 foreach ( $items as $item ) {
00411                         if ( is_array( $item ) ) {
00412                                 $title = $item['title'];
00413                                 $options = $item;
00414                                 unset( $options['title'] );
00415                         } else {
00416                                 $title = $item;
00417                                 $options = array();
00418                         }
00419                         $file = $this->findFile( $title, $options );
00420                         if ( $file ) {
00421                                 $result[$file->getTitle()->getDBkey()] = $file;
00422                         }
00423                 }
00424                 return $result;
00425         }
00426 
00436         public function findFileFromKey( $sha1, $options = array() ) {
00437                 $time = isset( $options['time'] ) ? $options['time'] : false;
00438                 # First try to find a matching current version of a file...
00439                 if ( $this->fileFactoryKey ) {
00440                         $img = call_user_func( $this->fileFactoryKey, $sha1, $this, $time );
00441                 } else {
00442                         return false; // find-by-sha1 not supported
00443                 }
00444                 if ( $img && $img->exists() ) {
00445                         return $img;
00446                 }
00447                 # Now try to find a matching old version of a file...
00448                 if ( $time !== false && $this->oldFileFactoryKey ) { // find-by-sha1 supported?
00449                         $img = call_user_func( $this->oldFileFactoryKey, $sha1, $this, $time );
00450                         if ( $img && $img->exists() ) {
00451                                 if ( !$img->isDeleted( File::DELETED_FILE ) ) {
00452                                         return $img; // always OK
00453                                 } elseif ( !empty( $options['private'] ) && $img->userCan( File::DELETED_FILE ) ) {
00454                                         return $img;
00455                                 }
00456                         }
00457                 }
00458                 return false;
00459         }
00460 
00469         public function findBySha1( $hash ) {
00470                 return array();
00471         }
00472 
00480         public function findBySha1s( array $hashes ) {
00481                 $result = array();
00482                 foreach ( $hashes as $hash ) {
00483                         $files = $this->findBySha1( $hash );
00484                         if ( count( $files ) ) {
00485                                 $result[$hash] = $files;
00486                         }
00487                 }
00488                 return $result;
00489         }
00490 
00497         public function getRootUrl() {
00498                 return $this->getZoneUrl( 'public' );
00499         }
00500 
00506         public function getThumbScriptUrl() {
00507                 return $this->thumbScriptUrl;
00508         }
00509 
00515         public function canTransformVia404() {
00516                 return $this->transformVia404;
00517         }
00518 
00525         public function getNameFromTitle( Title $title ) {
00526                 global $wgContLang;
00527                 if ( $this->initialCapital != MWNamespace::isCapitalized( NS_FILE ) ) {
00528                         $name = $title->getUserCaseDBKey();
00529                         if ( $this->initialCapital ) {
00530                                 $name = $wgContLang->ucfirst( $name );
00531                         }
00532                 } else {
00533                         $name = $title->getDBkey();
00534                 }
00535                 return $name;
00536         }
00537 
00543         public function getRootDirectory() {
00544                 return $this->getZonePath( 'public' );
00545         }
00546 
00554         public function getHashPath( $name ) {
00555                 return self::getHashPathForLevel( $name, $this->hashLevels );
00556         }
00557 
00565         public function getTempHashPath( $suffix ) {
00566                 $parts = explode( '!', $suffix, 2 ); // format is <timestamp>!<name> or just <name>
00567                 $name = isset( $parts[1] ) ? $parts[1] : $suffix; // hash path is not based on timestamp
00568                 return self::getHashPathForLevel( $name, $this->hashLevels );
00569         }
00570 
00576         protected static function getHashPathForLevel( $name, $levels ) {
00577                 if ( $levels == 0 ) {
00578                         return '';
00579                 } else {
00580                         $hash = md5( $name );
00581                         $path = '';
00582                         for ( $i = 1; $i <= $levels; $i++ ) {
00583                                 $path .= substr( $hash, 0, $i ) . '/';
00584                         }
00585                         return $path;
00586                 }
00587         }
00588 
00594         public function getHashLevels() {
00595                 return $this->hashLevels;
00596         }
00597 
00603         public function getName() {
00604                 return $this->name;
00605         }
00606 
00614         public function makeUrl( $query = '', $entry = 'index' ) {
00615                 if ( isset( $this->scriptDirUrl ) ) {
00616                         $ext = isset( $this->scriptExtension ) ? $this->scriptExtension : '.php';
00617                         return wfAppendQuery( "{$this->scriptDirUrl}/{$entry}{$ext}", $query );
00618                 }
00619                 return false;
00620         }
00621 
00634         public function getDescriptionUrl( $name ) {
00635                 $encName = wfUrlencode( $name );
00636                 if ( !is_null( $this->descBaseUrl ) ) {
00637                         # "http://example.com/wiki/Image:"
00638                         return $this->descBaseUrl . $encName;
00639                 }
00640                 if ( !is_null( $this->articleUrl ) ) {
00641                         # "http://example.com/wiki/$1"
00642                         #
00643                         # We use "Image:" as the canonical namespace for
00644                         # compatibility across all MediaWiki versions.
00645                         return str_replace( '$1',
00646                                 "Image:$encName", $this->articleUrl );
00647                 }
00648                 if ( !is_null( $this->scriptDirUrl ) ) {
00649                         # "http://example.com/w"
00650                         #
00651                         # We use "Image:" as the canonical namespace for
00652                         # compatibility across all MediaWiki versions,
00653                         # and just sort of hope index.php is right. ;)
00654                         return $this->makeUrl( "title=Image:$encName" );
00655                 }
00656                 return false;
00657         }
00658 
00669         public function getDescriptionRenderUrl( $name, $lang = null ) {
00670                 $query = 'action=render';
00671                 if ( !is_null( $lang ) ) {
00672                         $query .= '&uselang=' . $lang;
00673                 }
00674                 if ( isset( $this->scriptDirUrl ) ) {
00675                         return $this->makeUrl(
00676                                 'title=' .
00677                                 wfUrlencode( 'Image:' . $name ) .
00678                                 "&$query" );
00679                 } else {
00680                         $descUrl = $this->getDescriptionUrl( $name );
00681                         if ( $descUrl ) {
00682                                 return wfAppendQuery( $descUrl, $query );
00683                         } else {
00684                                 return false;
00685                         }
00686                 }
00687         }
00688 
00694         public function getDescriptionStylesheetUrl() {
00695                 if ( isset( $this->scriptDirUrl ) ) {
00696                         return $this->makeUrl( 'title=MediaWiki:Filepage.css&' .
00697                                 wfArrayToCGI( Skin::getDynamicStylesheetQuery() ) );
00698                 }
00699                 return false;
00700         }
00701 
00716         public function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) {
00717                 $this->assertWritableRepo(); // fail out if read-only
00718 
00719                 $status = $this->storeBatch( array( array( $srcPath, $dstZone, $dstRel ) ), $flags );
00720                 if ( $status->successCount == 0 ) {
00721                         $status->ok = false;
00722                 }
00723 
00724                 return $status;
00725         }
00726 
00740         public function storeBatch( array $triplets, $flags = 0 ) {
00741                 $this->assertWritableRepo(); // fail out if read-only
00742 
00743                 $status = $this->newGood();
00744                 $backend = $this->backend; // convenience
00745 
00746                 $operations = array();
00747                 $sourceFSFilesToDelete = array(); // cleanup for disk source files
00748                 // Validate each triplet and get the store operation...
00749                 foreach ( $triplets as $triplet ) {
00750                         list( $srcPath, $dstZone, $dstRel ) = $triplet;
00751                         wfDebug( __METHOD__
00752                                 . "( \$src='$srcPath', \$dstZone='$dstZone', \$dstRel='$dstRel' )\n"
00753                         );
00754 
00755                         // Resolve destination path
00756                         $root = $this->getZonePath( $dstZone );
00757                         if ( !$root ) {
00758                                 throw new MWException( "Invalid zone: $dstZone" );
00759                         }
00760                         if ( !$this->validateFilename( $dstRel ) ) {
00761                                 throw new MWException( 'Validation error in $dstRel' );
00762                         }
00763                         $dstPath = "$root/$dstRel";
00764                         $dstDir  = dirname( $dstPath );
00765                         // Create destination directories for this triplet
00766                         if ( !$this->initDirectory( $dstDir )->isOK() ) {
00767                                 return $this->newFatal( 'directorycreateerror', $dstDir );
00768                         }
00769 
00770                         // Resolve source to a storage path if virtual
00771                         $srcPath = $this->resolveToStoragePath( $srcPath );
00772 
00773                         // Get the appropriate file operation
00774                         if ( FileBackend::isStoragePath( $srcPath ) ) {
00775                                 $opName = ( $flags & self::DELETE_SOURCE ) ? 'move' : 'copy';
00776                         } else {
00777                                 $opName = 'store';
00778                                 if ( $flags & self::DELETE_SOURCE ) {
00779                                         $sourceFSFilesToDelete[] = $srcPath;
00780                                 }
00781                         }
00782                         $operations[] = array(
00783                                 'op'            => $opName,
00784                                 'src'           => $srcPath,
00785                                 'dst'           => $dstPath,
00786                                 'overwrite'     => $flags & self::OVERWRITE,
00787                                 'overwriteSame' => $flags & self::OVERWRITE_SAME,
00788                         );
00789                 }
00790 
00791                 // Execute the store operation for each triplet
00792                 $opts = array( 'force' => true );
00793                 if ( $flags & self::SKIP_LOCKING ) {
00794                         $opts['nonLocking'] = true;
00795                 }
00796                 $status->merge( $backend->doOperations( $operations, $opts ) );
00797                 // Cleanup for disk source files...
00798                 foreach ( $sourceFSFilesToDelete as $file ) {
00799                         wfSuppressWarnings();
00800                         unlink( $file ); // FS cleanup
00801                         wfRestoreWarnings();
00802                 }
00803 
00804                 return $status;
00805         }
00806 
00817         public function cleanupBatch( array $files, $flags = 0 ) {
00818                 $this->assertWritableRepo(); // fail out if read-only
00819 
00820                 $status = $this->newGood();
00821 
00822                 $operations = array();
00823                 foreach ( $files as $path ) {
00824                         if ( is_array( $path ) ) {
00825                                 // This is a pair, extract it
00826                                 list( $zone, $rel ) = $path;
00827                                 $path = $this->getZonePath( $zone ) . "/$rel";
00828                         } else {
00829                                 // Resolve source to a storage path if virtual
00830                                 $path = $this->resolveToStoragePath( $path );
00831                         }
00832                         $operations[] = array( 'op' => 'delete', 'src' => $path );
00833                 }
00834                 // Actually delete files from storage...
00835                 $opts = array( 'force' => true );
00836                 if ( $flags & self::SKIP_LOCKING ) {
00837                         $opts['nonLocking'] = true;
00838                 }
00839                 $status->merge( $this->backend->doOperations( $operations, $opts ) );
00840 
00841                 return $status;
00842         }
00843 
00855         final public function quickImport( $src, $dst, $disposition = null ) {
00856                 return $this->quickImportBatch( array( array( $src, $dst, $disposition ) ) );
00857         }
00858 
00867         final public function quickPurge( $path ) {
00868                 return $this->quickPurgeBatch( array( $path ) );
00869         }
00870 
00878         public function quickCleanDir( $dir ) {
00879                 $status = $this->newGood();
00880                 $status->merge( $this->backend->clean(
00881                         array( 'dir' => $this->resolveToStoragePath( $dir ) ) ) );
00882 
00883                 return $status;
00884         }
00885 
00898         public function quickImportBatch( array $triples ) {
00899                 $status = $this->newGood();
00900                 $operations = array();
00901                 foreach ( $triples as $triple ) {
00902                         list( $src, $dst ) = $triple;
00903                         $src = $this->resolveToStoragePath( $src );
00904                         $dst = $this->resolveToStoragePath( $dst );
00905                         $operations[] = array(
00906                                 'op'          => FileBackend::isStoragePath( $src ) ? 'copy' : 'store',
00907                                 'src'         => $src,
00908                                 'dst'         => $dst,
00909                                 'disposition' => isset( $triple[2] ) ? $triple[2] : null
00910                         );
00911                         $status->merge( $this->initDirectory( dirname( $dst ) ) );
00912                 }
00913                 $status->merge( $this->backend->doQuickOperations( $operations ) );
00914 
00915                 return $status;
00916         }
00917 
00926         public function quickPurgeBatch( array $paths ) {
00927                 $status = $this->newGood();
00928                 $operations = array();
00929                 foreach ( $paths as $path ) {
00930                         $operations[] = array(
00931                                 'op'                  => 'delete',
00932                                 'src'                 => $this->resolveToStoragePath( $path ),
00933                                 'ignoreMissingSource' => true
00934                         );
00935                 }
00936                 $status->merge( $this->backend->doQuickOperations( $operations ) );
00937 
00938                 return $status;
00939         }
00940 
00951         public function storeTemp( $originalName, $srcPath ) {
00952                 $this->assertWritableRepo(); // fail out if read-only
00953 
00954                 $date       = gmdate( "YmdHis" );
00955                 $hashPath   = $this->getHashPath( $originalName );
00956                 $dstRel     = "{$hashPath}{$date}!{$originalName}";
00957                 $dstUrlRel  = $hashPath . $date . '!' . rawurlencode( $originalName );
00958                 $virtualUrl = $this->getVirtualUrl( 'temp' )  . '/' . $dstUrlRel;
00959 
00960                 $result = $this->quickImport( $srcPath, $virtualUrl );
00961                 $result->value = $virtualUrl;
00962 
00963                 return $result;
00964         }
00965 
00972         public function freeTemp( $virtualUrl ) {
00973                 $this->assertWritableRepo(); // fail out if read-only
00974 
00975                 $temp = $this->getVirtualUrl( 'temp' );
00976                 if ( substr( $virtualUrl, 0, strlen( $temp ) ) != $temp ) {
00977                         wfDebug( __METHOD__.": Invalid temp virtual URL\n" );
00978                         return false;
00979                 }
00980 
00981                 return $this->quickPurge( $virtualUrl )->isOK();
00982         }
00983 
00993         public function concatenate( array $srcPaths, $dstPath, $flags = 0 ) {
00994                 $this->assertWritableRepo(); // fail out if read-only
00995 
00996                 $status = $this->newGood();
00997 
00998                 $sources = array();
00999                 foreach ( $srcPaths as $srcPath ) {
01000                         // Resolve source to a storage path if virtual
01001                         $source = $this->resolveToStoragePath( $srcPath );
01002                         $sources[] = $source; // chunk to merge
01003                 }
01004 
01005                 // Concatenate the chunks into one FS file
01006                 $params = array( 'srcs' => $sources, 'dst' => $dstPath );
01007                 $status->merge( $this->backend->concatenate( $params ) );
01008                 if ( !$status->isOK() ) {
01009                         return $status;
01010                 }
01011 
01012                 // Delete the sources if required
01013                 if ( $flags & self::DELETE_SOURCE ) {
01014                         $status->merge( $this->quickPurgeBatch( $srcPaths ) );
01015                 }
01016 
01017                 // Make sure status is OK, despite any quickPurgeBatch() fatals
01018                 $status->setResult( true );
01019 
01020                 return $status;
01021         }
01022 
01038         public function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) {
01039                 $this->assertWritableRepo(); // fail out if read-only
01040 
01041                 $status = $this->publishBatch( array( array( $srcPath, $dstRel, $archiveRel ) ), $flags );
01042                 if ( $status->successCount == 0 ) {
01043                         $status->ok = false;
01044                 }
01045                 if ( isset( $status->value[0] ) ) {
01046                         $status->value = $status->value[0];
01047                 } else {
01048                         $status->value = false;
01049                 }
01050 
01051                 return $status;
01052         }
01053 
01063         public function publishBatch( array $triplets, $flags = 0 ) {
01064                 $this->assertWritableRepo(); // fail out if read-only
01065 
01066                 $backend = $this->backend; // convenience
01067                 // Try creating directories
01068                 $status = $this->initZones( 'public' );
01069                 if ( !$status->isOK() ) {
01070                         return $status;
01071                 }
01072 
01073                 $status = $this->newGood( array() );
01074 
01075                 $operations = array();
01076                 $sourceFSFilesToDelete = array(); // cleanup for disk source files
01077                 // Validate each triplet and get the store operation...
01078                 foreach ( $triplets as $i => $triplet ) {
01079                         list( $srcPath, $dstRel, $archiveRel ) = $triplet;
01080                         // Resolve source to a storage path if virtual
01081                         $srcPath = $this->resolveToStoragePath( $srcPath );
01082                         if ( !$this->validateFilename( $dstRel ) ) {
01083                                 throw new MWException( 'Validation error in $dstRel' );
01084                         }
01085                         if ( !$this->validateFilename( $archiveRel ) ) {
01086                                 throw new MWException( 'Validation error in $archiveRel' );
01087                         }
01088 
01089                         $publicRoot = $this->getZonePath( 'public' );
01090                         $dstPath = "$publicRoot/$dstRel";
01091                         $archivePath = "$publicRoot/$archiveRel";
01092 
01093                         $dstDir = dirname( $dstPath );
01094                         $archiveDir = dirname( $archivePath );
01095                         // Abort immediately on directory creation errors since they're likely to be repetitive
01096                         if ( !$this->initDirectory( $dstDir )->isOK() ) {
01097                                 return $this->newFatal( 'directorycreateerror', $dstDir );
01098                         }
01099                         if ( !$this->initDirectory($archiveDir )->isOK() ) {
01100                                 return $this->newFatal( 'directorycreateerror', $archiveDir );
01101                         }
01102 
01103                         // Archive destination file if it exists.
01104                         // This will check if the archive file also exists and fail if does.
01105                         // This is a sanity check to avoid data loss. On Windows and Linux,
01106                         // copy() will overwrite, so the existence check is vulnerable to
01107                         // race conditions unless an functioning LockManager is used.
01108                         // LocalFile also uses SELECT FOR UPDATE for synchronization.
01109                         $operations[] = array(
01110                                 'op'                  => 'copy',
01111                                 'src'                 => $dstPath,
01112                                 'dst'                 => $archivePath,
01113                                 'ignoreMissingSource' => true
01114                         );
01115 
01116                         // Copy (or move) the source file to the destination
01117                         if ( FileBackend::isStoragePath( $srcPath ) ) {
01118                                 if ( $flags & self::DELETE_SOURCE ) {
01119                                         $operations[] = array(
01120                                                 'op'           => 'move',
01121                                                 'src'          => $srcPath,
01122                                                 'dst'          => $dstPath,
01123                                                 'overwrite'    => true // replace current
01124                                         );
01125                                 } else {
01126                                         $operations[] = array(
01127                                                 'op'           => 'copy',
01128                                                 'src'          => $srcPath,
01129                                                 'dst'          => $dstPath,
01130                                                 'overwrite'    => true // replace current
01131                                         );
01132                                 }
01133                         } else { // FS source path
01134                                 $operations[] = array(
01135                                         'op'           => 'store',
01136                                         'src'          => $srcPath,
01137                                         'dst'          => $dstPath,
01138                                         'overwrite'    => true // replace current
01139                                 );
01140                                 if ( $flags & self::DELETE_SOURCE ) {
01141                                         $sourceFSFilesToDelete[] = $srcPath;
01142                                 }
01143                         }
01144                 }
01145 
01146                 // Execute the operations for each triplet
01147                 $status->merge( $backend->doOperations( $operations ) );
01148                 // Find out which files were archived...
01149                 foreach ( $triplets as $i => $triplet ) {
01150                         list( $srcPath, $dstRel, $archiveRel ) = $triplet;
01151                         $archivePath = $this->getZonePath( 'public' ) . "/$archiveRel";
01152                         if ( $this->fileExists( $archivePath ) ) {
01153                                 $status->value[$i] = 'archived';
01154                         } else {
01155                                 $status->value[$i] = 'new';
01156                         }
01157                 }
01158                 // Cleanup for disk source files...
01159                 foreach ( $sourceFSFilesToDelete as $file ) {
01160                         wfSuppressWarnings();
01161                         unlink( $file ); // FS cleanup
01162                         wfRestoreWarnings();
01163                 }
01164 
01165                 return $status;
01166         }
01167 
01175         protected function initDirectory( $dir ) {
01176                 $path = $this->resolveToStoragePath( $dir );
01177                 list( $b, $container, $r ) = FileBackend::splitStoragePath( $path );
01178 
01179                 $params = array( 'dir' => $path );
01180                 if ( $this->isPrivate || $container === $this->zones['deleted']['container'] ) {
01181                         # Take all available measures to prevent web accessibility of new deleted
01182                         # directories, in case the user has not configured offline storage
01183                         $params = array( 'noAccess' => true, 'noListing' => true ) + $params;
01184                 }
01185 
01186                 return $this->backend->prepare( $params );
01187         }
01188 
01195         public function cleanDir( $dir ) {
01196                 $this->assertWritableRepo(); // fail out if read-only
01197 
01198                 $status = $this->newGood();
01199                 $status->merge( $this->backend->clean(
01200                         array( 'dir' => $this->resolveToStoragePath( $dir ) ) ) );
01201 
01202                 return $status;
01203         }
01204 
01211         public function fileExists( $file ) {
01212                 $result = $this->fileExistsBatch( array( $file ) );
01213                 return $result[0];
01214         }
01215 
01222         public function fileExistsBatch( array $files ) {
01223                 $result = array();
01224                 foreach ( $files as $key => $file ) {
01225                         $file = $this->resolveToStoragePath( $file );
01226                         $result[$key] = $this->backend->fileExists( array( 'src' => $file ) );
01227                 }
01228                 return $result;
01229         }
01230 
01241         public function delete( $srcRel, $archiveRel ) {
01242                 $this->assertWritableRepo(); // fail out if read-only
01243 
01244                 return $this->deleteBatch( array( array( $srcRel, $archiveRel ) ) );
01245         }
01246 
01264         public function deleteBatch( array $sourceDestPairs ) {
01265                 $this->assertWritableRepo(); // fail out if read-only
01266 
01267                 // Try creating directories
01268                 $status = $this->initZones( array( 'public', 'deleted' ) );
01269                 if ( !$status->isOK() ) {
01270                         return $status;
01271                 }
01272 
01273                 $status = $this->newGood();
01274 
01275                 $backend = $this->backend; // convenience
01276                 $operations = array();
01277                 // Validate filenames and create archive directories
01278                 foreach ( $sourceDestPairs as $pair ) {
01279                         list( $srcRel, $archiveRel ) = $pair;
01280                         if ( !$this->validateFilename( $srcRel ) ) {
01281                                 throw new MWException( __METHOD__.':Validation error in $srcRel' );
01282                         } elseif ( !$this->validateFilename( $archiveRel ) ) {
01283                                 throw new MWException( __METHOD__.':Validation error in $archiveRel' );
01284                         }
01285 
01286                         $publicRoot = $this->getZonePath( 'public' );
01287                         $srcPath = "{$publicRoot}/$srcRel";
01288 
01289                         $deletedRoot = $this->getZonePath( 'deleted' );
01290                         $archivePath = "{$deletedRoot}/{$archiveRel}";
01291                         $archiveDir = dirname( $archivePath ); // does not touch FS
01292 
01293                         // Create destination directories
01294                         if ( !$this->initDirectory( $archiveDir )->isOK() ) {
01295                                 return $this->newFatal( 'directorycreateerror', $archiveDir );
01296                         }
01297 
01298                         $operations[] = array(
01299                                 'op'            => 'move',
01300                                 'src'           => $srcPath,
01301                                 'dst'           => $archivePath,
01302                                 // We may have 2+ identical files being deleted,
01303                                 // all of which will map to the same destination file
01304                                 'overwriteSame' => true // also see bug 31792
01305                         );
01306                 }
01307 
01308                 // Move the files by execute the operations for each pair.
01309                 // We're now committed to returning an OK result, which will
01310                 // lead to the files being moved in the DB also.
01311                 $opts = array( 'force' => true );
01312                 $status->merge( $backend->doOperations( $operations, $opts ) );
01313 
01314                 return $status;
01315         }
01316 
01322         public function cleanupDeletedBatch( array $storageKeys ) {
01323                 $this->assertWritableRepo();
01324         }
01325 
01334         public function getDeletedHashPath( $key ) {
01335                 if ( strlen( $key ) < 31 ) {
01336                         throw new MWException( "Invalid storage key '$key'." );
01337                 }
01338                 $path = '';
01339                 for ( $i = 0; $i < $this->deletedHashLevels; $i++ ) {
01340                         $path .= $key[$i] . '/';
01341                 }
01342                 return $path;
01343         }
01344 
01353         protected function resolveToStoragePath( $path ) {
01354                 if ( $this->isVirtualUrl( $path ) ) {
01355                         return $this->resolveVirtualUrl( $path );
01356                 }
01357                 return $path;
01358         }
01359 
01367         public function getLocalCopy( $virtualUrl ) {
01368                 $path = $this->resolveToStoragePath( $virtualUrl );
01369                 return $this->backend->getLocalCopy( array( 'src' => $path ) );
01370         }
01371 
01380         public function getLocalReference( $virtualUrl ) {
01381                 $path = $this->resolveToStoragePath( $virtualUrl );
01382                 return $this->backend->getLocalReference( array( 'src' => $path ) );
01383         }
01384 
01392         public function getFileProps( $virtualUrl ) {
01393                 $path = $this->resolveToStoragePath( $virtualUrl );
01394                 return $this->backend->getFileProps( array( 'src' => $path ) );
01395         }
01396 
01403         public function getFileTimestamp( $virtualUrl ) {
01404                 $path = $this->resolveToStoragePath( $virtualUrl );
01405                 return $this->backend->getFileTimestamp( array( 'src' => $path ) );
01406         }
01407 
01414         public function getFileSha1( $virtualUrl ) {
01415                 $path = $this->resolveToStoragePath( $virtualUrl );
01416                 return $this->backend->getFileSha1Base36( array( 'src' => $path ) );
01417         }
01418 
01426         public function streamFile( $virtualUrl, $headers = array() ) {
01427                 $path = $this->resolveToStoragePath( $virtualUrl );
01428                 $params = array( 'src' => $path, 'headers' => $headers );
01429                 return $this->backend->streamFile( $params )->isOK();
01430         }
01431 
01440         public function enumFiles( $callback ) {
01441                 $this->enumFilesInStorage( $callback );
01442         }
01443 
01451         protected function enumFilesInStorage( $callback ) {
01452                 $publicRoot = $this->getZonePath( 'public' );
01453                 $numDirs = 1 << ( $this->hashLevels * 4 );
01454                 // Use a priori assumptions about directory structure
01455                 // to reduce the tree height of the scanning process.
01456                 for ( $flatIndex = 0; $flatIndex < $numDirs; $flatIndex++ ) {
01457                         $hexString = sprintf( "%0{$this->hashLevels}x", $flatIndex );
01458                         $path = $publicRoot;
01459                         for ( $hexPos = 0; $hexPos < $this->hashLevels; $hexPos++ ) {
01460                                 $path .= '/' . substr( $hexString, 0, $hexPos + 1 );
01461                         }
01462                         $iterator = $this->backend->getFileList( array( 'dir' => $path ) );
01463                         foreach ( $iterator as $name ) {
01464                                 // Each item returned is a public file
01465                                 call_user_func( $callback, "{$path}/{$name}" );
01466                         }
01467                 }
01468         }
01469 
01476         public function validateFilename( $filename ) {
01477                 if ( strval( $filename ) == '' ) {
01478                         return false;
01479                 }
01480                 return FileBackend::isPathTraversalFree( $filename );
01481         }
01482 
01488         function getErrorCleanupFunction() {
01489                 switch ( $this->pathDisclosureProtection ) {
01490                         case 'none':
01491                         case 'simple': // b/c
01492                                 $callback = array( $this, 'passThrough' );
01493                                 break;
01494                         default: // 'paranoid'
01495                                 $callback = array( $this, 'paranoidClean' );
01496                 }
01497                 return $callback;
01498         }
01499 
01506         function paranoidClean( $param ) {
01507                 return '[hidden]';
01508         }
01509 
01516         function passThrough( $param ) {
01517                 return $param;
01518         }
01519 
01525         public function newFatal( $message /*, parameters...*/ ) {
01526                 $params = func_get_args();
01527                 array_unshift( $params, $this );
01528                 return MWInit::callStaticMethod( 'FileRepoStatus', 'newFatal', $params );
01529         }
01530 
01537         public function newGood( $value = null ) {
01538                 return FileRepoStatus::newGood( $this, $value );
01539         }
01540 
01549         public function checkRedirect( Title $title ) {
01550                 return false;
01551         }
01552 
01560         public function invalidateImageRedirect( Title $title ) {}
01561 
01567         public function getDisplayName() {
01568                 // We don't name our own repo, return nothing
01569                 if ( $this->isLocal() ) {
01570                         return null;
01571                 }
01572                 // 'shared-repo-name-wikimediacommons' is used when $wgUseInstantCommons = true
01573                 return wfMessageFallback( 'shared-repo-name-' . $this->name, 'shared-repo' )->text();
01574         }
01575 
01583         public function nameForThumb( $name ) {
01584                 if ( strlen( $name ) > $this->abbrvThreshold ) {
01585                         $ext  = FileBackend::extensionFromPath( $name );
01586                         $name = ( $ext == '' ) ? 'thumbnail' : "thumbnail.$ext";
01587                 }
01588                 return $name;
01589         }
01590 
01596         public function isLocal() {
01597                 return $this->getName() == 'local';
01598         }
01599 
01608         public function getSharedCacheKey( /*...*/ ) {
01609                 return false;
01610         }
01611 
01619         public function getLocalCacheKey( /*...*/ ) {
01620                 $args = func_get_args();
01621                 array_unshift( $args, 'filerepo', $this->getName() );
01622                 return call_user_func_array( 'wfMemcKey', $args );
01623         }
01624 
01633         public function getTempRepo() {
01634                 return new TempFileRepo( array(
01635                         'name'      => "{$this->name}-temp",
01636                         'backend'   => $this->backend,
01637                         'zones'     => array(
01638                                 'public' => array(
01639                                         'container' => $this->zones['temp']['container'],
01640                                         'directory' => $this->zones['temp']['directory']
01641                                 ),
01642                                 'thumb'  => array(
01643                                         'container' => $this->zones['thumb']['container'],
01644                                         'directory' => ( $this->zones['thumb']['directory'] == '' )
01645                                                 ? 'temp'
01646                                                 : $this->zones['thumb']['directory'] . '/temp'
01647                                 )
01648                         ),
01649                         'url'        => $this->getZoneUrl( 'temp' ),
01650                         'thumbUrl'   => $this->getZoneUrl( 'thumb' ) . '/temp',
01651                         'hashLevels' => $this->hashLevels // performance
01652                 ) );
01653         }
01654 
01660         public function getUploadStash() {
01661                 return new UploadStash( $this );
01662         }
01663 
01671         protected function assertWritableRepo() {}
01672 }
01673 
01677 class TempFileRepo extends FileRepo {
01678         public function getTempRepo() {
01679                 throw new MWException( "Cannot get a temp repo from a temp repo." );
01680         }
01681 }