00001 <?php
00002
00008 class FSRepo extends FileRepo {
00009 var $directory, $deletedDir, $deletedHashLevels, $fileMode;
00010 var $fileFactory = array( 'UnregisteredLocalFile', 'newFromTitle' );
00011 var $oldFileFactory = false;
00012 var $pathDisclosureProtection = 'simple';
00013
00014 function __construct( $info ) {
00015 parent::__construct( $info );
00016
00017
00018 $this->directory = $info['directory'];
00019 $this->url = $info['url'];
00020
00021
00022 $this->hashLevels = isset( $info['hashLevels'] ) ? $info['hashLevels'] : 2;
00023 $this->deletedHashLevels = isset( $info['deletedHashLevels'] ) ?
00024 $info['deletedHashLevels'] : $this->hashLevels;
00025 $this->deletedDir = isset( $info['deletedDir'] ) ? $info['deletedDir'] : false;
00026 $this->fileMode = isset( $info['fileMode'] ) ? $info['fileMode'] : 0644;
00027 if ( isset( $info['thumbDir'] ) ) {
00028 $this->thumbDir = $info['thumbDir'];
00029 } else {
00030 $this->thumbDir = "{$this->directory}/thumb";
00031 }
00032 if ( isset( $info['thumbUrl'] ) ) {
00033 $this->thumbUrl = $info['thumbUrl'];
00034 } else {
00035 $this->thumbUrl = "{$this->url}/thumb";
00036 }
00037 }
00038
00042 function getRootDirectory() {
00043 return $this->directory;
00044 }
00045
00049 function getRootUrl() {
00050 return $this->url;
00051 }
00052
00056 function isHashed() {
00057 return (bool)$this->hashLevels;
00058 }
00059
00063 function getZonePath( $zone ) {
00064 switch ( $zone ) {
00065 case 'public':
00066 return $this->directory;
00067 case 'temp':
00068 return "{$this->directory}/temp";
00069 case 'deleted':
00070 return $this->deletedDir;
00071 case 'thumb':
00072 return $this->thumbDir;
00073 default:
00074 return false;
00075 }
00076 }
00077
00081 function getZoneUrl( $zone ) {
00082 switch ( $zone ) {
00083 case 'public':
00084 return $this->url;
00085 case 'temp':
00086 return "{$this->url}/temp";
00087 case 'deleted':
00088 return parent::getZoneUrl( $zone );
00089 case 'thumb':
00090 return $this->thumbUrl;
00091 default:
00092 return parent::getZoneUrl( $zone );
00093 }
00094 }
00095
00101 function getVirtualUrl( $suffix = false ) {
00102 $path = 'mwrepo://' . $this->name;
00103 if ( $suffix !== false ) {
00104 $path .= '/' . rawurlencode( $suffix );
00105 }
00106 return $path;
00107 }
00108
00112 function resolveVirtualUrl( $url ) {
00113 if ( substr( $url, 0, 9 ) != 'mwrepo://' ) {
00114 throw new MWException( __METHOD__.': unknown protoocl' );
00115 }
00116
00117 $bits = explode( '/', substr( $url, 9 ), 3 );
00118 if ( count( $bits ) != 3 ) {
00119 throw new MWException( __METHOD__.": invalid mwrepo URL: $url" );
00120 }
00121 list( $repo, $zone, $rel ) = $bits;
00122 if ( $repo !== $this->name ) {
00123 throw new MWException( __METHOD__.": fetching from a foreign repo is not supported" );
00124 }
00125 $base = $this->getZonePath( $zone );
00126 if ( !$base ) {
00127 throw new MWException( __METHOD__.": invalid zone: $zone" );
00128 }
00129 return $base . '/' . rawurldecode( $rel );
00130 }
00131
00142 function storeBatch( $triplets, $flags = 0 ) {
00143 if ( !wfMkdirParents( $this->directory ) ) {
00144 return $this->newFatal( 'upload_directory_missing', $this->directory );
00145 }
00146 if ( !is_writable( $this->directory ) ) {
00147 return $this->newFatal( 'upload_directory_read_only', $this->directory );
00148 }
00149 $status = $this->newGood();
00150 foreach ( $triplets as $i => $triplet ) {
00151 list( $srcPath, $dstZone, $dstRel ) = $triplet;
00152
00153 $root = $this->getZonePath( $dstZone );
00154 if ( !$root ) {
00155 throw new MWException( "Invalid zone: $dstZone" );
00156 }
00157 if ( !$this->validateFilename( $dstRel ) ) {
00158 throw new MWException( 'Validation error in $dstRel' );
00159 }
00160 $dstPath = "$root/$dstRel";
00161 $dstDir = dirname( $dstPath );
00162
00163 if ( !is_dir( $dstDir ) ) {
00164 if ( !wfMkdirParents( $dstDir ) ) {
00165 return $this->newFatal( 'directorycreateerror', $dstDir );
00166 }
00167 if ( $dstZone == 'deleted' ) {
00168 $this->initDeletedDir( $dstDir );
00169 }
00170 }
00171
00172 if ( self::isVirtualUrl( $srcPath ) ) {
00173 $srcPath = $triplets[$i][0] = $this->resolveVirtualUrl( $srcPath );
00174 }
00175 if ( !is_file( $srcPath ) ) {
00176
00177 $status->fatal( 'filenotfound', $srcPath );
00178 continue;
00179 }
00180 if ( !( $flags & self::OVERWRITE ) && file_exists( $dstPath ) ) {
00181 if ( $flags & self::OVERWRITE_SAME ) {
00182 $hashSource = sha1_file( $srcPath );
00183 $hashDest = sha1_file( $dstPath );
00184 if ( $hashSource != $hashDest ) {
00185 $status->fatal( 'fileexistserror', $dstPath );
00186 }
00187 } else {
00188 $status->fatal( 'fileexistserror', $dstPath );
00189 }
00190 }
00191 }
00192
00193 $deleteDest = wfIsWindows() && ( $flags & self::OVERWRITE );
00194
00195
00196 if ( !$status->ok ) {
00197 return $status;
00198 }
00199
00200 foreach ( $triplets as $triplet ) {
00201 list( $srcPath, $dstZone, $dstRel ) = $triplet;
00202 $root = $this->getZonePath( $dstZone );
00203 $dstPath = "$root/$dstRel";
00204 $good = true;
00205
00206 if ( $flags & self::DELETE_SOURCE ) {
00207 if ( $deleteDest ) {
00208 unlink( $dstPath );
00209 }
00210 if ( !rename( $srcPath, $dstPath ) ) {
00211 $status->error( 'filerenameerror', $srcPath, $dstPath );
00212 $good = false;
00213 }
00214 } else {
00215 if ( !copy( $srcPath, $dstPath ) ) {
00216 $status->error( 'filecopyerror', $srcPath, $dstPath );
00217 $good = false;
00218 }
00219 }
00220 if ( $good ) {
00221 $this->chmod( $dstPath );
00222 $status->successCount++;
00223 } else {
00224 $status->failCount++;
00225 }
00226 }
00227 return $status;
00228 }
00229
00230 function append( $srcPath, $toAppendPath, $flags = 0 ) {
00231 $status = $this->newGood();
00232
00233
00234 if ( self::isVirtualUrl( $srcPath ) ) {
00235 $srcPath = $this->resolveVirtualUrl( $srcPath );
00236 }
00237
00238 if ( !is_file( $srcPath ) )
00239 $status->fatal( 'filenotfound', $srcPath );
00240
00241 if ( !is_file( $toAppendPath ) )
00242 $status->fatal( 'filenotfound', $toAppendPath );
00243
00244 if ( !$status->isOk() ) return $status;
00245
00246
00247 $chunk = file_get_contents( $toAppendPath );
00248 if( $chunk === false ) {
00249 $status->fatal( 'fileappenderrorread', $toAppendPath );
00250 }
00251
00252 if( $status->isOk() ) {
00253 if ( file_put_contents( $srcPath, $chunk, FILE_APPEND ) ) {
00254 $status->value = $srcPath;
00255 } else {
00256 $status->fatal( 'fileappenderror', $toAppendPath, $srcPath);
00257 }
00258 }
00259
00260 if ( $flags & self::DELETE_SOURCE ) {
00261 unlink( $toAppendPath );
00262 }
00263
00264 return $status;
00265 }
00266
00275 function fileExistsBatch( $files, $flags = 0 ) {
00276 if ( !file_exists( $this->directory ) || !is_readable( $this->directory ) ) {
00277 return false;
00278 }
00279 $result = array();
00280 foreach ( $files as $key => $file ) {
00281 if ( self::isVirtualUrl( $file ) ) {
00282 $file = $this->resolveVirtualUrl( $file );
00283 }
00284 if( $flags & self::FILES_ONLY ) {
00285 $result[$key] = is_file( $file );
00286 } else {
00287 $result[$key] = file_exists( $file );
00288 }
00289 }
00290
00291 return $result;
00292 }
00293
00298 protected function initDeletedDir( $dir ) {
00299
00300 $root = $this->getZonePath( 'deleted' );
00301 if ( !file_exists( "$root/.htaccess" ) ) {
00302 file_put_contents( "$root/.htaccess", "Deny from all\n" );
00303 }
00304
00305 file_put_contents( "$dir/index.html", '' );
00306 }
00307
00315 function storeTemp( $originalName, $srcPath ) {
00316 $date = gmdate( "YmdHis" );
00317 $hashPath = $this->getHashPath( $originalName );
00318 $dstRel = "$hashPath$date!$originalName";
00319 $dstUrlRel = $hashPath . $date . '!' . rawurlencode( $originalName );
00320
00321 $result = $this->store( $srcPath, 'temp', $dstRel );
00322 $result->value = $this->getVirtualUrl( 'temp' ) . '/' . $dstUrlRel;
00323 return $result;
00324 }
00325
00331 function freeTemp( $virtualUrl ) {
00332 $temp = "mwrepo://{$this->name}/temp";
00333 if ( substr( $virtualUrl, 0, strlen( $temp ) ) != $temp ) {
00334 wfDebug( __METHOD__.": Invalid virtual URL\n" );
00335 return false;
00336 }
00337 $path = $this->resolveVirtualUrl( $virtualUrl );
00338 wfSuppressWarnings();
00339 $success = unlink( $path );
00340 wfRestoreWarnings();
00341 return $success;
00342 }
00343
00350 function publishBatch( $triplets, $flags = 0 ) {
00351
00352 if ( !wfMkdirParents( $this->directory ) ) {
00353 return $this->newFatal( 'upload_directory_missing', $this->directory );
00354 }
00355 if ( !is_writable( $this->directory ) ) {
00356 return $this->newFatal( 'upload_directory_read_only', $this->directory );
00357 }
00358 $status = $this->newGood( array() );
00359 foreach ( $triplets as $i => $triplet ) {
00360 list( $srcPath, $dstRel, $archiveRel ) = $triplet;
00361
00362 if ( substr( $srcPath, 0, 9 ) == 'mwrepo://' ) {
00363 $triplets[$i][0] = $srcPath = $this->resolveVirtualUrl( $srcPath );
00364 }
00365 if ( !$this->validateFilename( $dstRel ) ) {
00366 throw new MWException( 'Validation error in $dstRel' );
00367 }
00368 if ( !$this->validateFilename( $archiveRel ) ) {
00369 throw new MWException( 'Validation error in $archiveRel' );
00370 }
00371 $dstPath = "{$this->directory}/$dstRel";
00372 $archivePath = "{$this->directory}/$archiveRel";
00373
00374 $dstDir = dirname( $dstPath );
00375 $archiveDir = dirname( $archivePath );
00376
00377 if ( !is_dir( $dstDir ) && !wfMkdirParents( $dstDir ) ) {
00378 return $this->newFatal( 'directorycreateerror', $dstDir );
00379 }
00380 if ( !is_dir( $archiveDir ) && !wfMkdirParents( $archiveDir ) ) {
00381 return $this->newFatal( 'directorycreateerror', $archiveDir );
00382 }
00383 if ( !is_file( $srcPath ) ) {
00384
00385 $status->fatal( 'filenotfound', $srcPath );
00386 }
00387 }
00388
00389 if ( !$status->ok ) {
00390 return $status;
00391 }
00392
00393 foreach ( $triplets as $i => $triplet ) {
00394 list( $srcPath, $dstRel, $archiveRel ) = $triplet;
00395 $dstPath = "{$this->directory}/$dstRel";
00396 $archivePath = "{$this->directory}/$archiveRel";
00397
00398
00399 if( is_file( $dstPath ) ) {
00400
00401
00402
00403
00404
00405 if ( is_file( $archivePath ) ) {
00406 $success = false;
00407 } else {
00408 wfSuppressWarnings();
00409 $success = rename( $dstPath, $archivePath );
00410 wfRestoreWarnings();
00411 }
00412
00413 if( !$success ) {
00414 $status->error( 'filerenameerror',$dstPath, $archivePath );
00415 $status->failCount++;
00416 continue;
00417 } else {
00418 wfDebug(__METHOD__.": moved file $dstPath to $archivePath\n");
00419 }
00420 $status->value[$i] = 'archived';
00421 } else {
00422 $status->value[$i] = 'new';
00423 }
00424
00425 $good = true;
00426 wfSuppressWarnings();
00427 if ( $flags & self::DELETE_SOURCE ) {
00428 if ( !rename( $srcPath, $dstPath ) ) {
00429 $status->error( 'filerenameerror', $srcPath, $dstPath );
00430 $good = false;
00431 }
00432 } else {
00433 if ( !copy( $srcPath, $dstPath ) ) {
00434 $status->error( 'filecopyerror', $srcPath, $dstPath );
00435 $good = false;
00436 }
00437 }
00438 wfRestoreWarnings();
00439
00440 if ( $good ) {
00441 $status->successCount++;
00442 wfDebug(__METHOD__.": wrote tempfile $srcPath to $dstPath\n");
00443
00444 $this->chmod( $dstPath );
00445 } else {
00446 $status->failCount++;
00447 }
00448 }
00449 return $status;
00450 }
00451
00463 function deleteBatch( $sourceDestPairs ) {
00464 $status = $this->newGood();
00465 if ( !$this->deletedDir ) {
00466 throw new MWException( __METHOD__.': no valid deletion archive directory' );
00467 }
00468
00472 foreach ( $sourceDestPairs as $pair ) {
00473 list( $srcRel, $archiveRel ) = $pair;
00474 if ( !$this->validateFilename( $srcRel ) ) {
00475 throw new MWException( __METHOD__.':Validation error in $srcRel' );
00476 }
00477 if ( !$this->validateFilename( $archiveRel ) ) {
00478 throw new MWException( __METHOD__.':Validation error in $archiveRel' );
00479 }
00480 $archivePath = "{$this->deletedDir}/$archiveRel";
00481 $archiveDir = dirname( $archivePath );
00482 if ( !is_dir( $archiveDir ) ) {
00483 if ( !wfMkdirParents( $archiveDir ) ) {
00484 $status->fatal( 'directorycreateerror', $archiveDir );
00485 continue;
00486 }
00487 $this->initDeletedDir( $archiveDir );
00488 }
00489
00490
00491 if ( !is_writable( $archiveDir ) ) {
00492 $status->fatal( 'filedelete-archive-read-only', $archiveDir );
00493 }
00494 }
00495 if ( !$status->ok ) {
00496
00497 return $status;
00498 }
00499
00505 foreach ( $sourceDestPairs as $pair ) {
00506 list( $srcRel, $archiveRel ) = $pair;
00507 $srcPath = "{$this->directory}/$srcRel";
00508 $archivePath = "{$this->deletedDir}/$archiveRel";
00509 $good = true;
00510 if ( file_exists( $archivePath ) ) {
00511 # A file with this content hash is already archived
00512 if ( !@unlink( $srcPath ) ) {
00513 $status->error( 'filedeleteerror', $srcPath );
00514 $good = false;
00515 }
00516 } else{
00517 if ( !@rename( $srcPath, $archivePath ) ) {
00518 $status->error( 'filerenameerror', $srcPath, $archivePath );
00519 $good = false;
00520 } else {
00521 $this->chmod( $archivePath );
00522 }
00523 }
00524 if ( $good ) {
00525 $status->successCount++;
00526 } else {
00527 $status->failCount++;
00528 }
00529 }
00530 return $status;
00531 }
00532
00537 function getDeletedHashPath( $key ) {
00538 $path = '';
00539 for ( $i = 0; $i < $this->deletedHashLevels; $i++ ) {
00540 $path .= $key[$i] . '/';
00541 }
00542 return $path;
00543 }
00544
00549 function enumFilesInFS( $callback ) {
00550 $numDirs = 1 << ( $this->hashLevels * 4 );
00551 for ( $flatIndex = 0; $flatIndex < $numDirs; $flatIndex++ ) {
00552 $hexString = sprintf( "%0{$this->hashLevels}x", $flatIndex );
00553 $path = $this->directory;
00554 for ( $hexPos = 0; $hexPos < $this->hashLevels; $hexPos++ ) {
00555 $path .= '/' . substr( $hexString, 0, $hexPos + 1 );
00556 }
00557 if ( !file_exists( $path ) || !is_dir( $path ) ) {
00558 continue;
00559 }
00560 $dir = opendir( $path );
00561 while ( false !== ( $name = readdir( $dir ) ) ) {
00562 call_user_func( $callback, $path . '/' . $name );
00563 }
00564 }
00565 }
00566
00571 function enumFiles( $callback ) {
00572 $this->enumFilesInFS( $callback );
00573 }
00574
00579 function getFileProps( $virtualUrl ) {
00580 $path = $this->resolveVirtualUrl( $virtualUrl );
00581 return File::getPropsFromPath( $path );
00582 }
00583
00589 function getErrorCleanupFunction() {
00590 switch ( $this->pathDisclosureProtection ) {
00591 case 'simple':
00592 $callback = array( $this, 'simpleClean' );
00593 break;
00594 default:
00595 $callback = parent::getErrorCleanupFunction();
00596 }
00597 return $callback;
00598 }
00599
00600 function simpleClean( $param ) {
00601 if ( !isset( $this->simpleCleanPairs ) ) {
00602 global $IP;
00603 $this->simpleCleanPairs = array(
00604 $this->directory => 'public',
00605 "{$this->directory}/temp" => 'temp',
00606 $IP => '$IP',
00607 dirname( __FILE__ ) => '$IP/extensions/WebStore',
00608 );
00609 if ( $this->deletedDir ) {
00610 $this->simpleCleanPairs[$this->deletedDir] = 'deleted';
00611 }
00612 }
00613 return strtr( $param, $this->simpleCleanPairs );
00614 }
00615
00620 protected function chmod( $path ) {
00621 wfSuppressWarnings();
00622 chmod( $path, $this->fileMode );
00623 wfRestoreWarnings();
00624 }
00625
00626 }