MediaWiki  REL1_19
FileRepo.php
Go to the documentation of this file.
00001 <?php
00022 class FileRepo {
00023         const FILES_ONLY = 1;
00024 
00025         const DELETE_SOURCE = 1;
00026         const OVERWRITE = 2;
00027         const OVERWRITE_SAME = 4;
00028         const SKIP_LOCKING = 8;
00029 
00031         protected $backend;
00033         protected $zones = array();
00034 
00035         var $thumbScriptUrl, $transformVia404;
00036         var $descBaseUrl, $scriptDirUrl, $scriptExtension, $articleUrl;
00037         var $fetchDescription, $initialCapital;
00038         var $pathDisclosureProtection = 'simple'; // 'paranoid'
00039         var $descriptionCacheExpiry, $url, $thumbUrl;
00040         var $hashLevels, $deletedHashLevels;
00041 
00046         var $fileFactory = array( 'UnregisteredLocalFile', 'newFromTitle' );
00047         var $oldFileFactory = false;
00048         var $fileFactoryKey = false, $oldFileFactoryKey = false;
00049 
00050         function __construct( Array $info = null ) {
00051                 // Verify required settings presence
00052                 if(
00053                         $info === null
00054                         || !array_key_exists( 'name', $info )
00055                         || !array_key_exists( 'backend', $info )
00056                 ) {
00057                         throw new MWException( __CLASS__ . " requires an array of options having both 'name' and 'backend' keys.\n" );
00058                 }
00059 
00060                 // Required settings
00061                 $this->name = $info['name'];
00062                 if ( $info['backend'] instanceof FileBackend ) {
00063                         $this->backend = $info['backend']; // useful for testing
00064                 } else {
00065                         $this->backend = FileBackendGroup::singleton()->get( $info['backend'] );
00066                 }
00067 
00068                 // Optional settings that can have no value
00069                 $optionalSettings = array(
00070                         'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription',
00071                         'thumbScriptUrl', 'pathDisclosureProtection', 'descriptionCacheExpiry',
00072                         'scriptExtension'
00073                 );
00074                 foreach ( $optionalSettings as $var ) {
00075                         if ( isset( $info[$var] ) ) {
00076                                 $this->$var = $info[$var];
00077                         }
00078                 }
00079 
00080                 // Optional settings that have a default
00081                 $this->initialCapital = isset( $info['initialCapital'] )
00082                         ? $info['initialCapital']
00083                         : MWNamespace::isCapitalized( NS_FILE );
00084                 $this->url = isset( $info['url'] )
00085                         ? $info['url']
00086                         : false; // a subclass may set the URL (e.g. ForeignAPIRepo)
00087                 if ( isset( $info['thumbUrl'] ) ) {
00088                         $this->thumbUrl = $info['thumbUrl'];
00089                 } else {
00090                         $this->thumbUrl = $this->url ? "{$this->url}/thumb" : false;
00091                 }
00092                 $this->hashLevels = isset( $info['hashLevels'] )
00093                         ? $info['hashLevels']
00094                         : 2;
00095                 $this->deletedHashLevels = isset( $info['deletedHashLevels'] )
00096                         ? $info['deletedHashLevels']
00097                         : $this->hashLevels;
00098                 $this->transformVia404 = !empty( $info['transformVia404'] );
00099                 $this->zones = isset( $info['zones'] )
00100                         ? $info['zones']
00101                         : array();
00102                 // Give defaults for the basic zones...
00103                 foreach ( array( 'public', 'thumb', 'temp', 'deleted' ) as $zone ) {
00104                         if ( !isset( $this->zones[$zone] ) ) {
00105                                 $this->zones[$zone] = array(
00106                                         'container' => "{$this->name}-{$zone}",
00107                                         'directory' => '' // container root
00108                                 );
00109                         }
00110                 }
00111         }
00112 
00118         public function getBackend() {
00119                 return $this->backend;
00120         }
00121 
00129         protected function initZones( $doZones = array() ) {
00130                 $status = $this->newGood();
00131                 foreach ( (array)$doZones as $zone ) {
00132                         $root = $this->getZonePath( $zone );
00133                         if ( $root === null ) {
00134                                 throw new MWException( "No '$zone' zone defined in the {$this->name} repo." );
00135                         }
00136                 }
00137                 return $status;
00138         }
00139 
00147         protected function initDeletedDir( $dir ) {
00148                 $this->backend->secure( // prevent web access & dir listings
00149                         array( 'dir' => $dir, 'noAccess' => true, 'noListing' => true ) );
00150         }
00151 
00158         public static function isVirtualUrl( $url ) {
00159                 return substr( $url, 0, 9 ) == 'mwrepo://';
00160         }
00161 
00170         public function getVirtualUrl( $suffix = false ) {
00171                 $path = 'mwrepo://' . $this->name;
00172                 if ( $suffix !== false ) {
00173                         $path .= '/' . rawurlencode( $suffix );
00174                 }
00175                 return $path;
00176         }
00177 
00184         public function getZoneUrl( $zone ) {
00185                 switch ( $zone ) {
00186                         case 'public':
00187                                 return $this->url;
00188                         case 'temp':
00189                                 return "{$this->url}/temp";
00190                         case 'deleted':
00191                                 return false; // no public URL
00192                         case 'thumb':
00193                                 return $this->thumbUrl;
00194                         default:
00195                                 return false;
00196                 }
00197         }
00198 
00205         function resolveVirtualUrl( $url ) {
00206                 if ( substr( $url, 0, 9 ) != 'mwrepo://' ) {
00207                         throw new MWException( __METHOD__.': unknown protocol' );
00208                 }
00209                 $bits = explode( '/', substr( $url, 9 ), 3 );
00210                 if ( count( $bits ) != 3 ) {
00211                         throw new MWException( __METHOD__.": invalid mwrepo URL: $url" );
00212                 }
00213                 list( $repo, $zone, $rel ) = $bits;
00214                 if ( $repo !== $this->name ) {
00215                         throw new MWException( __METHOD__.": fetching from a foreign repo is not supported" );
00216                 }
00217                 $base = $this->getZonePath( $zone );
00218                 if ( !$base ) {
00219                         throw new MWException( __METHOD__.": invalid zone: $zone" );
00220                 }
00221                 return $base . '/' . rawurldecode( $rel );
00222         }
00223 
00230         protected function getZoneLocation( $zone ) {
00231                 if ( !isset( $this->zones[$zone] ) ) {
00232                         return array( null, null ); // bogus
00233                 }
00234                 return array( $this->zones[$zone]['container'], $this->zones[$zone]['directory'] );
00235         }
00236 
00243         public function getZonePath( $zone ) {
00244                 list( $container, $base ) = $this->getZoneLocation( $zone );
00245                 if ( $container === null || $base === null ) {
00246                         return null;
00247                 }
00248                 $backendName = $this->backend->getName();
00249                 if ( $base != '' ) { // may not be set
00250                         $base = "/{$base}";
00251                 }
00252                 return "mwstore://$backendName/{$container}{$base}";
00253         }
00254 
00266         public function newFile( $title, $time = false ) {
00267                 $title = File::normalizeTitle( $title );
00268                 if ( !$title ) {
00269                         return null;
00270                 }
00271                 if ( $time ) {
00272                         if ( $this->oldFileFactory ) {
00273                                 return call_user_func( $this->oldFileFactory, $title, $this, $time );
00274                         } else {
00275                                 return false;
00276                         }
00277                 } else {
00278                         return call_user_func( $this->fileFactory, $title, $this );
00279                 }
00280         }
00281 
00300         public function findFile( $title, $options = array() ) {
00301                 $title = File::normalizeTitle( $title );
00302                 if ( !$title ) {
00303                         return false;
00304                 }
00305                 $time = isset( $options['time'] ) ? $options['time'] : false;
00306                 # First try the current version of the file to see if it precedes the timestamp
00307                 $img = $this->newFile( $title );
00308                 if ( !$img ) {
00309                         return false;
00310                 }
00311                 if ( $img->exists() && ( !$time || $img->getTimestamp() == $time ) ) {
00312                         return $img;
00313                 }
00314                 # Now try an old version of the file
00315                 if ( $time !== false ) {
00316                         $img = $this->newFile( $title, $time );
00317                         if ( $img && $img->exists() ) {
00318                                 if ( !$img->isDeleted( File::DELETED_FILE ) ) {
00319                                         return $img; // always OK
00320                                 } elseif ( !empty( $options['private'] ) && $img->userCan( File::DELETED_FILE ) ) {
00321                                         return $img;
00322                                 }
00323                         }
00324                 }
00325 
00326                 # Now try redirects
00327                 if ( !empty( $options['ignoreRedirect'] ) ) {
00328                         return false;
00329                 }
00330                 $redir = $this->checkRedirect( $title );
00331                 if ( $redir && $title->getNamespace() == NS_FILE) {
00332                         $img = $this->newFile( $redir );
00333                         if ( !$img ) {
00334                                 return false;
00335                         }
00336                         if ( $img->exists() ) {
00337                                 $img->redirectedFrom( $title->getDBkey() );
00338                                 return $img;
00339                         }
00340                 }
00341                 return false;
00342         }
00343 
00355         public function findFiles( $items ) {
00356                 $result = array();
00357                 foreach ( $items as $item ) {
00358                         if ( is_array( $item ) ) {
00359                                 $title = $item['title'];
00360                                 $options = $item;
00361                                 unset( $options['title'] );
00362                         } else {
00363                                 $title = $item;
00364                                 $options = array();
00365                         }
00366                         $file = $this->findFile( $title, $options );
00367                         if ( $file ) {
00368                                 $result[$file->getTitle()->getDBkey()] = $file;
00369                         }
00370                 }
00371                 return $result;
00372         }
00373 
00383         public function findFileFromKey( $sha1, $options = array() ) {
00384                 $time = isset( $options['time'] ) ? $options['time'] : false;
00385 
00386                 # First try to find a matching current version of a file...
00387                 if ( $this->fileFactoryKey ) {
00388                         $img = call_user_func( $this->fileFactoryKey, $sha1, $this, $time );
00389                 } else {
00390                         return false; // find-by-sha1 not supported
00391                 }
00392                 if ( $img && $img->exists() ) {
00393                         return $img;
00394                 }
00395                 # Now try to find a matching old version of a file...
00396                 if ( $time !== false && $this->oldFileFactoryKey ) { // find-by-sha1 supported?
00397                         $img = call_user_func( $this->oldFileFactoryKey, $sha1, $this, $time );
00398                         if ( $img && $img->exists() ) {
00399                                 if ( !$img->isDeleted( File::DELETED_FILE ) ) {
00400                                         return $img; // always OK
00401                                 } elseif ( !empty( $options['private'] ) && $img->userCan( File::DELETED_FILE ) ) {
00402                                         return $img;
00403                                 }
00404                         }
00405                 }
00406                 return false;
00407         }
00408 
00415         public function findBySha1( $hash ) {
00416                 return array();
00417         }
00418 
00424         public function getRootUrl() {
00425                 return $this->url;
00426         }
00427 
00433         public function isHashed() {
00434                 return (bool)$this->hashLevels;
00435         }
00436 
00442         public function getThumbScriptUrl() {
00443                 return $this->thumbScriptUrl;
00444         }
00445 
00451         public function canTransformVia404() {
00452                 return $this->transformVia404;
00453         }
00454 
00460         public function getNameFromTitle( Title $title ) {
00461                 global $wgContLang;
00462                 if ( $this->initialCapital != MWNamespace::isCapitalized( NS_FILE ) ) {
00463                         $name = $title->getUserCaseDBKey();
00464                         if ( $this->initialCapital ) {
00465                                 $name = $wgContLang->ucfirst( $name );
00466                         }
00467                 } else {
00468                         $name = $title->getDBkey();
00469                 }
00470                 return $name;
00471         }
00472 
00478         public function getRootDirectory() {
00479                 return $this->getZonePath( 'public' );
00480         }
00481 
00489         public function getHashPath( $name ) {
00490                 return self::getHashPathForLevel( $name, $this->hashLevels );
00491         }
00492 
00498         static function getHashPathForLevel( $name, $levels ) {
00499                 if ( $levels == 0 ) {
00500                         return '';
00501                 } else {
00502                         $hash = md5( $name );
00503                         $path = '';
00504                         for ( $i = 1; $i <= $levels; $i++ ) {
00505                                 $path .= substr( $hash, 0, $i ) . '/';
00506                         }
00507                         return $path;
00508                 }
00509         }
00510 
00516         public function getHashLevels() {
00517                 return $this->hashLevels;
00518         }
00519 
00525         public function getName() {
00526                 return $this->name;
00527         }
00528 
00536         public function makeUrl( $query = '', $entry = 'index' ) {
00537                 if ( isset( $this->scriptDirUrl ) ) {
00538                         $ext = isset( $this->scriptExtension ) ? $this->scriptExtension : '.php';
00539                         return wfAppendQuery( "{$this->scriptDirUrl}/{$entry}{$ext}", $query );
00540                 }
00541                 return false;
00542         }
00543 
00556         public function getDescriptionUrl( $name ) {
00557                 $encName = wfUrlencode( $name );
00558                 if ( !is_null( $this->descBaseUrl ) ) {
00559                         # "http://example.com/wiki/Image:"
00560                         return $this->descBaseUrl . $encName;
00561                 }
00562                 if ( !is_null( $this->articleUrl ) ) {
00563                         # "http://example.com/wiki/$1"
00564                         #
00565                         # We use "Image:" as the canonical namespace for
00566                         # compatibility across all MediaWiki versions.
00567                         return str_replace( '$1',
00568                                 "Image:$encName", $this->articleUrl );
00569                 }
00570                 if ( !is_null( $this->scriptDirUrl ) ) {
00571                         # "http://example.com/w"
00572                         #
00573                         # We use "Image:" as the canonical namespace for
00574                         # compatibility across all MediaWiki versions,
00575                         # and just sort of hope index.php is right. ;)
00576                         return $this->makeUrl( "title=Image:$encName" );
00577                 }
00578                 return false;
00579         }
00580 
00591         public function getDescriptionRenderUrl( $name, $lang = null ) {
00592                 $query = 'action=render';
00593                 if ( !is_null( $lang ) ) {
00594                         $query .= '&uselang=' . $lang;
00595                 }
00596                 if ( isset( $this->scriptDirUrl ) ) {
00597                         return $this->makeUrl(
00598                                 'title=' .
00599                                 wfUrlencode( 'Image:' . $name ) .
00600                                 "&$query" );
00601                 } else {
00602                         $descUrl = $this->getDescriptionUrl( $name );
00603                         if ( $descUrl ) {
00604                                 return wfAppendQuery( $descUrl, $query );
00605                         } else {
00606                                 return false;
00607                         }
00608                 }
00609         }
00610 
00616         public function getDescriptionStylesheetUrl() {
00617                 if ( isset( $this->scriptDirUrl ) ) {
00618                         return $this->makeUrl( 'title=MediaWiki:Filepage.css&' .
00619                                 wfArrayToCGI( Skin::getDynamicStylesheetQuery() ) );
00620                 }
00621                 return false;
00622         }
00623 
00638         public function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) {
00639                 $status = $this->storeBatch( array( array( $srcPath, $dstZone, $dstRel ) ), $flags );
00640                 if ( $status->successCount == 0 ) {
00641                         $status->ok = false;
00642                 }
00643                 return $status;
00644         }
00645 
00658         public function storeBatch( $triplets, $flags = 0 ) {
00659                 $backend = $this->backend; // convenience
00660 
00661                 $status = $this->newGood();
00662 
00663                 $operations = array();
00664                 $sourceFSFilesToDelete = array(); // cleanup for disk source files
00665                 // Validate each triplet and get the store operation...
00666                 foreach ( $triplets as $triplet ) {
00667                         list( $srcPath, $dstZone, $dstRel ) = $triplet;
00668                         wfDebug( __METHOD__
00669                                 . "( \$src='$srcPath', \$dstZone='$dstZone', \$dstRel='$dstRel' )\n"
00670                         );
00671 
00672                         // Resolve destination path
00673                         $root = $this->getZonePath( $dstZone );
00674                         if ( !$root ) {
00675                                 throw new MWException( "Invalid zone: $dstZone" );
00676                         }
00677                         if ( !$this->validateFilename( $dstRel ) ) {
00678                                 throw new MWException( 'Validation error in $dstRel' );
00679                         }
00680                         $dstPath = "$root/$dstRel";
00681                         $dstDir  = dirname( $dstPath );
00682                         // Create destination directories for this triplet
00683                         if ( !$backend->prepare( array( 'dir' => $dstDir ) )->isOK() ) {
00684                                 return $this->newFatal( 'directorycreateerror', $dstDir );
00685                         }
00686 
00687                         if ( $dstZone == 'deleted' ) {
00688                                 $this->initDeletedDir( $dstDir );
00689                         }
00690 
00691                         // Resolve source to a storage path if virtual
00692                         if ( self::isVirtualUrl( $srcPath ) ) {
00693                                 $srcPath = $this->resolveVirtualUrl( $srcPath );
00694                         }
00695 
00696                         // Get the appropriate file operation
00697                         if ( FileBackend::isStoragePath( $srcPath ) ) {
00698                                 $opName = ( $flags & self::DELETE_SOURCE ) ? 'move' : 'copy';
00699                         } else {
00700                                 $opName = 'store';
00701                                 if ( $flags & self::DELETE_SOURCE ) {
00702                                         $sourceFSFilesToDelete[] = $srcPath;
00703                                 }
00704                         }
00705                         $operations[] = array(
00706                                 'op'            => $opName,
00707                                 'src'           => $srcPath,
00708                                 'dst'           => $dstPath,
00709                                 'overwrite'     => $flags & self::OVERWRITE,
00710                                 'overwriteSame' => $flags & self::OVERWRITE_SAME,
00711                         );
00712                 }
00713 
00714                 // Execute the store operation for each triplet
00715                 $opts = array( 'force' => true );
00716                 if ( $flags & self::SKIP_LOCKING ) {
00717                         $opts['nonLocking'] = true;
00718                 }
00719                 $status->merge( $backend->doOperations( $operations, $opts ) );
00720                 // Cleanup for disk source files...
00721                 foreach ( $sourceFSFilesToDelete as $file ) {
00722                         wfSuppressWarnings();
00723                         unlink( $file ); // FS cleanup
00724                         wfRestoreWarnings();
00725                 }
00726 
00727                 return $status;
00728         }
00729 
00740         public function cleanupBatch( $files, $flags = 0 ) {
00741                 $operations = array();
00742                 $sourceFSFilesToDelete = array(); // cleanup for disk source files
00743                 foreach ( $files as $file ) {
00744                         if ( is_array( $file ) ) {
00745                                 // This is a pair, extract it
00746                                 list( $zone, $rel ) = $file;
00747                                 $root = $this->getZonePath( $zone );
00748                                 $path = "$root/$rel";
00749                         } else {
00750                                 if ( self::isVirtualUrl( $file ) ) {
00751                                         // This is a virtual url, resolve it
00752                                         $path = $this->resolveVirtualUrl( $file );
00753                                 } else {
00754                                         // This is a full file name
00755                                         $path = $file;
00756                                 }
00757                         }
00758                         // Get a file operation if needed
00759                         if ( FileBackend::isStoragePath( $path ) ) {
00760                                 $operations[] = array(
00761                                         'op'           => 'delete',
00762                                         'src'          => $path,
00763                                 );
00764                         } else {
00765                                 $sourceFSFilesToDelete[] = $path;
00766                         }
00767                 }
00768                 // Actually delete files from storage...
00769                 $opts = array( 'force' => true );
00770                 if ( $flags & self::SKIP_LOCKING ) {
00771                         $opts['nonLocking'] = true;
00772                 }
00773                 $this->backend->doOperations( $operations, $opts );
00774                 // Cleanup for disk source files...
00775                 foreach ( $sourceFSFilesToDelete as $file ) {
00776                         wfSuppressWarnings();
00777                         unlink( $file ); // FS cleanup
00778                         wfRestoreWarnings();
00779                 }
00780         }
00781 
00793         public function storeTemp( $originalName, $srcPath ) {
00794                 $date      = gmdate( "YmdHis" );
00795                 $hashPath  = $this->getHashPath( $originalName );
00796                 $dstRel    = "{$hashPath}{$date}!{$originalName}";
00797                 $dstUrlRel = $hashPath . $date . '!' . rawurlencode( $originalName );
00798 
00799                 $result = $this->store( $srcPath, 'temp', $dstRel, self::SKIP_LOCKING );
00800                 $result->value = $this->getVirtualUrl( 'temp' ) . '/' . $dstUrlRel;
00801                 return $result;
00802         }
00803 
00813         function concatenate( $srcPaths, $dstPath, $flags = 0 ) {
00814                 $status = $this->newGood();
00815 
00816                 $sources = array();
00817                 $deleteOperations = array(); // post-concatenate ops
00818                 foreach ( $srcPaths as $srcPath ) {
00819                         // Resolve source to a storage path if virtual
00820                         $source = $this->resolveToStoragePath( $srcPath );
00821                         $sources[] = $source; // chunk to merge
00822                         if ( $flags & self::DELETE_SOURCE ) {
00823                                 $deleteOperations[] = array( 'op' => 'delete', 'src' => $source );
00824                         }
00825                 }
00826 
00827                 // Concatenate the chunks into one FS file
00828                 $params = array( 'srcs' => $sources, 'dst' => $dstPath );
00829                 $status->merge( $this->backend->concatenate( $params ) );
00830                 if ( !$status->isOK() ) {
00831                         return $status;
00832                 }
00833 
00834                 // Delete the sources if required
00835                 if ( $deleteOperations ) {
00836                         $opts = array( 'force' => true );
00837                         $status->merge( $this->backend->doOperations( $deleteOperations, $opts ) );
00838                 }
00839 
00840                 // Make sure status is OK, despite any $deleteOperations fatals
00841                 $status->setResult( true );
00842 
00843                 return $status;
00844         }
00845 
00852         public function freeTemp( $virtualUrl ) {
00853                 $temp = "mwrepo://{$this->name}/temp";
00854                 if ( substr( $virtualUrl, 0, strlen( $temp ) ) != $temp ) {
00855                         wfDebug( __METHOD__.": Invalid temp virtual URL\n" );
00856                         return false;
00857                 }
00858                 $path   = $this->resolveVirtualUrl( $virtualUrl );
00859                 $op     = array( 'op' => 'delete', 'src' => $path );
00860                 $status = $this->backend->doOperation( $op );
00861                 return $status->isOK();
00862         }
00863 
00878         public function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) {
00879                 $status = $this->publishBatch( array( array( $srcPath, $dstRel, $archiveRel ) ), $flags );
00880                 if ( $status->successCount == 0 ) {
00881                         $status->ok = false;
00882                 }
00883                 if ( isset( $status->value[0] ) ) {
00884                         $status->value = $status->value[0];
00885                 } else {
00886                         $status->value = false;
00887                 }
00888                 return $status;
00889         }
00890 
00899         public function publishBatch( $triplets, $flags = 0 ) {
00900                 $backend = $this->backend; // convenience
00901 
00902                 // Try creating directories
00903                 $status = $this->initZones( 'public' );
00904                 if ( !$status->isOK() ) {
00905                         return $status;
00906                 }
00907 
00908                 $status = $this->newGood( array() );
00909 
00910                 $operations = array();
00911                 $sourceFSFilesToDelete = array(); // cleanup for disk source files
00912                 // Validate each triplet and get the store operation...
00913                 foreach ( $triplets as $i => $triplet ) {
00914                         list( $srcPath, $dstRel, $archiveRel ) = $triplet;
00915                         // Resolve source to a storage path if virtual
00916                         if ( substr( $srcPath, 0, 9 ) == 'mwrepo://' ) {
00917                                 $srcPath = $this->resolveVirtualUrl( $srcPath );
00918                         }
00919                         if ( !$this->validateFilename( $dstRel ) ) {
00920                                 throw new MWException( 'Validation error in $dstRel' );
00921                         }
00922                         if ( !$this->validateFilename( $archiveRel ) ) {
00923                                 throw new MWException( 'Validation error in $archiveRel' );
00924                         }
00925 
00926                         $publicRoot = $this->getZonePath( 'public' );
00927                         $dstPath = "$publicRoot/$dstRel";
00928                         $archivePath = "$publicRoot/$archiveRel";
00929 
00930                         $dstDir = dirname( $dstPath );
00931                         $archiveDir = dirname( $archivePath );
00932                         // Abort immediately on directory creation errors since they're likely to be repetitive
00933                         if ( !$backend->prepare( array( 'dir' => $dstDir ) )->isOK() ) {
00934                                 return $this->newFatal( 'directorycreateerror', $dstDir );
00935                         }
00936                         if ( !$backend->prepare( array( 'dir' => $archiveDir ) )->isOK() ) {
00937                                 return $this->newFatal( 'directorycreateerror', $archiveDir );
00938                         }
00939 
00940                         // Archive destination file if it exists
00941                         if ( $backend->fileExists( array( 'src' => $dstPath ) ) ) {
00942                                 // Check if the archive file exists
00943                                 // This is a sanity check to avoid data loss. In UNIX, the rename primitive
00944                                 // unlinks the destination file if it exists. DB-based synchronisation in
00945                                 // publishBatch's caller should prevent races. In Windows there's no
00946                                 // problem because the rename primitive fails if the destination exists.
00947                                 if ( $backend->fileExists( array( 'src' => $archivePath ) ) ) {
00948                                         $operations[] = array( 'op' => 'null' );
00949                                         continue;
00950                                 } else {
00951                                         $operations[] = array(
00952                                                 'op'           => 'move',
00953                                                 'src'          => $dstPath,
00954                                                 'dst'          => $archivePath
00955                                         );
00956                                 }
00957                                 $status->value[$i] = 'archived';
00958                         } else {
00959                                 $status->value[$i] = 'new';
00960                         }
00961                         // Copy (or move) the source file to the destination
00962                         if ( FileBackend::isStoragePath( $srcPath ) ) {
00963                                 if ( $flags & self::DELETE_SOURCE ) {
00964                                         $operations[] = array(
00965                                                 'op'           => 'move',
00966                                                 'src'          => $srcPath,
00967                                                 'dst'          => $dstPath
00968                                         );
00969                                 } else {
00970                                         $operations[] = array(
00971                                                 'op'           => 'copy',
00972                                                 'src'          => $srcPath,
00973                                                 'dst'          => $dstPath
00974                                         );
00975                                 }
00976                         } else { // FS source path
00977                                 $operations[] = array(
00978                                         'op'           => 'store',
00979                                         'src'          => $srcPath,
00980                                         'dst'          => $dstPath
00981                                 );
00982                                 if ( $flags & self::DELETE_SOURCE ) {
00983                                         $sourceFSFilesToDelete[] = $srcPath;
00984                                 }
00985                         }
00986                 }
00987 
00988                 // Execute the operations for each triplet
00989                 $opts = array( 'force' => true );
00990                 $status->merge( $backend->doOperations( $operations, $opts ) );
00991                 // Cleanup for disk source files...
00992                 foreach ( $sourceFSFilesToDelete as $file ) {
00993                         wfSuppressWarnings();
00994                         unlink( $file ); // FS cleanup
00995                         wfRestoreWarnings();
00996                 }
00997 
00998                 return $status;
00999         }
01000 
01009         public function fileExists( $file, $flags = 0 ) {
01010                 $result = $this->fileExistsBatch( array( $file ), $flags );
01011                 return $result[0];
01012         }
01013 
01022         public function fileExistsBatch( $files, $flags = 0 ) {
01023                 $result = array();
01024                 foreach ( $files as $key => $file ) {
01025                         if ( self::isVirtualUrl( $file ) ) {
01026                                 $file = $this->resolveVirtualUrl( $file );
01027                         }
01028                         if ( FileBackend::isStoragePath( $file ) ) {
01029                                 $result[$key] = $this->backend->fileExists( array( 'src' => $file ) );
01030                         } else {
01031                                 if ( $flags & self::FILES_ONLY ) {
01032                                         $result[$key] = is_file( $file ); // FS only
01033                                 } else {
01034                                         $result[$key] = file_exists( $file ); // FS only
01035                                 }
01036                         }
01037                 }
01038 
01039                 return $result;
01040         }
01041 
01052         public function delete( $srcRel, $archiveRel ) {
01053                 return $this->deleteBatch( array( array( $srcRel, $archiveRel ) ) );
01054         }
01055 
01072         public function deleteBatch( $sourceDestPairs ) {
01073                 $backend = $this->backend; // convenience
01074 
01075                 // Try creating directories
01076                 $status = $this->initZones( array( 'public', 'deleted' ) );
01077                 if ( !$status->isOK() ) {
01078                         return $status;
01079                 }
01080 
01081                 $status = $this->newGood();
01082 
01083                 $operations = array();
01084                 // Validate filenames and create archive directories
01085                 foreach ( $sourceDestPairs as $pair ) {
01086                         list( $srcRel, $archiveRel ) = $pair;
01087                         if ( !$this->validateFilename( $srcRel ) ) {
01088                                 throw new MWException( __METHOD__.':Validation error in $srcRel' );
01089                         }
01090                         if ( !$this->validateFilename( $archiveRel ) ) {
01091                                 throw new MWException( __METHOD__.':Validation error in $archiveRel' );
01092                         }
01093 
01094                         $publicRoot = $this->getZonePath( 'public' );
01095                         $srcPath = "{$publicRoot}/$srcRel";
01096 
01097                         $deletedRoot = $this->getZonePath( 'deleted' );
01098                         $archivePath = "{$deletedRoot}/{$archiveRel}";
01099                         $archiveDir = dirname( $archivePath ); // does not touch FS
01100 
01101                         // Create destination directories
01102                         if ( !$backend->prepare( array( 'dir' => $archiveDir ) )->isOK() ) {
01103                                 return $this->newFatal( 'directorycreateerror', $archiveDir );
01104                         }
01105                         $this->initDeletedDir( $archiveDir );
01106 
01107                         $operations[] = array(
01108                                 'op'            => 'move',
01109                                 'src'           => $srcPath,
01110                                 'dst'           => $archivePath,
01111                                 // We may have 2+ identical files being deleted,
01112                                 // all of which will map to the same destination file
01113                                 'overwriteSame' => true // also see bug 31792
01114                         );
01115                 }
01116 
01117                 // Move the files by execute the operations for each pair.
01118                 // We're now committed to returning an OK result, which will
01119                 // lead to the files being moved in the DB also.
01120                 $opts = array( 'force' => true );
01121                 $status->merge( $backend->doOperations( $operations, $opts ) );
01122 
01123                 return $status;
01124         }
01125 
01132         public function getDeletedHashPath( $key ) {
01133                 $path = '';
01134                 for ( $i = 0; $i < $this->deletedHashLevels; $i++ ) {
01135                         $path .= $key[$i] . '/';
01136                 }
01137                 return $path;
01138         }
01139 
01148         protected function resolveToStoragePath( $path ) {
01149                 if ( $this->isVirtualUrl( $path ) ) {
01150                         return $this->resolveVirtualUrl( $path );
01151                 }
01152                 return $path;
01153         }
01154 
01162         public function getLocalCopy( $virtualUrl ) {
01163                 $path = $this->resolveToStoragePath( $virtualUrl );
01164                 return $this->backend->getLocalCopy( array( 'src' => $path ) );
01165         }
01166 
01175         public function getLocalReference( $virtualUrl ) {
01176                 $path = $this->resolveToStoragePath( $virtualUrl );
01177                 return $this->backend->getLocalReference( array( 'src' => $path ) );
01178         }
01179 
01187         public function getFileProps( $virtualUrl ) {
01188                 $path = $this->resolveToStoragePath( $virtualUrl );
01189                 return $this->backend->getFileProps( array( 'src' => $path ) );
01190         }
01191 
01198         public function getFileTimestamp( $virtualUrl ) {
01199                 $path = $this->resolveToStoragePath( $virtualUrl );
01200                 return $this->backend->getFileTimestamp( array( 'src' => $path ) );
01201         }
01202 
01209         public function getFileSha1( $virtualUrl ) {
01210                 $path = $this->resolveToStoragePath( $virtualUrl );
01211                 $tmpFile = $this->backend->getLocalReference( array( 'src' => $path ) );
01212                 if ( !$tmpFile ) {
01213                         return false;
01214                 }
01215                 return $tmpFile->getSha1Base36();
01216         }
01217 
01225         public function streamFile( $virtualUrl, $headers = array() ) {
01226                 $path = $this->resolveToStoragePath( $virtualUrl );
01227                 $params = array( 'src' => $path, 'headers' => $headers );
01228                 return $this->backend->streamFile( $params )->isOK();
01229         }
01230 
01239         public function enumFiles( $callback ) {
01240                 $this->enumFilesInStorage( $callback );
01241         }
01242 
01250         protected function enumFilesInStorage( $callback ) {
01251                 $publicRoot = $this->getZonePath( 'public' );
01252                 $numDirs = 1 << ( $this->hashLevels * 4 );
01253                 // Use a priori assumptions about directory structure
01254                 // to reduce the tree height of the scanning process.
01255                 for ( $flatIndex = 0; $flatIndex < $numDirs; $flatIndex++ ) {
01256                         $hexString = sprintf( "%0{$this->hashLevels}x", $flatIndex );
01257                         $path = $publicRoot;
01258                         for ( $hexPos = 0; $hexPos < $this->hashLevels; $hexPos++ ) {
01259                                 $path .= '/' . substr( $hexString, 0, $hexPos + 1 );
01260                         }
01261                         $iterator = $this->backend->getFileList( array( 'dir' => $path ) );
01262                         foreach ( $iterator as $name ) {
01263                                 // Each item returned is a public file
01264                                 call_user_func( $callback, "{$path}/{$name}" );
01265                         }
01266                 }
01267         }
01268 
01275         public function validateFilename( $filename ) {
01276                 if ( strval( $filename ) == '' ) {
01277                         return false;
01278                 }
01279                 if ( wfIsWindows() ) {
01280                         $filename = strtr( $filename, '\\', '/' );
01281                 }
01285                 if ( strpos( $filename, '.' ) !== false &&
01286                         ( $filename === '.' || $filename === '..' ||
01287                                 strpos( $filename, './' ) === 0  ||
01288                                 strpos( $filename, '../' ) === 0 ||
01289                                 strpos( $filename, '/./' ) !== false ||
01290                                 strpos( $filename, '/../' ) !== false ) )
01291                 {
01292                         return false;
01293                 } else {
01294                         return true;
01295                 }
01296         }
01297 
01303         function getErrorCleanupFunction() {
01304                 switch ( $this->pathDisclosureProtection ) {
01305                         case 'none':
01306                                 $callback = array( $this, 'passThrough' );
01307                                 break;
01308                         case 'simple':
01309                                 $callback = array( $this, 'simpleClean' );
01310                                 break;
01311                         default: // 'paranoid'
01312                                 $callback = array( $this, 'paranoidClean' );
01313                 }
01314                 return $callback;
01315         }
01316 
01323         function paranoidClean( $param ) {
01324                 return '[hidden]';
01325         }
01326 
01333         function simpleClean( $param ) {
01334                 global $IP;
01335                 if ( !isset( $this->simpleCleanPairs ) ) {
01336                         $this->simpleCleanPairs = array(
01337                                 $IP => '$IP', // sanity
01338                         );
01339                 }
01340                 return strtr( $param, $this->simpleCleanPairs );
01341         }
01342 
01349         function passThrough( $param ) {
01350                 return $param;
01351         }
01352 
01358         function newFatal( $message /*, parameters...*/ ) {
01359                 $params = func_get_args();
01360                 array_unshift( $params, $this );
01361                 return MWInit::callStaticMethod( 'FileRepoStatus', 'newFatal', $params );
01362         }
01363 
01369         function newGood( $value = null ) {
01370                 return FileRepoStatus::newGood( $this, $value );
01371         }
01372 
01378         public function cleanupDeletedBatch( $storageKeys ) {}
01379 
01388         public function checkRedirect( Title $title ) {
01389                 return false;
01390         }
01391 
01399         public function invalidateImageRedirect( Title $title ) {}
01400 
01406         public function getDisplayName() {
01407                 // We don't name our own repo, return nothing
01408                 if ( $this->isLocal() ) {
01409                         return null;
01410                 }
01411                 // 'shared-repo-name-wikimediacommons' is used when $wgUseInstantCommons = true
01412                 return wfMessageFallback( 'shared-repo-name-' . $this->name, 'shared-repo' )->text();
01413         }
01414 
01420         public function isLocal() {
01421                 return $this->getName() == 'local';
01422         }
01423 
01431         function getSharedCacheKey( /*...*/ ) {
01432                 return false;
01433         }
01434 
01442         function getLocalCacheKey( /*...*/ ) {
01443                 $args = func_get_args();
01444                 array_unshift( $args, 'filerepo', $this->getName() );
01445                 return call_user_func_array( 'wfMemcKey', $args );
01446         }
01447 
01453         public function getUploadStash() {
01454                 return new UploadStash( $this );
01455         }
01456 }