r20844 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r20843‎ | r20844 | r20845 >
Date:16:03, 30 March 2007
Author:tstarling
Status:old
Tags:
Comment:
Web-only (non-NFS) file storage middleware. Work in progress.
Modified paths:
  • /trunk/extensions/WebStore (added) (history)
  • /trunk/extensions/WebStore/WebStore.i18n.php (added) (history)
  • /trunk/extensions/WebStore/WebStoreCommon.php (added) (history)
  • /trunk/extensions/WebStore/delete.php (added) (history)
  • /trunk/extensions/WebStore/inplace-scaler.php (added) (history)
  • /trunk/extensions/WebStore/metadata.php (added) (history)
  • /trunk/extensions/WebStore/publish.php (added) (history)
  • /trunk/extensions/WebStore/store.php (added) (history)

Diff [purge]

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
187 + 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
191 + 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
169 + 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
1118 + 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
1252 + 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
1161 + 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
139 + native