<?php
/**
- * first destination checks are made (if ignorewarnings is not checked) errors / warning is returned.
+ * @file
+ * @ingroup upload
*
- * we return the uploadUrl
- * we then accept chunk uploads from the client.
- * return chunk id on each POSTED chunk
- * once the client posts done=1 concatenated the files together.
- * more info at: http://firefogg.org/dev/chunk_post.html
+ * First, destination checks are made, and, if ignorewarnings is not
+ * checked, errors / warning is returned.
+ *
+ * 1. We return the uploadUrl.
+ * 2. We then accept chunk uploads from the client.
+ * 3. Return chunk id on each POSTED chunk.
+ * 4. Once the client posts "done=1", the files are concatenated together.
+ *
+ * (More info at: http://firefogg.org/dev/chunk_post.html)
*/
class UploadFromChunks extends UploadBase {
- var $chunk_mode; // init, chunk, done
- var $mSessionKey = false;
- var $status = array();
-
- const INIT = 1;
+ const INIT = 1;
const CHUNK = 2;
- const DONE = 3;
-
- function initializeFromParams( $param, &$request ) {
- $this->initFromSessionKey( $param['chunksessionkey'], $request );
- // set the chunk mode:
- if( !$this->mSessionKey && !$param['done'] ){
- // session key not set init the chunk upload system:
- $this->chunk_mode = UploadFromChunks::INIT;
- $this->mDesiredDestName = $param['filename'];
- } else if( $this->mSessionKey && !$param['done'] ){
- // this is a chunk piece
- $this->chunk_mode = UploadFromChunks::CHUNK;
- } else if( $this->mSessionKey && $param['done'] ){
- // this is the last chunk
- $this->chunk_mode = UploadFromChunks::DONE;
- }
- if( $this->chunk_mode == UploadFromChunks::CHUNK ||
- $this->chunk_mode == UploadFromChunks::DONE ){
- // set chunk related vars:
- $this->mTempPath = $request->getFileTempName( 'chunk' );
- $this->mFileSize = $request->getFileSize( 'chunk' );
- }
-
- return $this->status;
- }
-
- static function isValidRequest( $request ) {
- $sessionData = $request->getSessionData( 'wsUploadData' );
- if( !self::isValidSessionKey(
- $request->getInt( 'wpSessionKey' ),
- $sessionData ) )
- return false;
- // check for the file:
- return (bool)$request->getFileTempName( 'file' );
- }
-
- /* check warnings depending on chunk_mode */
- function checkWarnings(){
- $warning = array();
- return $warning;
- }
-
- function isEmptyFile(){
- // does not apply to chunk init
- if( $this->chunk_mode == UploadFromChunks::INIT ){
- return false;
- } else {
- return parent::isEmptyFile();
- }
- }
-
- /**
- * Verify whether the upload is sane.
- * Returns self::OK or else an array with error information
- */
- function verifyUpload() {
- // no checks on chunk upload mode:
- if( $this->chunk_mode == UploadFromChunks::INIT )
- return self::OK;
-
- // verify on init and last chunk request
- if( $this->chunk_mode == UploadFromChunks::CHUNK ||
- $this->chunk_mode == UploadFromChunks::DONE )
- return parent::verifyUpload();
+ const DONE = 3;
+
+ protected $chunkMode; // INIT, CHUNK, DONE
+ protected $sessionKey;
+ protected $comment;
+ protected $fileSize = 0;
+ protected $repoPath;
+ protected $pageText;
+ protected $watch;
+
+ public $status;
+
+ // Parent class requires this function even though it is only
+ // used from SpecialUpload.php and we don't do chunked uploading
+ // from SpecialUpload -- best to raise an exception for
+ // now.
+ public function initializeFromRequest( &$request ) {
+ throw new MWException( 'not implemented' );
}
- // only run verifyFile on completed uploaded chunks
- function verifyFile( $tmpFile ){
- if( $this->chunk_mode == UploadFromChunks::DONE ){
- // first append last chunk (so we can do a real verifyFile check... (check file type etc)
- $status = $this->doChunkAppend();
- if( $status->isOK() ){
- $this->mTempPath = $this->getRealPath( $this->mTempAppendPath );
- // verify the completed merged chunks as if it was the file that got uploaded:
- return parent::verifyFile( $this->mTempPath );
- } else {
- // conflict of status returns (have to return the error ary) ... why we don't consistantly use a status object is beyond me..
- return $status->getErrorsArray();
- }
- } else {
- return true;
+ public function initialize( $done, $filename, $sessionKey, $path,
+ $fileSize, $sessionData )
+ {
+ $this->initFromSessionKey( $sessionKey, $sessionData );
+
+ if ( !$this->sessionKey && !$done ) {
+ // session key not set, init the chunk upload system:
+ $this->chunkMode = self::INIT;
+ $this->mDesiredDestName = $filename;
+ } else if ( $this->sessionKey && !$done ) {
+ $this->chunkMode = self::CHUNK;
+ } else if ( $this->sessionKey && $done ) {
+ $this->chunkMode = self::DONE;
}
- }
- function getRealPath( $srcPath ){
- $repo = RepoGroup::singleton()->getLocalRepo();
- if ( $repo->isVirtualUrl( $srcPath ) ) {
- return $repo->resolveVirtualUrl( $srcPath );
+ if ( $this->chunkMode == self::CHUNK || $this->chunkMode == self::DONE ) {
+ $this->mTempPath = $path;
+ $this->fileSize += $fileSize;
}
}
- // pretty ugly inter-mixing of mParam and local vars
- function setupChunkSession( $summary, $comment, $watch ) {
- $this->mSessionKey = $this->getSessionKey();
- $_SESSION['wsUploadData'][$this->mSessionKey] = array(
- 'mComment' => $comment,
- 'mSummary' => $summary,
- 'mWatch' => $watch,
- 'mIgnorewarnings' => true, //ignore warning on chunk uploads (for now)
- 'mFilteredName' => $this->mFilteredName,
- 'mTempAppendPath' => null, // the repo append path (not temporary local node mTempPath)
- 'mDesiredDestName' => $this->mDesiredDestName,
- 'version' => self::SESSION_VERSION,
+ /**
+ * Set session information for chunked uploads and allocate a unique key.
+ * @param $comment string
+ * @param $pageText string
+ * @param $watch boolean
+ *
+ * @returns string the session key for this chunked upload
+ */
+ protected function setupChunkSession( $comment, $pageText, $watch ) {
+ $this->sessionKey = $this->getSessionKey();
+ $_SESSION['wsUploadData'][$this->sessionKey] = array(
+ 'comment' => $comment,
+ 'pageText' => $pageText,
+ 'watch' => $watch,
+ 'mFilteredName' => $this->mFilteredName,
+ 'repoPath' => null,
+ 'mDesiredDestName' => $this->mDesiredDestName,
+ 'version' => self::SESSION_VERSION,
);
- return $this->mSessionKey;
+ return $this->sessionKey;
}
- function initFromSessionKey( $sessionKey, $request ){
- if( !$sessionKey || empty( $sessionKey ) ){
- return false;
+ /**
+ * Initialize a continuation of a chunked upload from a session key
+ * @param $sessionKey string
+ * @param $request WebRequest
+ *
+ * @returns void
+ */
+ protected function initFromSessionKey( $sessionKey, $sessionData ) {
+ // testing against null because we don't want to cause obscure
+ // bugs when $sessionKey is full of "0"
+ if ( $sessionKey !== null ) {
+ $this->status = Status::newFromFatal( 'import-token-mismatch' );
+ return;
}
- $this->mSessionKey = $sessionKey;
- // load the sessionData array:
- $sessionData = $request->getSessionData( 'wsUploadData' );
-
- if( isset( $sessionData[$this->mSessionKey]['version'] ) &&
- $sessionData[$this->mSessionKey]['version'] == self::SESSION_VERSION ) {
- // update the local object from the session
- $this->mComment = $sessionData[$this->mSessionKey]['mComment'];
- $this->mSummary = $sessionData[$this->mSessionKey]['mSummary'];
- $this->mWatch = $sessionData[$this->mSessionKey]['mWatch'];
- $this->mIgnorewarnings = $sessionData[$this->mSessionKey]['mIgnorewarnings'];
- $this->mFilteredName = $sessionData[$this->mSessionKey]['mFilteredName'];
- $this->mTempAppendPath = $sessionData[$this->mSessionKey]['mTempAppendPath'];
- $this->mDesiredDestName = $sessionData[$this->mSessionKey]['mDesiredDestName'];
+ $this->sessionKey = $sessionKey;
+
+ if ( isset( $sessionData[$this->sessionKey]['version'] )
+ && $sessionData[$this->sessionKey]['version'] == self::SESSION_VERSION )
+ {
+ $this->comment = $sessionData[$this->sessionKey]['comment'];
+ $this->pageText = $sessionData[$this->sessionKey]['pageText'];
+ $this->watch = $sessionData[$this->sessionKey]['watch'];
+ $this->mFilteredName = $sessionData[$this->sessionKey]['mFilteredName'];
+ $this->repoPath = $sessionData[$this->sessionKey]['repoPath'];
+ $this->mDesiredDestName = $sessionData[$this->sessionKey]['mDesiredDestName'];
} else {
- $this->status = array( 'error' => 'missing session data' );
- return false;
+ $this->status = Status::newFromFatal( 'Missing session data.' );
}
}
- // Lets us return an api result (as flow for chunk uploads is kind of different than others.
- function performUpload( $summary = '', $comment = '', $watch = '', $user ){
- global $wgServer, $wgScriptPath, $wgUser;
+ /**
+ * Handle a chunk of the upload. Overrides the parent method
+ * because Chunked Uploading clients (i.e. Firefogg) require
+ * specific API responses.
+ * @see UploadBase::performUpload
+ */
+ public function performUpload( $comment, $pageText, $watch, $user ) {
+ wfDebug( "\n\n\performUpload(chunked): comment:" . $comment . ' pageText: ' . $pageText . ' watch:' . $watch );
+ global $wgUser, $wgOut;
- if( $this->chunk_mode == UploadFromChunks::INIT ){
+ if ( $this->chunkMode == self::INIT ) {
// firefogg expects a specific result per:
// http://www.firefogg.org/dev/chunk_post.html
// it's okay to return the token here because
// a) the user must have requested the token to get here and
// b) should only happen over POST
- // c) (we need the token to validate chunks are coming from a non-xss request)
+ // c) we need the token to validate chunks are coming from a non-xss request
$token = urlencode( $wgUser->editToken() );
ob_clean();
- echo ApiFormatJson::getJsonEncode( array(
- 'uploadUrl' => "{$wgServer}{$wgScriptPath}/api.php?action=upload&".
- "token={$token}&format=json&enablechunks=true&chunksessionkey=".
- $this->setupChunkSession( $summary, $comment, $watch ) ) );
- exit( 0 );
- } else if( $this->chunk_mode == UploadFromChunks::CHUNK ){
- $status = $this->doChunkAppend();
- if( $status->isOK() ){
- // return success:
- // firefogg expects a specific result per:
- // http://www.firefogg.org/dev/chunk_post.html
- ob_clean();
- echo ApiFormatJson::getJsonEncode( array(
- 'result' => 1,
- 'filesize' => filesize( $this->getRealPath( $this->mTempAppendPath ) )
- )
- );
- exit( 0 );
- /*return array(
- 'result' => 1
- );*/
- } else {
+ echo FormatJson::encode( array(
+ 'uploadUrl' => wfExpandUrl( wfScript( 'api' ) ) . "?action=upload&" .
+ "token={$token}&format=json&enablechunks=true&chunksessionkey=" .
+ $this->setupChunkSession( $comment, $pageText, $watch ) ) );
+ $wgOut->disable();
+ } else if ( $this->chunkMode == self::CHUNK ) {
+ $status = $this->appendChunk();
+ if ( !$status->isOK() ) {
return $status;
}
- } else if( $this->chunk_mode == UploadFromChunks::DONE ){
- // update the values from the local (session init) if not paseed again)
- if( $summary == '' )
- $summary = $this->mSummary;
-
- if( $comment == '' )
- $comment = $this->mComment;
-
- if( $watch == '' )
- $watch = $this->mWatch;
- $status = parent::performUpload( $summary, $comment, $watch, $user );
- if( !$status->isGood() ) {
+ // return success:
+ // firefogg expects a specific result
+ // http://www.firefogg.org/dev/chunk_post.html
+ ob_clean();
+ echo FormatJson::encode(
+ array( 'result' => 1, 'filesize' => $this->fileSize )
+ );
+ $wgOut->disable();
+ } else if ( $this->chunkMode == self::DONE ) {
+ if ( !$comment )
+ $comment = $this->comment;
+
+ if ( !$pageText )
+ $pageText = $this->pageText;
+
+ if ( !$watch )
+ $watch = $this->watch;
+
+ $status = parent::performUpload( $comment, $pageText, $watch, $user );
+ if ( !$status->isGood() ) {
return $status;
}
$file = $this->getLocalFile();
- // firefogg expects a specific result per:
+
+ // firefogg expects a specific result
// http://www.firefogg.org/dev/chunk_post.html
ob_clean();
- echo ApiFormatJson::getJsonEncode( array(
- 'result' => 1,
- 'done' => 1,
- 'resultUrl' => $file->getDescriptionUrl()
- )
+ echo FormatJson::encode( array(
+ 'result' => 1,
+ 'done' => 1,
+ 'resultUrl' => $file->getDescriptionUrl() )
);
- exit( 0 );
-
+ $wgOut->disable();
}
}
- // append the given chunk to the temporary uploaded file. (if no temporary uploaded file exists created it.
- function doChunkAppend(){
- // if we don't have a mTempAppendPath to generate a file from the chunk packaged var:
- if( !$this->mTempAppendPath ){
- // get temp name:
- // make a chunk store path. (append tmp file to chunk)
- $status = $this->saveTempUploadedFile( $this->mDestName, $this->mTempPath );
-
- if( $status->isOK() ) {
- $this->mTempAppendPath = $status->value;
- $_SESSION['wsUploadData'][$this->mSessionKey]['mTempAppendPath'] = $this->mTempAppendPath;
+ /**
+ * Append a chunk to the Repo file
+ *
+ * @param string $srcPath Path to file to append from
+ * @param string $toAppendPath Path to file to append to
+ * @return Status Status
+ */
+ protected function appendToUploadFile( $srcPath, $toAppendPath ) {
+ $repo = RepoGroup::singleton()->getLocalRepo();
+ $status = $repo->append( $srcPath, $toAppendPath );
+ return $status;
+ }
+
+ /**
+ * Append a chunk to the temporary file.
+ *
+ * @return void
+ */
+ protected function appendChunk() {
+ global $wgMaxUploadSize;
+
+ if ( !$this->repoPath ) {
+ $this->status = $this->saveTempUploadedFile( $this->mDesiredDestName, $this->mTempPath );
+
+ if ( $status->isOK() ) {
+ $this->repoPath = $status->value;
+ $_SESSION['wsUploadData'][$this->sessionKey]['repoPath'] = $this->repoPath;
}
return $status;
+ }
+ if ( $this->getRealPath( $this->repoPath ) ) {
+ $this->status = $this->appendToUploadFile( $this->repoPath, $this->mTempPath );
} else {
- if( is_file( $this->getRealPath( $this->mTempAppendPath ) ) ){
- $status = $this->appendToUploadFile( $this->mTempAppendPath, $this->mTempPath );
- } else {
- $status = Status::newFatal( 'filenotfound', $this->mTempAppendPath );
- }
- return $status;
+ $this->status = Status::newFatal( 'filenotfound', $this->repoPath );
+ }
+ if ( $this->fileSize > $wgMaxUploadSize )
+ $this->status = Status::newFatal( 'largefileserver' );
+ }
+
+ public function verifyUpload() {
+ if ( $this->chunkMode != self::DONE ) {
+ return Status::newGood();
+ }
+ return parent::verifyUpload();
+ }
+
+ public function checkWarnings() {
+ if ( $this->chunkMode != self::DONE ) {
+ return null;
}
+ return parent::checkWarnings();
}
+ public function getImageInfo( $result ) {
+ if ( $this->chunkMode != self::DONE ) {
+ return null;
+ }
+ return parent::getImageInfo( $result );
+ }
}