Index: trunk/extensions/WebStore/store.php |
— | — | @@ -0,0 +1,85 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +/** |
| 5 | + * Store a file to a temporary, private location |
| 6 | + * |
| 7 | + * TODO: expiration |
| 8 | + */ |
| 9 | + |
| 10 | +require( dirname( __FILE__ ) . '/WebStoreCommon.php' ); |
| 11 | +$IP = dirname( realpath( __FILE__ ) ) . '/../..'; |
| 12 | +chdir( $IP ); |
| 13 | +require( './includes/WebStart.php' ); |
| 14 | + |
| 15 | +class WebStoreStore extends WebStoreCommon { |
| 16 | + function execute() { |
| 17 | + global $wgRequest; |
| 18 | + if ( !$this->checkAccess() ) { |
| 19 | + $this->error( 403, 'webstore_access' ); |
| 20 | + return false; |
| 21 | + } |
| 22 | + |
| 23 | + if ( !$wgRequest->wasPosted() ) { |
| 24 | + echo $this->dtd(); |
| 25 | + echo <<<EOT |
| 26 | +<html> |
| 27 | +<head><title>store.php Test Interface</title></head> |
| 28 | +<body> |
| 29 | +<form method="post" action="store.php" enctype="multipart/form-data" > |
| 30 | +<p>File: <input type="file" name="file"/></p> |
| 31 | +<p><input type="submit" value="OK"/></p> |
| 32 | +</form></body></html> |
| 33 | +EOT; |
| 34 | + return true; |
| 35 | + } |
| 36 | + |
| 37 | + $srcFile = $wgRequest->getFileTempname( 'file' ); |
| 38 | + if ( !$srcFile ) { |
| 39 | + $this->error( 400, 'webstore_no_file' ); |
| 40 | + return false; |
| 41 | + } |
| 42 | + |
| 43 | + // Use an hourly timestamped directory for easy cleanup |
| 44 | + $now = time(); |
| 45 | + $this->cleanupTemp( $now ); |
| 46 | + |
| 47 | + $timestamp = gmdate( self::$tempDirFormat, $now ); |
| 48 | + if ( !wfMkdirParents( "{$this->tmpDir}/$timestamp" ) ) { |
| 49 | + $this->error( 500, 'webstore_dest_mkdir' ); |
| 50 | + return false; |
| 51 | + } |
| 52 | + |
| 53 | + // Get the extension of the upload, needs to be preserved for type detection |
| 54 | + $name = $wgRequest->getFileName( 'file' ); |
| 55 | + $n = strrpos( $name, '.' ); |
| 56 | + if ( $n ) { |
| 57 | + $extension = '.' . Image::normalizeExtension( substr( $name, $n + 1 ) ); |
| 58 | + } else { |
| 59 | + $extension = ''; |
| 60 | + } |
| 61 | + |
| 62 | + // Pick a random temporary path |
| 63 | + $destRel = $timestamp . '/' . md5( mt_rand() . mt_rand() . mt_rand() ) . $extension; |
| 64 | + if ( !@move_uploaded_file( $srcFile, "{$this->tmpDir}/$destRel" ) ) { |
| 65 | + $this->error( 400, 'webstore_move_uploaded' ); |
| 66 | + return false; |
| 67 | + } |
| 68 | + |
| 69 | + // Succeeded, return temporary location |
| 70 | + header( 'Content-Type: text/xml' ); |
| 71 | + header( 'X-Store-Location: ' . $destRel ); |
| 72 | + echo <<<EOT |
| 73 | +<?xml version="1.0"?> |
| 74 | +<response> |
| 75 | +<location>$destRel</location> |
| 76 | +</response> |
| 77 | +EOT; |
| 78 | + return true; |
| 79 | + } |
| 80 | + |
| 81 | +} |
| 82 | + |
| 83 | +$s = new WebStoreStore; |
| 84 | +$s->execute(); |
| 85 | + |
| 86 | +?> |
Property changes on: trunk/extensions/WebStore/store.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 87 | + native |
Index: trunk/extensions/WebStore/metadata.php |
— | — | @@ -0,0 +1,89 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +/* |
| 5 | + * Retrieve metadata for a file |
| 6 | + */ |
| 7 | + |
| 8 | +require( dirname( __FILE__ ) . '/WebStoreCommon.php' ); |
| 9 | +$IP = dirname( realpath( __FILE__ ) ) . '/../..'; |
| 10 | +chdir( $IP ); |
| 11 | +require( './includes/WebStart.php' ); |
| 12 | + |
| 13 | +class WebStoreImage extends Image { |
| 14 | + function __construct( $path ) { |
| 15 | + $title = Title::makeTitle( NS_IMAGE, 'Metadata.php dummy image'); |
| 16 | + $this->imagePath = $path; |
| 17 | + parent::__construct( $title ); |
| 18 | + } |
| 19 | + |
| 20 | + function getFullPath() { |
| 21 | + return $this->imagePath; |
| 22 | + } |
| 23 | +} |
| 24 | + |
| 25 | +class WebStoreMetadata extends WebStoreCommon { |
| 26 | + function execute() { |
| 27 | + global $wgRequest; |
| 28 | + if ( !$this->checkAccess() ) { |
| 29 | + $this->error( 403, 'webstore_access' ); |
| 30 | + return false; |
| 31 | + } |
| 32 | + |
| 33 | + if ( !$wgRequest->wasPosted() ) { |
| 34 | + echo $this->dtd(); |
| 35 | +?> |
| 36 | +<html> |
| 37 | +<head><title>metadata.php Test interface</title> |
| 38 | +<body> |
| 39 | +<form method="post" action="metadata.php"> |
| 40 | +<p>Repository: <select name="repository" value="public"> |
| 41 | +<option>public</option> |
| 42 | +<option>temp</option> |
| 43 | +<option>deleted</option> |
| 44 | +</select> |
| 45 | +</p> |
| 46 | +<p>Relative path: <input type="text" name="path"></p> |
| 47 | +<p><input type="submit" value="OK" /></p> |
| 48 | +</form> |
| 49 | +</body></html> |
| 50 | +<?php |
| 51 | + return true; |
| 52 | + } |
| 53 | + |
| 54 | + $repository = $wgRequest->getVal( 'repository' ); |
| 55 | + $root = $this->getRepositoryRoot( $repository ); |
| 56 | + if ( strval( $root ) == '' ) { |
| 57 | + $this->error( 400, 'webstore_invalid_repository' ); |
| 58 | + return false; |
| 59 | + } |
| 60 | + |
| 61 | + $rel = $wgRequest->getVal( 'path' ); |
| 62 | + if ( !$this->validateFilename( $rel ) ) { |
| 63 | + $this->error( 400, 'webstore_path_invalid' ); |
| 64 | + return false; |
| 65 | + } |
| 66 | + |
| 67 | + $fullPath = $root . '/' . $rel; |
| 68 | + |
| 69 | + $image = new WebStoreImage( $root . '/' . $rel ); |
| 70 | + $image->loadFromFile(); |
| 71 | + |
| 72 | + $fields = array( 'width', 'height', 'bits', 'type', 'mime', 'metadata', 'size' ); |
| 73 | + |
| 74 | + header( 'Content-Type: text/xml' ); |
| 75 | + echo "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<response>\n"; |
| 76 | + foreach ( $fields as $field ) { |
| 77 | + $value = $image->$field; |
| 78 | + if ( is_bool( $image->$field ) ) { |
| 79 | + $value = $value ? 1 : 0; |
| 80 | + } |
| 81 | + echo "<$field>" . htmlspecialchars( $value ) . "</$field>\n"; |
| 82 | + } |
| 83 | + echo "</response>\n"; |
| 84 | + } |
| 85 | +} |
| 86 | + |
| 87 | +$obj = new WebStoreMetadata; |
| 88 | +$obj->execute(); |
| 89 | + |
| 90 | +?> |
Property changes on: trunk/extensions/WebStore/metadata.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 91 | + native |
Index: trunk/extensions/WebStore/delete.php |
— | — | @@ -0,0 +1,67 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +require( dirname( __FILE__ ) . '/WebStoreCommon.php' ); |
| 5 | +$IP = dirname( realpath( __FILE__ ) ) . '/../..'; |
| 6 | +chdir( $IP ); |
| 7 | +require( './includes/WebStart.php' ); |
| 8 | + |
| 9 | +class WebStoreDelete extends WebStoreCommon { |
| 10 | + function execute() { |
| 11 | + global $wgRequest; |
| 12 | + if ( !$this->checkAccess() ) { |
| 13 | + $this->error( 403, 'webstore_access' ); |
| 14 | + return false; |
| 15 | + } |
| 16 | + if ( strval( $this->deletedDir ) == '' ) { |
| 17 | + $this->error( 500, 'webstore_no_deleted' ); |
| 18 | + return false; |
| 19 | + } |
| 20 | + |
| 21 | + if ( !$wgRequest->wasPosted() ) { |
| 22 | + echo $this->dtd(); |
| 23 | +?> |
| 24 | +<html> |
| 25 | +<head><title>delete.php Test Interface</title></head> |
| 26 | +<body> |
| 27 | +<form method="post" action="delete.php"> |
| 28 | +<p>Relative path to delete: <input type="text" name="src"/></p> |
| 29 | +<p>Relative archive path: <input type="text" name="dst"/></p> |
| 30 | +<p><input type="submit" value="OK"/></p> |
| 31 | +</form></body></html> |
| 32 | +<?php |
| 33 | + return true; |
| 34 | + } |
| 35 | + |
| 36 | + $srcRel = $wgRequest->getVal( 'src' ); |
| 37 | + $dstRel = $wgRequest->getVal( 'dst' ); |
| 38 | + // Check for directory traversal |
| 39 | + if ( !$this->validateFilename( $srcRel ) || |
| 40 | + !$this->validateFilename( $dstRel ) ) |
| 41 | + { |
| 42 | + $this->error( 400, 'webstore_path_invalid' ); |
| 43 | + return false; |
| 44 | + } |
| 45 | + |
| 46 | + $srcPath = $this->publicDir . '/' . $srcRel; |
| 47 | + $dstPath = $this->deletedDir .'/'. $dstRel; |
| 48 | + |
| 49 | + $error = $this->movePath( $srcPath, $dstPath ); |
| 50 | + if ( $error !== true ) { |
| 51 | + $this->error( 500, $error ); |
| 52 | + return false; |
| 53 | + } |
| 54 | + echo $this->dtd(); |
| 55 | +?> |
| 56 | +<html> |
| 57 | +<head><title>MediaWiki delete OK</title></head> |
| 58 | +<body>File deleted successfully</body> |
| 59 | +</html> |
| 60 | +<?php |
| 61 | + return true; |
| 62 | + } |
| 63 | +} |
| 64 | + |
| 65 | +$d = new WebStoreDelete; |
| 66 | +$d->execute(); |
| 67 | + |
| 68 | +?> |
Property changes on: trunk/extensions/WebStore/delete.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 69 | + native |
Index: trunk/extensions/WebStore/inplace-scaler.php |
— | — | @@ -0,0 +1,116 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +require( dirname( __FILE__ ) . '/WebStoreCommon.php' ); |
| 5 | +$IP = dirname( realpath( __FILE__ ) ) . '/../..'; |
| 6 | +chdir( $IP ); |
| 7 | +define( 'MW_NO_OUTPUT_COMPRESSION', 1 ); |
| 8 | +require( './includes/WebStart.php' ); |
| 9 | + |
| 10 | +class InplaceScaler extends WebStoreCommon { |
| 11 | + function execute() { |
| 12 | + global $wgRequest, $wgContLanguageCode; |
| 13 | + |
| 14 | + if ( !$this->scalerAccessRanges ) { |
| 15 | + $this->error( 403, 'inplace_access_disabled' ); |
| 16 | + return false; |
| 17 | + } |
| 18 | + |
| 19 | + /** |
| 20 | + * Run access checks against REMOTE_ADDR rather than wfGetIP(), since we're not |
| 21 | + * giving access even to trusted proxies, only direct clients. |
| 22 | + */ |
| 23 | + $allowed = false; |
| 24 | + foreach ( $this->scalerAccessRanges as $range ) { |
| 25 | + if ( IP::isInRange( $_SERVER['REMOTE_ADDR'], $range ) ) { |
| 26 | + $allowed = true; |
| 27 | + break; |
| 28 | + } |
| 29 | + } |
| 30 | + |
| 31 | + if ( !$allowed ) { |
| 32 | + $this->error( 403, 'inplace_access_denied' ); |
| 33 | + return false; |
| 34 | + } |
| 35 | + |
| 36 | + if ( !$wgRequest->wasPosted() ) { |
| 37 | + echo $this->dtd(); |
| 38 | +?> |
| 39 | +<html> |
| 40 | +<head><title>inplace-scaler.php Test Interface</title></head> |
| 41 | +<body> |
| 42 | +<form method="post" action="inplace-scaler.php" enctype="multipart/form-data" > |
| 43 | +<p>File: <input type="file" name="data" /></p> |
| 44 | +<p>Width: <input type="text" name="width" /></p> |
| 45 | +<p>Page: <input type="page" name="page" /></p> |
| 46 | +<p><input type="submit" value="OK" /></p> |
| 47 | +</form> |
| 48 | +</body> |
| 49 | +</html> |
| 50 | +<?php |
| 51 | + return true; |
| 52 | + } |
| 53 | + |
| 54 | + $tempDir = $this->tmpDir . '/' . gmdate( self::$tempDirFormat ); |
| 55 | + if ( !is_dir( $tempDir ) ) { |
| 56 | + if ( !wfMkdirParents( $tempDir ) ) { |
| 57 | + $this->error( 500, 'inplace_scaler_no_temp' ); |
| 58 | + return false; |
| 59 | + } |
| 60 | + } |
| 61 | + |
| 62 | + $name = $wgRequest->getFileName( 'data' ); |
| 63 | + $srcTemp = $wgRequest->getFileTempname( 'data' ); |
| 64 | + $page = $wgRequest->getInt( 'page', 1 ); |
| 65 | + $dstWidth = $wgRequest->getInt( 'width', 0 ); |
| 66 | + |
| 67 | + # Check that the parameters are present |
| 68 | + if ( is_null( $name ) || !$dstWidth ) { |
| 69 | + $this->error( 400, 'inplace_scaler_not_enough_params' ); |
| 70 | + return false; |
| 71 | + } |
| 72 | + |
| 73 | + $i = strrpos( $name, '.' ); |
| 74 | + $ext = Image::normalizeExtension( $i ? substr( $name, $i + 1 ) : '' ); |
| 75 | + |
| 76 | + $magic = MimeMagic::singleton(); |
| 77 | + $mime = $magic->guessTypesForExtension( $ext ); |
| 78 | + $deja = false; |
| 79 | + $size = Image::getImageSize( $srcTemp, $mime, $deja ); |
| 80 | + if ( !$size ) { |
| 81 | + $this->error( 400, 'inplace_scaler_invalid_image' ); |
| 82 | + return false; |
| 83 | + } |
| 84 | + |
| 85 | + $dstHeight = Image::scaleHeight( $size[0], $size[1], $dstWidth ); |
| 86 | + |
| 87 | + list( $dstExt, $dstMime ) = Image::getThumbType( $ext, $mime ); |
| 88 | + if ( preg_match( '/[ \\n;=]/', $name ) ) { |
| 89 | + $dstName = "thumb.$ext"; |
| 90 | + } else { |
| 91 | + $dstName = $name; |
| 92 | + } |
| 93 | + if ( $dstExt != $ext ) { |
| 94 | + $dstName = "$dstName.$dstExt"; |
| 95 | + } |
| 96 | + |
| 97 | + $dstTemp = tempnam( $tempDir, 'mwimg' ); |
| 98 | + |
| 99 | + $error = Image::reallyRenderThumb( $srcTemp, $dstTemp, $mime, $dstWidth, $dstHeight, $page ); |
| 100 | + if ( $error !== true ) { |
| 101 | + $this->error( 500, 'inplace_scaler_failed', $error ); |
| 102 | + @unlink( $dstTemp ); |
| 103 | + return false; |
| 104 | + } |
| 105 | + |
| 106 | + header( "Content-Type: $dstMime" ); |
| 107 | + header( "Content-Disposition: inline;filename*=utf-8'$wgContLanguageCode'" . urlencode( $dstName ) ); |
| 108 | + readfile( $dstTemp ); |
| 109 | + unlink( $dstTemp ); |
| 110 | + } |
| 111 | +} |
| 112 | + |
| 113 | + |
| 114 | +$s = new InplaceScaler; |
| 115 | +$s->execute(); |
| 116 | + |
| 117 | +?> |
Property changes on: trunk/extensions/WebStore/inplace-scaler.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 118 | + native |
Index: trunk/extensions/WebStore/WebStoreCommon.php |
— | — | @@ -0,0 +1,250 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +$wgWebStoreSettings = array( |
| 5 | + /** |
| 6 | + * Set this in LocalSettings.php to an array of IP ranges allowed to access |
| 7 | + * the store. Empty by default for maximum security. |
| 8 | + */ |
| 9 | + 'accessRanges' => array(), |
| 10 | + |
| 11 | + /** |
| 12 | + * Access ranges for inplace-scaler.php |
| 13 | + */ |
| 14 | + 'scalerAccessRanges' => array(), |
| 15 | + |
| 16 | + /** |
| 17 | + * Main public directory. If false, uses $wgUploadDirectory |
| 18 | + */ |
| 19 | + 'publicDir' => false, |
| 20 | + |
| 21 | + /** |
| 22 | + * Private temporary directory. If false, uses $wgTmpDirectory |
| 23 | + */ |
| 24 | + 'tmpDir' => false, |
| 25 | + |
| 26 | + /** |
| 27 | + * Private directory for deleted files. If false, uses $wgFileStore['deleted']['directory'] |
| 28 | + */ |
| 29 | + 'deletedDir' => false, |
| 30 | + |
| 31 | + /** |
| 32 | + * Expiration time for temporary files in seconds. Must be at least 7200. |
| 33 | + */ |
| 34 | + 'tempExpiry' => 7200, |
| 35 | + |
| 36 | + /** |
| 37 | + * PHP file to display on 404 errors in 404-handler.php |
| 38 | + */ |
| 39 | + 'fallback404' => false, |
| 40 | +); |
| 41 | + |
| 42 | + |
| 43 | +$wgWebStoreAccess = array(); |
| 44 | + |
| 45 | +class WebStoreCommon { |
| 46 | + static $httpErrors = array( |
| 47 | + 400 => 'Bad Request', |
| 48 | + 403 => 'Access Denied', |
| 49 | + 500 => 'Internal Server Error', |
| 50 | + ); |
| 51 | + |
| 52 | + static $tempDirFormat = 'Y-m-d\TH'; |
| 53 | + |
| 54 | + var $accessRanges = array(), $publicDir = false, $tmpDir = false, |
| 55 | + $deletedDir = false, $tempExpiry = 7200, |
| 56 | + $inplaceScalerAccess = array(); |
| 57 | + |
| 58 | + function __construct() { |
| 59 | + global $wgWebStoreSettings, $wgUploadDirectory, $wgTmpDirectory, $wgFileStore; |
| 60 | + |
| 61 | + foreach ( $wgWebStoreSettings as $name => $value ) { |
| 62 | + $this->$name = $value; |
| 63 | + } |
| 64 | + if ( !$this->tmpDir ) { |
| 65 | + $this->tmpDir = $wgTmpDirectory; |
| 66 | + } |
| 67 | + if ( !$this->publicDir ) { |
| 68 | + $this->publicDir = $wgUploadDirectory; |
| 69 | + } |
| 70 | + if ( !$this->deletedDir ) { |
| 71 | + if ( isset( $wgFileStore['deleted']['directory'] ) ) { |
| 72 | + $this->deletedDir = $wgFileStore['deleted']['directory']; |
| 73 | + } else { |
| 74 | + // No deletion |
| 75 | + $this->deletedDir = false; |
| 76 | + } |
| 77 | + } |
| 78 | + |
| 79 | + self::initialiseMessages(); |
| 80 | + } |
| 81 | + |
| 82 | + function dtd() { |
| 83 | + return <<<EOT |
| 84 | +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> |
| 85 | + |
| 86 | +EOT; |
| 87 | + } |
| 88 | + |
| 89 | + function error( $code, $msgName /*, ... */ ) { |
| 90 | + $params = array_slice( func_get_args(), 1 ); |
| 91 | + $msgText = htmlspecialchars( call_user_func_array( 'wfMsg', $params ) ); |
| 92 | + $encMsgName = htmlspecialchars( $msgName ); |
| 93 | + $info = self::$httpErrors[$code]; |
| 94 | + header( "HTTP/1.1 $code $info" ); |
| 95 | + echo $this->dtd(); |
| 96 | + echo <<<EOT |
| 97 | +<html><head><title>$info</title></head> |
| 98 | +<body><h1>$info</h1><p> |
| 99 | +$encMsgName: $msgText |
| 100 | +</p></body></html> |
| 101 | +EOT; |
| 102 | + } |
| 103 | + |
| 104 | + function validateFilename( $filename ) { |
| 105 | + if ( strval( $filename ) == '' ) { |
| 106 | + return false; |
| 107 | + } |
| 108 | + /** |
| 109 | + * Use the same traversal protection as Title::secureAndSplit() |
| 110 | + */ |
| 111 | + if ( strpos( $filename, '.' ) !== false && |
| 112 | + ( $filename === '.' || $filename === '..' || |
| 113 | + strpos( $filename, './' ) === 0 || |
| 114 | + strpos( $filename, '../' ) === 0 || |
| 115 | + strpos( $filename, '/./' ) !== false || |
| 116 | + strpos( $filename, '/../' ) !== false ) ) |
| 117 | + { |
| 118 | + return false; |
| 119 | + } else { |
| 120 | + return true; |
| 121 | + } |
| 122 | + } |
| 123 | + |
| 124 | + /** |
| 125 | + * Copy from one open file handle to another, until EOF |
| 126 | + */ |
| 127 | + function copyFile( $src, $dest ) { |
| 128 | + while ( !feof( $src ) ) { |
| 129 | + $data = fread( $src, 1048576 ); |
| 130 | + if ( $data === false ) { |
| 131 | + return false; |
| 132 | + } |
| 133 | + if ( fwrite( $dest, $data ) === false ) { |
| 134 | + return false; |
| 135 | + } |
| 136 | + } |
| 137 | + return true; |
| 138 | + } |
| 139 | + |
| 140 | + /** |
| 141 | + * Move a file from one place to another. Fails if the destination file already exists. |
| 142 | + * Requires a filesystem with locking semantics to work concurrently, i.e. not NFS. |
| 143 | + */ |
| 144 | + function movePath( $srcPath, $dstPath, $deleteSource = true ) { |
| 145 | + // Create destination directory |
| 146 | + if ( !wfMkdirParents( dirname( $dstPath ) ) ) return 'webstore_dest_mkdir'; |
| 147 | + |
| 148 | + // Open destination file, lock it |
| 149 | + $dstFile = @fopen( $dstPath, 'x' ); |
| 150 | + if ( !$dstFile ) return 'webstore_dest_open'; |
| 151 | + if ( !flock( $dstFile, LOCK_EX | LOCK_NB ) ) return 'webstore_dest_lock'; |
| 152 | + |
| 153 | + // Open source file |
| 154 | + $srcFile = @fopen( $srcPath, 'r' ); |
| 155 | + if ( !$srcFile ) return 'webstore_src_open'; |
| 156 | + |
| 157 | + // Copy source to dest |
| 158 | + if ( !$this->copyFile( $srcFile, $dstFile ) ) return 'webstore_dest_copy'; |
| 159 | + |
| 160 | + // Unlink the source, close the files |
| 161 | + if ( $deleteSource ) { |
| 162 | + if ( wfIsWindows() ) { |
| 163 | + if ( !fclose( $srcFile ) ) return 'webstore_src_close'; |
| 164 | + unlink( $srcPath ); |
| 165 | + } else { |
| 166 | + unlink( $srcPath ); |
| 167 | + if ( !fclose( $srcFile ) ) return 'webstore_src_close'; |
| 168 | + } |
| 169 | + } else { |
| 170 | + if ( !fclose( $srcFile ) ) return 'webstore_src_close'; |
| 171 | + } |
| 172 | + |
| 173 | + if ( !fclose( $dstFile ) ) return 'webstore_dest_close'; |
| 174 | + |
| 175 | + return true; |
| 176 | + } |
| 177 | + |
| 178 | + |
| 179 | + function checkAccess() { |
| 180 | + foreach ( $this->accessRanges as $range ) { |
| 181 | + if ( IP::isInRange( $_SERVER['REMOTE_ADDR'], $range ) ) { |
| 182 | + return true; |
| 183 | + } |
| 184 | + } |
| 185 | + return false; |
| 186 | + } |
| 187 | + |
| 188 | + static function initialiseMessages() { |
| 189 | + static $done = false; |
| 190 | + if ( $done ) { |
| 191 | + return; |
| 192 | + } |
| 193 | + $done = true; |
| 194 | + |
| 195 | + require( dirname( __FILE__ ) . '/WebStore.i18n.php' ); |
| 196 | + |
| 197 | + global $wgMessageCache; |
| 198 | + foreach ( $messages as $code => $messages2 ) { |
| 199 | + $wgMessageCache->addMessages( $messages2, $code ); |
| 200 | + } |
| 201 | + } |
| 202 | + |
| 203 | + /** |
| 204 | + * Clean up temporary directories |
| 205 | + * @param integer $now The current unix timestamp |
| 206 | + */ |
| 207 | + function cleanupTemp( $now ) { |
| 208 | + $expiry = max( $this->tempExpiry, 7200 ); |
| 209 | + $cleanupDir = $this->tmpDir . '/' . gmdate( self::$tempDirFormat, $now - $expiry ); |
| 210 | + $this->cleanup( $cleanupDir ); |
| 211 | + } |
| 212 | + |
| 213 | + /** |
| 214 | + * Delete a directory if it's not being deleted already |
| 215 | + */ |
| 216 | + function cleanup( $path ) { |
| 217 | + if ( file_exists( $path ) ) { |
| 218 | + $lockFile = fopen( "$path.deleting", 'a+' ); |
| 219 | + if ( @flock( $lockFile, LOCK_EX | LOCK_NB ) ) { |
| 220 | + $dir = @opendir( $path ); |
| 221 | + if ( !$dir ) { |
| 222 | + fclose( $lockFile ); |
| 223 | + return; |
| 224 | + } |
| 225 | + while ( false !== ( $fileName = readdir( $dir ) ) ) { |
| 226 | + unlink( $fileName ); |
| 227 | + } |
| 228 | + closedir( $dir ); |
| 229 | + rmdir( $path ); |
| 230 | + } |
| 231 | + fclose( $lockFile ); |
| 232 | + } |
| 233 | + } |
| 234 | + |
| 235 | + /** |
| 236 | + * Get the root directory for a given repository: public, temp or deleted |
| 237 | + */ |
| 238 | + function getRepositoryRoot( $repository ) { |
| 239 | + switch ( $repository ) { |
| 240 | + case 'public': |
| 241 | + return $this->publicDir; |
| 242 | + case 'temp': |
| 243 | + return $this->tmpDir; |
| 244 | + case 'deleted': |
| 245 | + return $this->deletedDir; |
| 246 | + default: |
| 247 | + return false; |
| 248 | + } |
| 249 | + } |
| 250 | +} |
| 251 | +?> |
Property changes on: trunk/extensions/WebStore/WebStoreCommon.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 252 | + native |
Index: trunk/extensions/WebStore/publish.php |
— | — | @@ -0,0 +1,159 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +/** |
| 5 | + * Move a temporary file to a public directory, and archive the existing file |
| 6 | + * if there was one. |
| 7 | + */ |
| 8 | + |
| 9 | +require( dirname( __FILE__ ) . '/WebStoreCommon.php' ); |
| 10 | +$IP = dirname( realpath( __FILE__ ) ) . '/../..'; |
| 11 | +chdir( $IP ); |
| 12 | +require( './includes/WebStart.php' ); |
| 13 | + |
| 14 | +class WebStorePublish extends WebStoreCommon { |
| 15 | + function execute() { |
| 16 | + global $wgRequest; |
| 17 | + |
| 18 | + if ( !$this->checkAccess() ) { |
| 19 | + $this->error( 403, 'webstore_access' ); |
| 20 | + return false; |
| 21 | + } |
| 22 | + |
| 23 | + if ( !$wgRequest->wasPosted() ) { |
| 24 | + echo $this->dtd(); |
| 25 | +?> |
| 26 | +<html> |
| 27 | +<head><title>publish.php Test Interface</title></head> |
| 28 | +<body> |
| 29 | +<form method="post" action="publish.php"> |
| 30 | +<p>Source repository: <select name="srcRepo" value="public"> |
| 31 | +<option>public</option> |
| 32 | +<option>temp</option> |
| 33 | +<option>deleted</option> |
| 34 | +</select></p> |
| 35 | +<p>Source: <input type="text" name="src"/></p> |
| 36 | +<p>Destination: <input type="text" name="dst"/></p> |
| 37 | +<p>Archive: <input type="text" name="archive"/></p> |
| 38 | +<p><input type="submit" value="OK"/></p> |
| 39 | +</form> |
| 40 | +</body> |
| 41 | +</html> |
| 42 | +<?php |
| 43 | + return true; |
| 44 | + } |
| 45 | + |
| 46 | + $srcRepo = $wgRequest->getVal( 'srcRepo' ); |
| 47 | + if ( !$srcRepo ) { |
| 48 | + $srcRepo = 'temp'; |
| 49 | + } |
| 50 | + // Delete the source file if the source repo is not the public one |
| 51 | + $deleteSource = ( $srcRepo != 'public' ); |
| 52 | + |
| 53 | + $srcRel = $wgRequest->getVal( 'src' ); |
| 54 | + $dstRel = $wgRequest->getVal( 'dst' ); |
| 55 | + $archiveRel = $wgRequest->getVal( 'archive' ); |
| 56 | + |
| 57 | + // Check for directory traversal |
| 58 | + if ( !$this->validateFilename( $srcRel ) || |
| 59 | + !$this->validateFilename( $dstRel ) || |
| 60 | + !$this->validateFilename( $archiveRel ) ) |
| 61 | + { |
| 62 | + $this->error( 400, 'webstore_path_invalid' ); |
| 63 | + return false; |
| 64 | + } |
| 65 | + |
| 66 | + // Don't publish into odd subdirectories of the public repository. |
| 67 | + // Some directories may be temporary caches with a potential for |
| 68 | + // data loss. |
| 69 | + if ( !preg_match( '!^archive|[a-zA-Z0-9]/!', $dstRel ) ) { |
| 70 | + $this->error( 400, 'webstore_path_invalid' ); |
| 71 | + } |
| 72 | + |
| 73 | + $srcRoot = $this->getRepositoryRoot( $srcRepo ); |
| 74 | + if ( strval( $srcRoot ) == '' ) { |
| 75 | + $this->error( 400, 'webstore_invalid_repository' ); |
| 76 | + return false; |
| 77 | + } |
| 78 | + |
| 79 | + $srcPath = $srcRoot . '/' . $srcRel; |
| 80 | + $dstPath = $this->publicDir .'/'. $dstRel; |
| 81 | + $archivePath = $this->publicDir . '/archive/' . $archiveRel; |
| 82 | + |
| 83 | + if ( file_exists( $dstPath ) ) { |
| 84 | + $error = $this->publishAndArchive( $srcPath, $dstPath, $archivePath, $deleteSource ); |
| 85 | + } else { |
| 86 | + $error = $this->movePath( $srcPath, $dstPath, $deleteSource ); |
| 87 | + } |
| 88 | + if ( $error !== true ) { |
| 89 | + $this->error( 500, $error ); |
| 90 | + return false; |
| 91 | + } |
| 92 | + |
| 93 | + echo $this->dtd(); |
| 94 | +?> |
| 95 | +<html> |
| 96 | +<head><title>MediaWiki publish OK</title></head> |
| 97 | +<body>File published successfully</body> |
| 98 | +</html> |
| 99 | +<?php |
| 100 | + return true; |
| 101 | + } |
| 102 | + |
| 103 | + /** |
| 104 | + * Does a three-way move: |
| 105 | + * $dstPath -> $archivePath |
| 106 | + * $srcPath -> $dstPath |
| 107 | + * with a reasonable chance of atomic operation under various adverse conditions. |
| 108 | + */ |
| 109 | + function publishAndArchive( $srcPath, $dstPath, $archivePath, $deleteSource = true ) { |
| 110 | + // Create archive directory |
| 111 | + if ( !wfMkdirParents( dirname( $archivePath ) ) ) return 'webstore_archive_mkdir'; |
| 112 | + |
| 113 | + // Open archive file and lock it, fail if it exists |
| 114 | + $archiveFile = @fopen( $archivePath, 'x' ); |
| 115 | + if ( !$archiveFile ) return 'webstore_archive_open'; |
| 116 | + if ( !flock( $archiveFile, LOCK_EX | LOCK_NB ) ) return 'webstore_archive_lock'; |
| 117 | + |
| 118 | + // Open old destination file, lock it |
| 119 | + $dstFile = @fopen( $dstPath, 'r+' ); |
| 120 | + if ( !$dstFile ) return 'webstore_dest_open'; |
| 121 | + if ( !flock( $dstFile, LOCK_EX | LOCK_NB ) ) return 'webstore_dest_lock'; |
| 122 | + |
| 123 | + // Open source file |
| 124 | + $srcFile = @fopen( $srcPath, 'r' ); |
| 125 | + if ( !$srcFile ) return 'webstore_src_open'; |
| 126 | + |
| 127 | + // Copy dest to archive, close the archive file |
| 128 | + if ( !$this->copyFile( $dstFile, $archiveFile ) ) return 'webstore_archive_copy'; |
| 129 | + if ( !fclose( $archiveFile ) ) return 'webstore_archive_close'; |
| 130 | + |
| 131 | + // Truncate destination |
| 132 | + if ( !ftruncate( $dstFile, 0 ) || 0 !== fseek( $dstFile, 0 ) ) { |
| 133 | + return 'webstore_dest_write'; |
| 134 | + } |
| 135 | + |
| 136 | + // Copy source to dest |
| 137 | + if ( !$this->copyFile( $srcFile, $dstFile ) ) return 'webstore_dest_copy'; |
| 138 | + |
| 139 | + // Unlink the source, close the files |
| 140 | + if ( $deleteSource ) { |
| 141 | + if ( wfIsWindows() ) { |
| 142 | + if ( !fclose( $srcFile ) ) return 'webstore_src_close'; |
| 143 | + unlink( $srcPath ); |
| 144 | + } else { |
| 145 | + unlink( $srcPath ); |
| 146 | + if ( !fclose( $srcFile ) ) return 'webstore_src_close'; |
| 147 | + } |
| 148 | + } else { |
| 149 | + if ( !fclose( $srcFile ) ) return 'webstore_src_close'; |
| 150 | + } |
| 151 | + if ( !fclose( $dstFile ) ) return 'webstore_dest_close'; |
| 152 | + |
| 153 | + return true; |
| 154 | + } |
| 155 | +} |
| 156 | + |
| 157 | +$w = new WebStorePublish; |
| 158 | +$w->execute(); |
| 159 | + |
| 160 | +?> |
Property changes on: trunk/extensions/WebStore/publish.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 161 | + native |
Index: trunk/extensions/WebStore/WebStore.i18n.php |
— | — | @@ -0,0 +1,37 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +$messages = array( |
| 5 | + 'en' => array( |
| 6 | + 'inplace_access_disabled' => 'Access to this service has been disabled for all clients.', |
| 7 | + 'inplace_access_denied' => 'This service is restricted by client IP.', |
| 8 | + 'inplace_scaler_no_temp' => 'No valid temporary directory, set $wgLocalTmpDirectory to a writeable directory.', |
| 9 | + 'inplace_scaler_not_enough_params' => 'Not enough parameters.', |
| 10 | + 'inplace_scaler_invalid_image' => 'Invalid image, could not determine size.', |
| 11 | + 'inplace_scaler_failed' => 'An error was encountered during image scaling: $1', |
| 12 | + |
| 13 | + 'webstore_access' => 'This service is restricted by client IP.', |
| 14 | + 'webstore_path_invalid' => 'The filename was invalid.', |
| 15 | + 'webstore_dest_open' => 'Unable to open destination file.', |
| 16 | + 'webstore_dest_lock' => 'Failed to get lock on destination file.', |
| 17 | + 'webstore_dest_write' => 'Error writing to destination file.', |
| 18 | + 'webstore_dest_copy' => 'Error copying to destination file.', |
| 19 | + 'webstore_dest_close' => 'Error closing destination file.', |
| 20 | + 'webstore_dest_mkdir' => 'Unable to create directory.', |
| 21 | + 'webstore_archive_lock' => 'Failed to get lock on archive file.', |
| 22 | + 'webstore_archive_copy' => 'Error copying to archive.', |
| 23 | + 'webstore_archive_close' => 'Error closing destination file.', |
| 24 | + 'webstore_archive_mkdir' => 'Unable to create directory.', |
| 25 | + 'webstore_archive_open' => 'Unable to open archive file.', |
| 26 | + 'webstore_src_open' => 'Unable to open source file.', |
| 27 | + 'webstore_src_close' => 'Error closing source file.', |
| 28 | + |
| 29 | + 'webstore_no_file' => 'No file was uploaded.', |
| 30 | + 'webstore_move_uploaded' => 'Error moving uploaded file.', |
| 31 | + |
| 32 | + 'webstore_invalid_repository' => 'Invalid repository.', |
| 33 | + |
| 34 | + 'webstore_no_deleted' => 'No archive directory for deleted files is defined.', |
| 35 | + ), |
| 36 | +); |
| 37 | + |
| 38 | +?> |
Property changes on: trunk/extensions/WebStore/WebStore.i18n.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 39 | + native |