MediaWiki  REL1_21
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 = gmdate( "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 MWInit::callStaticMethod( '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 
01724 class TempFileRepo extends FileRepo {
01725         public function getTempRepo() {
01726                 throw new MWException( "Cannot get a temp repo from a temp repo." );
01727         }
01728 }