MediaWiki  REL1_20
FileRepo.php
Go to the documentation of this file.
00001 <?php
00037 class FileRepo {
00038         const DELETE_SOURCE = 1;
00039         const OVERWRITE = 2;
00040         const OVERWRITE_SAME = 4;
00041         const SKIP_LOCKING = 8;
00042 
00044         protected $backend;
00046         protected $zones = array();
00047 
00048         var $thumbScriptUrl, $transformVia404;
00049         var $descBaseUrl, $scriptDirUrl, $scriptExtension, $articleUrl;
00050         var $fetchDescription, $initialCapital;
00051         var $pathDisclosureProtection = 'simple'; // 'paranoid'
00052         var $descriptionCacheExpiry, $url, $thumbUrl;
00053         var $hashLevels, $deletedHashLevels;
00054         protected $abbrvThreshold;
00055 
00060         var $fileFactory = array( 'UnregisteredLocalFile', 'newFromTitle' );
00061         var $oldFileFactory = false;
00062         var $fileFactoryKey = false, $oldFileFactoryKey = false;
00063 
00068         public function __construct( array $info = null ) {
00069                 // Verify required settings presence
00070                 if(
00071                         $info === null
00072                         || !array_key_exists( 'name', $info )
00073                         || !array_key_exists( 'backend', $info )
00074                 ) {
00075                         throw new MWException( __CLASS__ . " requires an array of options having both 'name' and 'backend' keys.\n" );
00076                 }
00077 
00078                 // Required settings
00079                 $this->name = $info['name'];
00080                 if ( $info['backend'] instanceof FileBackend ) {
00081                         $this->backend = $info['backend']; // useful for testing
00082                 } else {
00083                         $this->backend = FileBackendGroup::singleton()->get( $info['backend'] );
00084                 }
00085 
00086                 // Optional settings that can have no value
00087                 $optionalSettings = array(
00088                         'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription',
00089                         'thumbScriptUrl', 'pathDisclosureProtection', 'descriptionCacheExpiry',
00090                         'scriptExtension'
00091                 );
00092                 foreach ( $optionalSettings as $var ) {
00093                         if ( isset( $info[$var] ) ) {
00094                                 $this->$var = $info[$var];
00095                         }
00096                 }
00097 
00098                 // Optional settings that have a default
00099                 $this->initialCapital = isset( $info['initialCapital'] )
00100                         ? $info['initialCapital']
00101                         : MWNamespace::isCapitalized( NS_FILE );
00102                 $this->url = isset( $info['url'] )
00103                         ? $info['url']
00104                         : false; // a subclass may set the URL (e.g. ForeignAPIRepo)
00105                 if ( isset( $info['thumbUrl'] ) ) {
00106                         $this->thumbUrl = $info['thumbUrl'];
00107                 } else {
00108                         $this->thumbUrl = $this->url ? "{$this->url}/thumb" : false;
00109                 }
00110                 $this->hashLevels = isset( $info['hashLevels'] )
00111                         ? $info['hashLevels']
00112                         : 2;
00113                 $this->deletedHashLevels = isset( $info['deletedHashLevels'] )
00114                         ? $info['deletedHashLevels']
00115                         : $this->hashLevels;
00116                 $this->transformVia404 = !empty( $info['transformVia404'] );
00117                 $this->abbrvThreshold = isset( $info['abbrvThreshold'] )
00118                         ? $info['abbrvThreshold']
00119                         : 255;
00120                 $this->isPrivate = !empty( $info['isPrivate'] );
00121                 // Give defaults for the basic zones...
00122                 $this->zones = isset( $info['zones'] ) ? $info['zones'] : array();
00123                 foreach ( array( 'public', 'thumb', 'temp', 'deleted' ) as $zone ) {
00124                         if ( !isset( $this->zones[$zone]['container'] ) ) {
00125                                 $this->zones[$zone]['container'] = "{$this->name}-{$zone}";
00126                         }
00127                         if ( !isset( $this->zones[$zone]['directory'] ) ) {
00128                                 $this->zones[$zone]['directory'] = '';
00129                         }
00130                 }
00131         }
00132 
00138         public function getBackend() {
00139                 return $this->backend;
00140         }
00141 
00148         public function getReadOnlyReason() {
00149                 return $this->backend->getReadOnlyReason();
00150         }
00151 
00159         protected function initZones( $doZones = array() ) {
00160                 $status = $this->newGood();
00161                 foreach ( (array)$doZones as $zone ) {
00162                         $root = $this->getZonePath( $zone );
00163                         if ( $root === null ) {
00164                                 throw new MWException( "No '$zone' zone defined in the {$this->name} repo." );
00165                         }
00166                 }
00167                 return $status;
00168         }
00169 
00176         public static function isVirtualUrl( $url ) {
00177                 return substr( $url, 0, 9 ) == 'mwrepo://';
00178         }
00179 
00188         public function getVirtualUrl( $suffix = false ) {
00189                 $path = 'mwrepo://' . $this->name;
00190                 if ( $suffix !== false ) {
00191                         $path .= '/' . rawurlencode( $suffix );
00192                 }
00193                 return $path;
00194         }
00195 
00202         public function getZoneUrl( $zone ) {
00203                 if ( isset( $this->zones[$zone]['url'] )
00204                         && in_array( $zone, array( 'public', 'temp', 'thumb' ) ) )
00205                 {
00206                         return $this->zones[$zone]['url']; // custom URL
00207                 }
00208                 switch ( $zone ) {
00209                         case 'public':
00210                                 return $this->url;
00211                         case 'temp':
00212                                 return "{$this->url}/temp";
00213                         case 'deleted':
00214                                 return false; // no public URL
00215                         case 'thumb':
00216                                 return $this->thumbUrl;
00217                         default:
00218                                 return false;
00219                 }
00220         }
00221 
00235         public function getZoneHandlerUrl( $zone ) {
00236                 if ( isset( $this->zones[$zone]['handlerUrl'] )
00237                         && in_array( $zone, array( 'public', 'temp', 'thumb' ) ) )
00238                 {
00239                         return $this->zones[$zone]['handlerUrl'];
00240                 }
00241                 return false;
00242         }
00243 
00252         public function resolveVirtualUrl( $url ) {
00253                 if ( substr( $url, 0, 9 ) != 'mwrepo://' ) {
00254                         throw new MWException( __METHOD__.': unknown protocol' );
00255                 }
00256                 $bits = explode( '/', substr( $url, 9 ), 3 );
00257                 if ( count( $bits ) != 3 ) {
00258                         throw new MWException( __METHOD__.": invalid mwrepo URL: $url" );
00259                 }
00260                 list( $repo, $zone, $rel ) = $bits;
00261                 if ( $repo !== $this->name ) {
00262                         throw new MWException( __METHOD__.": fetching from a foreign repo is not supported" );
00263                 }
00264                 $base = $this->getZonePath( $zone );
00265                 if ( !$base ) {
00266                         throw new MWException( __METHOD__.": invalid zone: $zone" );
00267                 }
00268                 return $base . '/' . rawurldecode( $rel );
00269         }
00270 
00277         protected function getZoneLocation( $zone ) {
00278                 if ( !isset( $this->zones[$zone] ) ) {
00279                         return array( null, null ); // bogus
00280                 }
00281                 return array( $this->zones[$zone]['container'], $this->zones[$zone]['directory'] );
00282         }
00283 
00290         public function getZonePath( $zone ) {
00291                 list( $container, $base ) = $this->getZoneLocation( $zone );
00292                 if ( $container === null || $base === null ) {
00293                         return null;
00294                 }
00295                 $backendName = $this->backend->getName();
00296                 if ( $base != '' ) { // may not be set
00297                         $base = "/{$base}";
00298                 }
00299                 return "mwstore://$backendName/{$container}{$base}";
00300         }
00301 
00313         public function newFile( $title, $time = false ) {
00314                 $title = File::normalizeTitle( $title );
00315                 if ( !$title ) {
00316                         return null;
00317                 }
00318                 if ( $time ) {
00319                         if ( $this->oldFileFactory ) {
00320                                 return call_user_func( $this->oldFileFactory, $title, $this, $time );
00321                         } else {
00322                                 return false;
00323                         }
00324                 } else {
00325                         return call_user_func( $this->fileFactory, $title, $this );
00326                 }
00327         }
00328 
00347         public function findFile( $title, $options = array() ) {
00348                 $title = File::normalizeTitle( $title );
00349                 if ( !$title ) {
00350                         return false;
00351                 }
00352                 $time = isset( $options['time'] ) ? $options['time'] : false;
00353                 # First try the current version of the file to see if it precedes the timestamp
00354                 $img = $this->newFile( $title );
00355                 if ( !$img ) {
00356                         return false;
00357                 }
00358                 if ( $img->exists() && ( !$time || $img->getTimestamp() == $time ) ) {
00359                         return $img;
00360                 }
00361                 # Now try an old version of the file
00362                 if ( $time !== false ) {
00363                         $img = $this->newFile( $title, $time );
00364                         if ( $img && $img->exists() ) {
00365                                 if ( !$img->isDeleted( File::DELETED_FILE ) ) {
00366                                         return $img; // always OK
00367                                 } elseif ( !empty( $options['private'] ) && $img->userCan( File::DELETED_FILE ) ) {
00368                                         return $img;
00369                                 }
00370                         }
00371                 }
00372 
00373                 # Now try redirects
00374                 if ( !empty( $options['ignoreRedirect'] ) ) {
00375                         return false;
00376                 }
00377                 $redir = $this->checkRedirect( $title );
00378                 if ( $redir && $title->getNamespace() == NS_FILE) {
00379                         $img = $this->newFile( $redir );
00380                         if ( !$img ) {
00381                                 return false;
00382                         }
00383                         if ( $img->exists() ) {
00384                                 $img->redirectedFrom( $title->getDBkey() );
00385                                 return $img;
00386                         }
00387                 }
00388                 return false;
00389         }
00390 
00402         public function findFiles( array $items ) {
00403                 $result = array();
00404                 foreach ( $items as $item ) {
00405                         if ( is_array( $item ) ) {
00406                                 $title = $item['title'];
00407                                 $options = $item;
00408                                 unset( $options['title'] );
00409                         } else {
00410                                 $title = $item;
00411                                 $options = array();
00412                         }
00413                         $file = $this->findFile( $title, $options );
00414                         if ( $file ) {
00415                                 $result[$file->getTitle()->getDBkey()] = $file;
00416                         }
00417                 }
00418                 return $result;
00419         }
00420 
00430         public function findFileFromKey( $sha1, $options = array() ) {
00431                 $time = isset( $options['time'] ) ? $options['time'] : false;
00432                 # First try to find a matching current version of a file...
00433                 if ( $this->fileFactoryKey ) {
00434                         $img = call_user_func( $this->fileFactoryKey, $sha1, $this, $time );
00435                 } else {
00436                         return false; // find-by-sha1 not supported
00437                 }
00438                 if ( $img && $img->exists() ) {
00439                         return $img;
00440                 }
00441                 # Now try to find a matching old version of a file...
00442                 if ( $time !== false && $this->oldFileFactoryKey ) { // find-by-sha1 supported?
00443                         $img = call_user_func( $this->oldFileFactoryKey, $sha1, $this, $time );
00444                         if ( $img && $img->exists() ) {
00445                                 if ( !$img->isDeleted( File::DELETED_FILE ) ) {
00446                                         return $img; // always OK
00447                                 } elseif ( !empty( $options['private'] ) && $img->userCan( File::DELETED_FILE ) ) {
00448                                         return $img;
00449                                 }
00450                         }
00451                 }
00452                 return false;
00453         }
00454 
00463         public function findBySha1( $hash ) {
00464                 return array();
00465         }
00466 
00474         public function findBySha1s( array $hashes ) {
00475                 $result = array();
00476                 foreach ( $hashes as $hash ) {
00477                         $files = $this->findBySha1( $hash );
00478                         if ( count( $files ) ) {
00479                                 $result[$hash] = $files;
00480                         }
00481                 }
00482                 return $result;
00483         }
00484 
00491         public function getRootUrl() {
00492                 return $this->getZoneUrl( 'public' );
00493         }
00494 
00500         public function getThumbScriptUrl() {
00501                 return $this->thumbScriptUrl;
00502         }
00503 
00509         public function canTransformVia404() {
00510                 return $this->transformVia404;
00511         }
00512 
00519         public function getNameFromTitle( Title $title ) {
00520                 global $wgContLang;
00521                 if ( $this->initialCapital != MWNamespace::isCapitalized( NS_FILE ) ) {
00522                         $name = $title->getUserCaseDBKey();
00523                         if ( $this->initialCapital ) {
00524                                 $name = $wgContLang->ucfirst( $name );
00525                         }
00526                 } else {
00527                         $name = $title->getDBkey();
00528                 }
00529                 return $name;
00530         }
00531 
00537         public function getRootDirectory() {
00538                 return $this->getZonePath( 'public' );
00539         }
00540 
00548         public function getHashPath( $name ) {
00549                 return self::getHashPathForLevel( $name, $this->hashLevels );
00550         }
00551 
00559         public function getTempHashPath( $suffix ) {
00560                 $parts = explode( '!', $suffix, 2 ); // format is <timestamp>!<name> or just <name>
00561                 $name = isset( $parts[1] ) ? $parts[1] : $suffix; // hash path is not based on timestamp
00562                 return self::getHashPathForLevel( $name, $this->hashLevels );
00563         }
00564 
00570         protected static function getHashPathForLevel( $name, $levels ) {
00571                 if ( $levels == 0 ) {
00572                         return '';
00573                 } else {
00574                         $hash = md5( $name );
00575                         $path = '';
00576                         for ( $i = 1; $i <= $levels; $i++ ) {
00577                                 $path .= substr( $hash, 0, $i ) . '/';
00578                         }
00579                         return $path;
00580                 }
00581         }
00582 
00588         public function getHashLevels() {
00589                 return $this->hashLevels;
00590         }
00591 
00597         public function getName() {
00598                 return $this->name;
00599         }
00600 
00608         public function makeUrl( $query = '', $entry = 'index' ) {
00609                 if ( isset( $this->scriptDirUrl ) ) {
00610                         $ext = isset( $this->scriptExtension ) ? $this->scriptExtension : '.php';
00611                         return wfAppendQuery( "{$this->scriptDirUrl}/{$entry}{$ext}", $query );
00612                 }
00613                 return false;
00614         }
00615 
00628         public function getDescriptionUrl( $name ) {
00629                 $encName = wfUrlencode( $name );
00630                 if ( !is_null( $this->descBaseUrl ) ) {
00631                         # "http://example.com/wiki/Image:"
00632                         return $this->descBaseUrl . $encName;
00633                 }
00634                 if ( !is_null( $this->articleUrl ) ) {
00635                         # "http://example.com/wiki/$1"
00636                         #
00637                         # We use "Image:" as the canonical namespace for
00638                         # compatibility across all MediaWiki versions.
00639                         return str_replace( '$1',
00640                                 "Image:$encName", $this->articleUrl );
00641                 }
00642                 if ( !is_null( $this->scriptDirUrl ) ) {
00643                         # "http://example.com/w"
00644                         #
00645                         # We use "Image:" as the canonical namespace for
00646                         # compatibility across all MediaWiki versions,
00647                         # and just sort of hope index.php is right. ;)
00648                         return $this->makeUrl( "title=Image:$encName" );
00649                 }
00650                 return false;
00651         }
00652 
00663         public function getDescriptionRenderUrl( $name, $lang = null ) {
00664                 $query = 'action=render';
00665                 if ( !is_null( $lang ) ) {
00666                         $query .= '&uselang=' . $lang;
00667                 }
00668                 if ( isset( $this->scriptDirUrl ) ) {
00669                         return $this->makeUrl(
00670                                 'title=' .
00671                                 wfUrlencode( 'Image:' . $name ) .
00672                                 "&$query" );
00673                 } else {
00674                         $descUrl = $this->getDescriptionUrl( $name );
00675                         if ( $descUrl ) {
00676                                 return wfAppendQuery( $descUrl, $query );
00677                         } else {
00678                                 return false;
00679                         }
00680                 }
00681         }
00682 
00688         public function getDescriptionStylesheetUrl() {
00689                 if ( isset( $this->scriptDirUrl ) ) {
00690                         return $this->makeUrl( 'title=MediaWiki:Filepage.css&' .
00691                                 wfArrayToCGI( Skin::getDynamicStylesheetQuery() ) );
00692                 }
00693                 return false;
00694         }
00695 
00710         public function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) {
00711                 $this->assertWritableRepo(); // fail out if read-only
00712 
00713                 $status = $this->storeBatch( array( array( $srcPath, $dstZone, $dstRel ) ), $flags );
00714                 if ( $status->successCount == 0 ) {
00715                         $status->ok = false;
00716                 }
00717 
00718                 return $status;
00719         }
00720 
00734         public function storeBatch( array $triplets, $flags = 0 ) {
00735                 $this->assertWritableRepo(); // fail out if read-only
00736 
00737                 $status = $this->newGood();
00738                 $backend = $this->backend; // convenience
00739 
00740                 $operations = array();
00741                 $sourceFSFilesToDelete = array(); // cleanup for disk source files
00742                 // Validate each triplet and get the store operation...
00743                 foreach ( $triplets as $triplet ) {
00744                         list( $srcPath, $dstZone, $dstRel ) = $triplet;
00745                         wfDebug( __METHOD__
00746                                 . "( \$src='$srcPath', \$dstZone='$dstZone', \$dstRel='$dstRel' )\n"
00747                         );
00748 
00749                         // Resolve destination path
00750                         $root = $this->getZonePath( $dstZone );
00751                         if ( !$root ) {
00752                                 throw new MWException( "Invalid zone: $dstZone" );
00753                         }
00754                         if ( !$this->validateFilename( $dstRel ) ) {
00755                                 throw new MWException( 'Validation error in $dstRel' );
00756                         }
00757                         $dstPath = "$root/$dstRel";
00758                         $dstDir  = dirname( $dstPath );
00759                         // Create destination directories for this triplet
00760                         if ( !$this->initDirectory( $dstDir )->isOK() ) {
00761                                 return $this->newFatal( 'directorycreateerror', $dstDir );
00762                         }
00763 
00764                         // Resolve source to a storage path if virtual
00765                         $srcPath = $this->resolveToStoragePath( $srcPath );
00766 
00767                         // Get the appropriate file operation
00768                         if ( FileBackend::isStoragePath( $srcPath ) ) {
00769                                 $opName = ( $flags & self::DELETE_SOURCE ) ? 'move' : 'copy';
00770                         } else {
00771                                 $opName = 'store';
00772                                 if ( $flags & self::DELETE_SOURCE ) {
00773                                         $sourceFSFilesToDelete[] = $srcPath;
00774                                 }
00775                         }
00776                         $operations[] = array(
00777                                 'op'            => $opName,
00778                                 'src'           => $srcPath,
00779                                 'dst'           => $dstPath,
00780                                 'overwrite'     => $flags & self::OVERWRITE,
00781                                 'overwriteSame' => $flags & self::OVERWRITE_SAME,
00782                         );
00783                 }
00784 
00785                 // Execute the store operation for each triplet
00786                 $opts = array( 'force' => true );
00787                 if ( $flags & self::SKIP_LOCKING ) {
00788                         $opts['nonLocking'] = true;
00789                 }
00790                 $status->merge( $backend->doOperations( $operations, $opts ) );
00791                 // Cleanup for disk source files...
00792                 foreach ( $sourceFSFilesToDelete as $file ) {
00793                         wfSuppressWarnings();
00794                         unlink( $file ); // FS cleanup
00795                         wfRestoreWarnings();
00796                 }
00797 
00798                 return $status;
00799         }
00800 
00811         public function cleanupBatch( array $files, $flags = 0 ) {
00812                 $this->assertWritableRepo(); // fail out if read-only
00813 
00814                 $status = $this->newGood();
00815 
00816                 $operations = array();
00817                 foreach ( $files as $path ) {
00818                         if ( is_array( $path ) ) {
00819                                 // This is a pair, extract it
00820                                 list( $zone, $rel ) = $path;
00821                                 $path = $this->getZonePath( $zone ) . "/$rel";
00822                         } else {
00823                                 // Resolve source to a storage path if virtual
00824                                 $path = $this->resolveToStoragePath( $path );
00825                         }
00826                         $operations[] = array( 'op' => 'delete', 'src' => $path );
00827                 }
00828                 // Actually delete files from storage...
00829                 $opts = array( 'force' => true );
00830                 if ( $flags & self::SKIP_LOCKING ) {
00831                         $opts['nonLocking'] = true;
00832                 }
00833                 $status->merge( $this->backend->doOperations( $operations, $opts ) );
00834 
00835                 return $status;
00836         }
00837 
00849         final public function quickImport( $src, $dst, $disposition = null ) {
00850                 return $this->quickImportBatch( array( array( $src, $dst, $disposition ) ) );
00851         }
00852 
00861         final public function quickPurge( $path ) {
00862                 return $this->quickPurgeBatch( array( $path ) );
00863         }
00864 
00872         public function quickCleanDir( $dir ) {
00873                 $status = $this->newGood();
00874                 $status->merge( $this->backend->clean(
00875                         array( 'dir' => $this->resolveToStoragePath( $dir ) ) ) );
00876 
00877                 return $status;
00878         }
00879 
00892         public function quickImportBatch( array $triples ) {
00893                 $status = $this->newGood();
00894                 $operations = array();
00895                 foreach ( $triples as $triple ) {
00896                         list( $src, $dst ) = $triple;
00897                         $src = $this->resolveToStoragePath( $src );
00898                         $dst = $this->resolveToStoragePath( $dst );
00899                         $operations[] = array(
00900                                 'op'          => FileBackend::isStoragePath( $src ) ? 'copy' : 'store',
00901                                 'src'         => $src,
00902                                 'dst'         => $dst,
00903                                 'disposition' => isset( $triple[2] ) ? $triple[2] : null
00904                         );
00905                         $status->merge( $this->initDirectory( dirname( $dst ) ) );
00906                 }
00907                 $status->merge( $this->backend->doQuickOperations( $operations ) );
00908 
00909                 return $status;
00910         }
00911 
00920         public function quickPurgeBatch( array $paths ) {
00921                 $status = $this->newGood();
00922                 $operations = array();
00923                 foreach ( $paths as $path ) {
00924                         $operations[] = array(
00925                                 'op'                  => 'delete',
00926                                 'src'                 => $this->resolveToStoragePath( $path ),
00927                                 'ignoreMissingSource' => true
00928                         );
00929                 }
00930                 $status->merge( $this->backend->doQuickOperations( $operations ) );
00931 
00932                 return $status;
00933         }
00934 
00945         public function storeTemp( $originalName, $srcPath ) {
00946                 $this->assertWritableRepo(); // fail out if read-only
00947 
00948                 $date       = gmdate( "YmdHis" );
00949                 $hashPath   = $this->getHashPath( $originalName );
00950                 $dstRel     = "{$hashPath}{$date}!{$originalName}";
00951                 $dstUrlRel  = $hashPath . $date . '!' . rawurlencode( $originalName );
00952                 $virtualUrl = $this->getVirtualUrl( 'temp' )  . '/' . $dstUrlRel;
00953 
00954                 $result = $this->quickImport( $srcPath, $virtualUrl );
00955                 $result->value = $virtualUrl;
00956 
00957                 return $result;
00958         }
00959 
00966         public function freeTemp( $virtualUrl ) {
00967                 $this->assertWritableRepo(); // fail out if read-only
00968 
00969                 $temp = $this->getVirtualUrl( 'temp' );
00970                 if ( substr( $virtualUrl, 0, strlen( $temp ) ) != $temp ) {
00971                         wfDebug( __METHOD__.": Invalid temp virtual URL\n" );
00972                         return false;
00973                 }
00974 
00975                 return $this->quickPurge( $virtualUrl )->isOK();
00976         }
00977 
00987         public function concatenate( array $srcPaths, $dstPath, $flags = 0 ) {
00988                 $this->assertWritableRepo(); // fail out if read-only
00989 
00990                 $status = $this->newGood();
00991 
00992                 $sources = array();
00993                 foreach ( $srcPaths as $srcPath ) {
00994                         // Resolve source to a storage path if virtual
00995                         $source = $this->resolveToStoragePath( $srcPath );
00996                         $sources[] = $source; // chunk to merge
00997                 }
00998 
00999                 // Concatenate the chunks into one FS file
01000                 $params = array( 'srcs' => $sources, 'dst' => $dstPath );
01001                 $status->merge( $this->backend->concatenate( $params ) );
01002                 if ( !$status->isOK() ) {
01003                         return $status;
01004                 }
01005 
01006                 // Delete the sources if required
01007                 if ( $flags & self::DELETE_SOURCE ) {
01008                         $status->merge( $this->quickPurgeBatch( $srcPaths ) );
01009                 }
01010 
01011                 // Make sure status is OK, despite any quickPurgeBatch() fatals
01012                 $status->setResult( true );
01013 
01014                 return $status;
01015         }
01016 
01032         public function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) {
01033                 $this->assertWritableRepo(); // fail out if read-only
01034 
01035                 $status = $this->publishBatch( array( array( $srcPath, $dstRel, $archiveRel ) ), $flags );
01036                 if ( $status->successCount == 0 ) {
01037                         $status->ok = false;
01038                 }
01039                 if ( isset( $status->value[0] ) ) {
01040                         $status->value = $status->value[0];
01041                 } else {
01042                         $status->value = false;
01043                 }
01044 
01045                 return $status;
01046         }
01047 
01057         public function publishBatch( array $triplets, $flags = 0 ) {
01058                 $this->assertWritableRepo(); // fail out if read-only
01059 
01060                 $backend = $this->backend; // convenience
01061                 // Try creating directories
01062                 $status = $this->initZones( 'public' );
01063                 if ( !$status->isOK() ) {
01064                         return $status;
01065                 }
01066 
01067                 $status = $this->newGood( array() );
01068 
01069                 $operations = array();
01070                 $sourceFSFilesToDelete = array(); // cleanup for disk source files
01071                 // Validate each triplet and get the store operation...
01072                 foreach ( $triplets as $i => $triplet ) {
01073                         list( $srcPath, $dstRel, $archiveRel ) = $triplet;
01074                         // Resolve source to a storage path if virtual
01075                         $srcPath = $this->resolveToStoragePath( $srcPath );
01076                         if ( !$this->validateFilename( $dstRel ) ) {
01077                                 throw new MWException( 'Validation error in $dstRel' );
01078                         }
01079                         if ( !$this->validateFilename( $archiveRel ) ) {
01080                                 throw new MWException( 'Validation error in $archiveRel' );
01081                         }
01082 
01083                         $publicRoot = $this->getZonePath( 'public' );
01084                         $dstPath = "$publicRoot/$dstRel";
01085                         $archivePath = "$publicRoot/$archiveRel";
01086 
01087                         $dstDir = dirname( $dstPath );
01088                         $archiveDir = dirname( $archivePath );
01089                         // Abort immediately on directory creation errors since they're likely to be repetitive
01090                         if ( !$this->initDirectory( $dstDir )->isOK() ) {
01091                                 return $this->newFatal( 'directorycreateerror', $dstDir );
01092                         }
01093                         if ( !$this->initDirectory($archiveDir )->isOK() ) {
01094                                 return $this->newFatal( 'directorycreateerror', $archiveDir );
01095                         }
01096 
01097                         // Archive destination file if it exists
01098                         if ( $backend->fileExists( array( 'src' => $dstPath ) ) ) {
01099                                 // Check if the archive file exists
01100                                 // This is a sanity check to avoid data loss. In UNIX, the rename primitive
01101                                 // unlinks the destination file if it exists. DB-based synchronisation in
01102                                 // publishBatch's caller should prevent races. In Windows there's no
01103                                 // problem because the rename primitive fails if the destination exists.
01104                                 if ( $backend->fileExists( array( 'src' => $archivePath ) ) ) {
01105                                         $operations[] = array( 'op' => 'null' );
01106                                         continue;
01107                                 } else {
01108                                         $operations[] = array(
01109                                                 'op'           => 'move',
01110                                                 'src'          => $dstPath,
01111                                                 'dst'          => $archivePath
01112                                         );
01113                                 }
01114                                 $status->value[$i] = 'archived';
01115                         } else {
01116                                 $status->value[$i] = 'new';
01117                         }
01118                         // Copy (or move) the source file to the destination
01119                         if ( FileBackend::isStoragePath( $srcPath ) ) {
01120                                 if ( $flags & self::DELETE_SOURCE ) {
01121                                         $operations[] = array(
01122                                                 'op'           => 'move',
01123                                                 'src'          => $srcPath,
01124                                                 'dst'          => $dstPath
01125                                         );
01126                                 } else {
01127                                         $operations[] = array(
01128                                                 'op'           => 'copy',
01129                                                 'src'          => $srcPath,
01130                                                 'dst'          => $dstPath
01131                                         );
01132                                 }
01133                         } else { // FS source path
01134                                 $operations[] = array(
01135                                         'op'           => 'store',
01136                                         'src'          => $srcPath,
01137                                         'dst'          => $dstPath
01138                                 );
01139                                 if ( $flags & self::DELETE_SOURCE ) {
01140                                         $sourceFSFilesToDelete[] = $srcPath;
01141                                 }
01142                         }
01143                 }
01144 
01145                 // Execute the operations for each triplet
01146                 $opts = array( 'force' => true );
01147                 $status->merge( $backend->doOperations( $operations, $opts ) );
01148                 // Cleanup for disk source files...
01149                 foreach ( $sourceFSFilesToDelete as $file ) {
01150                         wfSuppressWarnings();
01151                         unlink( $file ); // FS cleanup
01152                         wfRestoreWarnings();
01153                 }
01154 
01155                 return $status;
01156         }
01157 
01165         protected function initDirectory( $dir ) {
01166                 $path = $this->resolveToStoragePath( $dir );
01167                 list( $b, $container, $r ) = FileBackend::splitStoragePath( $path );
01168 
01169                 $params = array( 'dir' => $path );
01170                 if ( $this->isPrivate || $container === $this->zones['deleted']['container'] ) {
01171                         # Take all available measures to prevent web accessibility of new deleted
01172                         # directories, in case the user has not configured offline storage
01173                         $params = array( 'noAccess' => true, 'noListing' => true ) + $params;
01174                 }
01175 
01176                 return $this->backend->prepare( $params );
01177         }
01178 
01185         public function cleanDir( $dir ) {
01186                 $this->assertWritableRepo(); // fail out if read-only
01187 
01188                 $status = $this->newGood();
01189                 $status->merge( $this->backend->clean(
01190                         array( 'dir' => $this->resolveToStoragePath( $dir ) ) ) );
01191 
01192                 return $status;
01193         }
01194 
01201         public function fileExists( $file ) {
01202                 $result = $this->fileExistsBatch( array( $file ) );
01203                 return $result[0];
01204         }
01205 
01212         public function fileExistsBatch( array $files ) {
01213                 $result = array();
01214                 foreach ( $files as $key => $file ) {
01215                         $file = $this->resolveToStoragePath( $file );
01216                         $result[$key] = $this->backend->fileExists( array( 'src' => $file ) );
01217                 }
01218                 return $result;
01219         }
01220 
01231         public function delete( $srcRel, $archiveRel ) {
01232                 $this->assertWritableRepo(); // fail out if read-only
01233 
01234                 return $this->deleteBatch( array( array( $srcRel, $archiveRel ) ) );
01235         }
01236 
01254         public function deleteBatch( array $sourceDestPairs ) {
01255                 $this->assertWritableRepo(); // fail out if read-only
01256 
01257                 // Try creating directories
01258                 $status = $this->initZones( array( 'public', 'deleted' ) );
01259                 if ( !$status->isOK() ) {
01260                         return $status;
01261                 }
01262 
01263                 $status = $this->newGood();
01264 
01265                 $backend = $this->backend; // convenience
01266                 $operations = array();
01267                 // Validate filenames and create archive directories
01268                 foreach ( $sourceDestPairs as $pair ) {
01269                         list( $srcRel, $archiveRel ) = $pair;
01270                         if ( !$this->validateFilename( $srcRel ) ) {
01271                                 throw new MWException( __METHOD__.':Validation error in $srcRel' );
01272                         } elseif ( !$this->validateFilename( $archiveRel ) ) {
01273                                 throw new MWException( __METHOD__.':Validation error in $archiveRel' );
01274                         }
01275 
01276                         $publicRoot = $this->getZonePath( 'public' );
01277                         $srcPath = "{$publicRoot}/$srcRel";
01278 
01279                         $deletedRoot = $this->getZonePath( 'deleted' );
01280                         $archivePath = "{$deletedRoot}/{$archiveRel}";
01281                         $archiveDir = dirname( $archivePath ); // does not touch FS
01282 
01283                         // Create destination directories
01284                         if ( !$this->initDirectory( $archiveDir )->isOK() ) {
01285                                 return $this->newFatal( 'directorycreateerror', $archiveDir );
01286                         }
01287 
01288                         $operations[] = array(
01289                                 'op'            => 'move',
01290                                 'src'           => $srcPath,
01291                                 'dst'           => $archivePath,
01292                                 // We may have 2+ identical files being deleted,
01293                                 // all of which will map to the same destination file
01294                                 'overwriteSame' => true // also see bug 31792
01295                         );
01296                 }
01297 
01298                 // Move the files by execute the operations for each pair.
01299                 // We're now committed to returning an OK result, which will
01300                 // lead to the files being moved in the DB also.
01301                 $opts = array( 'force' => true );
01302                 $status->merge( $backend->doOperations( $operations, $opts ) );
01303 
01304                 return $status;
01305         }
01306 
01312         public function cleanupDeletedBatch( array $storageKeys ) {
01313                 $this->assertWritableRepo();
01314         }
01315 
01323         public function getDeletedHashPath( $key ) {
01324                 $path = '';
01325                 for ( $i = 0; $i < $this->deletedHashLevels; $i++ ) {
01326                         $path .= $key[$i] . '/';
01327                 }
01328                 return $path;
01329         }
01330 
01339         protected function resolveToStoragePath( $path ) {
01340                 if ( $this->isVirtualUrl( $path ) ) {
01341                         return $this->resolveVirtualUrl( $path );
01342                 }
01343                 return $path;
01344         }
01345 
01353         public function getLocalCopy( $virtualUrl ) {
01354                 $path = $this->resolveToStoragePath( $virtualUrl );
01355                 return $this->backend->getLocalCopy( array( 'src' => $path ) );
01356         }
01357 
01366         public function getLocalReference( $virtualUrl ) {
01367                 $path = $this->resolveToStoragePath( $virtualUrl );
01368                 return $this->backend->getLocalReference( array( 'src' => $path ) );
01369         }
01370 
01378         public function getFileProps( $virtualUrl ) {
01379                 $path = $this->resolveToStoragePath( $virtualUrl );
01380                 return $this->backend->getFileProps( array( 'src' => $path ) );
01381         }
01382 
01389         public function getFileTimestamp( $virtualUrl ) {
01390                 $path = $this->resolveToStoragePath( $virtualUrl );
01391                 return $this->backend->getFileTimestamp( array( 'src' => $path ) );
01392         }
01393 
01400         public function getFileSha1( $virtualUrl ) {
01401                 $path = $this->resolveToStoragePath( $virtualUrl );
01402                 return $this->backend->getFileSha1Base36( array( 'src' => $path ) );
01403         }
01404 
01412         public function streamFile( $virtualUrl, $headers = array() ) {
01413                 $path = $this->resolveToStoragePath( $virtualUrl );
01414                 $params = array( 'src' => $path, 'headers' => $headers );
01415                 return $this->backend->streamFile( $params )->isOK();
01416         }
01417 
01426         public function enumFiles( $callback ) {
01427                 $this->enumFilesInStorage( $callback );
01428         }
01429 
01437         protected function enumFilesInStorage( $callback ) {
01438                 $publicRoot = $this->getZonePath( 'public' );
01439                 $numDirs = 1 << ( $this->hashLevels * 4 );
01440                 // Use a priori assumptions about directory structure
01441                 // to reduce the tree height of the scanning process.
01442                 for ( $flatIndex = 0; $flatIndex < $numDirs; $flatIndex++ ) {
01443                         $hexString = sprintf( "%0{$this->hashLevels}x", $flatIndex );
01444                         $path = $publicRoot;
01445                         for ( $hexPos = 0; $hexPos < $this->hashLevels; $hexPos++ ) {
01446                                 $path .= '/' . substr( $hexString, 0, $hexPos + 1 );
01447                         }
01448                         $iterator = $this->backend->getFileList( array( 'dir' => $path ) );
01449                         foreach ( $iterator as $name ) {
01450                                 // Each item returned is a public file
01451                                 call_user_func( $callback, "{$path}/{$name}" );
01452                         }
01453                 }
01454         }
01455 
01462         public function validateFilename( $filename ) {
01463                 if ( strval( $filename ) == '' ) {
01464                         return false;
01465                 }
01466                 return FileBackend::isPathTraversalFree( $filename );
01467         }
01468 
01474         function getErrorCleanupFunction() {
01475                 switch ( $this->pathDisclosureProtection ) {
01476                         case 'none':
01477                         case 'simple': // b/c
01478                                 $callback = array( $this, 'passThrough' );
01479                                 break;
01480                         default: // 'paranoid'
01481                                 $callback = array( $this, 'paranoidClean' );
01482                 }
01483                 return $callback;
01484         }
01485 
01492         function paranoidClean( $param ) {
01493                 return '[hidden]';
01494         }
01495 
01502         function passThrough( $param ) {
01503                 return $param;
01504         }
01505 
01511         public function newFatal( $message /*, parameters...*/ ) {
01512                 $params = func_get_args();
01513                 array_unshift( $params, $this );
01514                 return MWInit::callStaticMethod( 'FileRepoStatus', 'newFatal', $params );
01515         }
01516 
01523         public function newGood( $value = null ) {
01524                 return FileRepoStatus::newGood( $this, $value );
01525         }
01526 
01535         public function checkRedirect( Title $title ) {
01536                 return false;
01537         }
01538 
01546         public function invalidateImageRedirect( Title $title ) {}
01547 
01553         public function getDisplayName() {
01554                 // We don't name our own repo, return nothing
01555                 if ( $this->isLocal() ) {
01556                         return null;
01557                 }
01558                 // 'shared-repo-name-wikimediacommons' is used when $wgUseInstantCommons = true
01559                 return wfMessageFallback( 'shared-repo-name-' . $this->name, 'shared-repo' )->text();
01560         }
01561 
01569         public function nameForThumb( $name ) {
01570                 if ( strlen( $name ) > $this->abbrvThreshold ) {
01571                         $ext  = FileBackend::extensionFromPath( $name );
01572                         $name = ( $ext == '' ) ? 'thumbnail' : "thumbnail.$ext";
01573                 }
01574                 return $name;
01575         }
01576 
01582         public function isLocal() {
01583                 return $this->getName() == 'local';
01584         }
01585 
01594         public function getSharedCacheKey( /*...*/ ) {
01595                 return false;
01596         }
01597 
01605         public function getLocalCacheKey( /*...*/ ) {
01606                 $args = func_get_args();
01607                 array_unshift( $args, 'filerepo', $this->getName() );
01608                 return call_user_func_array( 'wfMemcKey', $args );
01609         }
01610 
01619         public function getTempRepo() {
01620                 return new TempFileRepo( array(
01621                         'name'      => "{$this->name}-temp",
01622                         'backend'   => $this->backend,
01623                         'zones'     => array(
01624                                 'public' => array(
01625                                         'container' => $this->zones['temp']['container'],
01626                                         'directory' => $this->zones['temp']['directory']
01627                                 ),
01628                                 'thumb'  => array(
01629                                         'container' => $this->zones['thumb']['container'],
01630                                         'directory' => ( $this->zones['thumb']['directory'] == '' )
01631                                                 ? 'temp'
01632                                                 : $this->zones['thumb']['directory'] . '/temp'
01633                                 )
01634                         ),
01635                         'url'        => $this->getZoneUrl( 'temp' ),
01636                         'thumbUrl'   => $this->getZoneUrl( 'thumb' ) . '/temp',
01637                         'hashLevels' => $this->hashLevels // performance
01638                 ) );
01639         }
01640 
01646         public function getUploadStash() {
01647                 return new UploadStash( $this );
01648         }
01649 
01657         protected function assertWritableRepo() {}
01658 }
01659 
01663 class TempFileRepo extends FileRepo {
01664         public function getTempRepo() {
01665                 throw new MWException( "Cannot get a temp repo from a temp repo." );
01666         }
01667 }