r61355 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r61354‎ | r61355 | r61356 >
Date:04:37, 22 January 2010
Author:mah
Status:resolved (Comments)
Tags:
Comment:
UploadChunks added from js2 branch. Refactored. Still needs to be
tested w/o the UI first.
Modified paths:
  • /trunk/phase3/includes/AutoLoader.php (modified) (history)
  • /trunk/phase3/includes/api/ApiUpload.php (modified) (history)
  • /trunk/phase3/includes/upload/UploadFromChunks.php (added) (history)

Diff [purge]

Index: trunk/phase3/includes/upload/UploadFromChunks.php
@@ -0,0 +1,225 @@
 2+<?php
 3+/**
 4+ * @file
 5+ * @ingroup upload
 6+ *
 7+ * First, destination checks are made, and, if ignorewarnings is not
 8+ * checked, errors / warning is returned.
 9+ *
 10+ * 1. We return the uploadUrl.
 11+ * 2. We then accept chunk uploads from the client.
 12+ * 3. Return chunk id on each POSTED chunk.
 13+ * 4. Once the client posts "done=1", the files are concatenated together.
 14+ *
 15+ * (More info at: http://firefogg.org/dev/chunk_post.html)
 16+ */
 17+class UploadFromChunks extends UploadBase {
 18+
 19+ const INIT = 1;
 20+ const CHUNK = 2;
 21+ const DONE = 3;
 22+
 23+ // STYLE NOTE: Coding guidelines says the 'm' prefix for object
 24+ // member variables is discouraged in new code but "stay
 25+ // consistent within a class". UploadFromChunks is new, but extends
 26+ // UploadBase which has the 'm' prefix. I'm eschewing the prefix for
 27+ // member variables of this class.
 28+ protected $chunkMode; // INIT, CHUNK, DONE
 29+ protected $sessionKey;
 30+ protected $comment;
 31+ protected $fileSize = 0;
 32+ protected $repoPath;
 33+ protected $pageText;
 34+ protected $watch;
 35+
 36+ public $status;
 37+
 38+ // Parent class requires this function even though it is only
 39+ // used from SpecialUpload.php and we don't do chunked uploading
 40+ // from SpecialUpload -- best to raise an exception for
 41+ // now.
 42+ public function initializeFromRequest( &$request ) {
 43+ throw new MWException( 'not implemented' );
 44+ }
 45+
 46+ public function initialize( &$request ) {
 47+ $done = $request->getText( 'done' );
 48+ $filename = $request->getText( 'filename' );
 49+ $sessionKey = $request->getText( 'chunksessionkey' );
 50+
 51+ $this->initFromSessionKey( $sessionKey, $request );
 52+
 53+ if ( !$this->sessionKey && !$done ) {
 54+ // session key not set, init the chunk upload system:
 55+ $this->chunkMode = self::INIT;
 56+ $this->mDesiredDestName = $filename;
 57+ } else if ( $this->sessionKey && !$done ) {
 58+ $this->chunkMode = self::CHUNK;
 59+ } else if ( $this->sessionKey && $done ) {
 60+ $this->chunkMode = self::DONE;
 61+ }
 62+
 63+ if ( $this->chunkMode == self::CHUNK || $this->chunkMode == self::DONE ) {
 64+ $this->mTempPath = $request->getFileTempName( 'chunk' );
 65+ $this->fileSize += $request->getFileSize( 'chunk' );
 66+ }
 67+ }
 68+
 69+ /**
 70+ * Set session information for chunked uploads and allocate a unique key.
 71+ * @param $comment string
 72+ * @param $pageText string
 73+ * @param $watch boolean
 74+ *
 75+ * @returns string the session key for this chunked upload
 76+ */
 77+ protected function setupChunkSession( $comment, $pageText, $watch ) {
 78+ $this->sessionKey = $this->getSessionKey();
 79+ $_SESSION['wsUploadData'][$this->sessionKey] = array(
 80+ 'comment' => $comment,
 81+ 'pageText' => $pageText,
 82+ 'watch' => $watch,
 83+ 'mFilteredName' => $this->mFilteredName,
 84+ 'repoPath' => null,
 85+ 'mDesiredDestName' => $this->mDesiredDestName,
 86+ 'version' => self::SESSION_VERSION,
 87+ );
 88+ return $this->sessionKey;
 89+ }
 90+
 91+ /**
 92+ * Initialize a continuation of a chunked upload from a session key
 93+ * @param $sessionKey string
 94+ * @param $request WebRequest
 95+ *
 96+ * @returns void
 97+ */
 98+ protected function initFromSessionKey( $sessionKey, $request ) {
 99+ if ( !$sessionKey || empty( $sessionKey ) ) {
 100+ $this->status = Status::newFromFatal( 'Missing session data.' );
 101+ return;
 102+ }
 103+ $this->sessionKey = $sessionKey;
 104+ // load the sessionData array:
 105+ $sessionData = $request->getSessionData( 'wsUploadData' );
 106+
 107+ if ( isset( $sessionData[$this->sessionKey]['version'] )
 108+ && $sessionData[$this->sessionKey]['version'] == self::SESSION_VERSION ) {
 109+ $this->comment = $sessionData[$this->sessionKey]['comment'];
 110+ $this->pageText = $sessionData[$this->sessionKey]['pageText'];
 111+ $this->watch = $sessionData[$this->sessionKey]['watch'];
 112+ $this->mFilteredName = $sessionData[$this->sessionKey]['mFilteredName'];
 113+ $this->repoPath = $sessionData[$this->sessionKey]['repoPath'];
 114+ $this->mDesiredDestName = $sessionData[$this->sessionKey]['mDesiredDestName'];
 115+ } else {
 116+ $this->status = Status::newFromFatal( 'Missing session data.' );
 117+ }
 118+ }
 119+
 120+ /**
 121+ * Handle a chunk of the upload. Overrides the parent method
 122+ * because Chunked Uploading clients (i.e. Firefogg) require
 123+ * specific API responses.
 124+ * @see UploadBase::performUpload
 125+ */
 126+ public function performUpload( $comment, $pageText, $watch, $user ) {
 127+ wfDebug( "\n\n\performUpload(chunked): sum:" . $comment . ' c: ' . $pageText . ' w:' . $watch );
 128+ global $wgUser;
 129+
 130+ if ( $this->chunkMode == self::INIT ) {
 131+ // firefogg expects a specific result per:
 132+ // http://www.firefogg.org/dev/chunk_post.html
 133+
 134+ // it's okay to return the token here because
 135+ // a) the user must have requested the token to get here and
 136+ // b) should only happen over POST
 137+ // c) we need the token to validate chunks are coming from a non-xss request
 138+ $token = urlencode( $wgUser->editToken() );
 139+ ob_clean();
 140+ echo FormatJson::encode( array(
 141+ 'uploadUrl' => wfExpandUrl( wfScript( 'api' ) ) . "?action=upload&" .
 142+ "token={$token}&format=json&enablechunks=true&chunksessionkey=" .
 143+ $this->setupChunkSession( $comment, $pageText, $watch ) ) );
 144+ exit( 0 );
 145+ } else if ( $this->chunkMode == self::CHUNK ) {
 146+ $status = $this->appendChunk();
 147+ if ( !$status->isOK() ) {
 148+ return $status;
 149+ }
 150+ // return success:
 151+ // firefogg expects a specific result
 152+ // http://www.firefogg.org/dev/chunk_post.html
 153+ ob_clean();
 154+ echo FormatJson::encode(
 155+ array( 'result' => 1, 'filesize' => $this->fileSize )
 156+ );
 157+ exit( 0 );
 158+ } else if ( $this->chunkMode == self::DONE ) {
 159+ if ( $comment == '' )
 160+ $comment = $this->comment;
 161+
 162+ if ( $pageText == '' )
 163+ $pageText = $this->pageText;
 164+
 165+ if ( $watch == '' )
 166+ $watch = $this->watch;
 167+
 168+ $status = parent::performUpload( $comment, $pageText, $watch, $user );
 169+ if ( !$status->isGood() ) {
 170+ return $status;
 171+ }
 172+ $file = $this->getLocalFile();
 173+
 174+ // firefogg expects a specific result
 175+ // http://www.firefogg.org/dev/chunk_post.html
 176+ ob_clean();
 177+ echo FormatJson::encode( array(
 178+ 'result' => 1,
 179+ 'done' => 1,
 180+ 'resultUrl' => $file->getDescriptionUrl() )
 181+ );
 182+ exit( 0 );
 183+ }
 184+ }
 185+
 186+ /**
 187+ * Append a chunk to the Repo file
 188+ *
 189+ * @param string $srcPath Path to file to append from
 190+ * @param string $toAppendPath Path to file to append to
 191+ * @return Status Status
 192+ */
 193+ protected function appendToUploadFile( $srcPath, $toAppendPath ) {
 194+ $repo = RepoGroup::singleton()->getLocalRepo();
 195+ $status = $repo->append( $srcPath, $toAppendPath );
 196+ return $status;
 197+ }
 198+
 199+ /**
 200+ * Append a chunk to the temporary file.
 201+ *
 202+ * @return void
 203+ */
 204+ protected function appendChunk() {
 205+ global $wgMaxUploadSize;
 206+
 207+ if ( !$this->repoPath ) {
 208+ $this->status = $this->saveTempUploadedFile( $this->mDesiredDestName, $this->mTempPath );
 209+
 210+ if ( $status->isOK() ) {
 211+ $this->repoPath = $status->value;
 212+ $_SESSION['wsUploadData'][$this->sessionKey]['repoPath'] = $this->repoPath;
 213+ }
 214+ return $status;
 215+ } else {
 216+ if ( $this->getRealPath( $this->repoPath ) ) {
 217+ $this->status = $this->appendToUploadFile( $this->repoPath, $this->mTempPath );
 218+ } else {
 219+ $this->status = Status::newFatal( 'filenotfound', $this->repoPath );
 220+ }
 221+
 222+ if ( $this->fileSize > $wgMaxUploadSize )
 223+ $this->status = Status::newFatal( 'largefileserver' );
 224+ }
 225+ }
 226+}
Property changes on: trunk/phase3/includes/upload/UploadFromChunks.php
___________________________________________________________________
Name: svn:eol-syle
1227 + native
Index: trunk/phase3/includes/api/ApiUpload.php
@@ -60,7 +60,7 @@
6161
6262 // One and only one of the following parameters is needed
6363 $this->requireOnlyOneParameter( $this->mParams,
64 - 'sessionkey', 'file', 'url' );
 64+ 'sessionkey', 'file', 'url', 'enablechunks' );
6565
6666 if ( $this->mParams['sessionkey'] ) {
6767 /**
@@ -69,20 +69,28 @@
7070 // Check the session key
7171 if ( !isset( $_SESSION['wsUploadData'][$this->mParams['sessionkey']] ) )
7272 return $this->dieUsageMsg( array( 'invalid-session-key' ) );
73 -
 73+
7474 $this->mUpload = new UploadFromStash();
7575 $this->mUpload->initialize( $this->mParams['filename'],
7676 $_SESSION['wsUploadData'][$this->mParams['sessionkey']] );
7777 } else {
7878 /**
79 - * Upload from url or file
 79+ * Upload from url, etc
8080 * Parameter filename is required
8181 */
8282 if ( !isset( $this->mParams['filename'] ) )
8383 $this->dieUsageMsg( array( 'missingparam', 'filename' ) );
8484
8585 // Initialize $this->mUpload
86 - if ( isset( $this->mParams['file'] ) ) {
 86+ if ( $this->mParams['enablechunks'] ) {
 87+ $this->mUpload = new UploadFromChunks();
 88+ $this->mUpload->initialize( $request );
 89+
 90+ if ( !$this->mUpload->status->isOK() ) {
 91+ return $this->dieUsageMsg( $this->mUpload->status->getWikiText(),
 92+ 'chunked-error' );
 93+ }
 94+ } elseif ( isset( $this->mParams['file'] ) ) {
8795 $this->mUpload = new UploadFromFile();
8896 $this->mUpload->initialize(
8997 $this->mParams['filename'],
@@ -90,15 +98,15 @@
9199 $request->getFileSize( 'file' )
92100 );
93101 } elseif ( isset( $this->mParams['url'] ) ) {
94 - // make sure upload by url is enabled:
 102+ // make sure upload by url is enabled:
95103 if ( !$wgAllowCopyUploads )
96104 $this->dieUsageMsg( array( 'uploaddisabled' ) );
97 -
 105+
98106 // make sure the current user can upload
99107 if ( ! $wgUser->isAllowed( 'upload_by_url' ) )
100108 $this->dieUsageMsg( array( 'badaccess-groups' ) );
101 -
102 -
 109+
 110+
103111 $this->mUpload = new UploadFromUrl();
104112 $this->mUpload->initialize( $this->mParams['filename'],
105113 $this->mParams['url'] );
@@ -116,7 +124,6 @@
117125
118126 // Finish up the exec command:
119127 $this->doExecUpload();
120 -
121128 }
122129
123130 protected function doExecUpload() {
@@ -228,7 +235,7 @@
229236 // Use comment as initial page text by default
230237 if ( is_null( $this->mParams['text'] ) )
231238 $this->mParams['text'] = $this->mParams['comment'];
232 -
 239+
233240 // No errors, no warnings: do the upload
234241 $status = $this->mUpload->performUpload( $this->mParams['comment'],
235242 $this->mParams['text'], $this->mParams['watch'], $wgUser );
@@ -271,6 +278,10 @@
272279 'watch' => false,
273280 'ignorewarnings' => false,
274281 'file' => null,
 282+ 'enablechunks' => null,
 283+ 'chunksessionkey' => null,
 284+ 'chunk' => null,
 285+ 'done' => false,
275286 'url' => null,
276287 'sessionkey' => null,
277288 );
@@ -290,6 +301,7 @@
291302 'watch' => 'Watch the page',
292303 'ignorewarnings' => 'Ignore any warnings',
293304 'file' => 'File contents',
 305+ 'enablechunks' => 'Set to use chunk mode; see http://firefogg.org/dev/chunk_post.html for protocol',
294306 'url' => 'Url to fetch the file from',
295307 'sessionkey' => array(
296308 'Session key returned by a previous upload that failed due to warnings',
@@ -301,9 +313,11 @@
302314 return array(
303315 'Upload a file, or get the status of pending uploads. Several methods are available:',
304316 ' * Upload file contents directly, using the "file" parameter',
 317+ ' * Upload a file in chunks, using the "enablechunks",',
 318+ ' * Have the MediaWiki server fetch a file from a URL, using the "url" parameter',
305319 ' * Complete an earlier upload that failed due to warnings, using the "sessionkey" parameter',
306320 'Note that the HTTP POST must be done as a file upload (i.e. using multipart/form-data) when',
307 - 'sending the "file" parameter. Note also that queries using session keys must be',
 321+ 'sending the "file" or "chunk" parameters. Note also that queries using session keys must be',
308322 'done in the same login session as the query that originally returned the key (i.e. do not',
309323 'log out and then log back in). Also you must get and send an edit token before doing any upload stuff.'
310324 );
@@ -315,6 +329,8 @@
316330 ' api.php?action=upload&filename=Wiki.png&url=http%3A//upload.wikimedia.org/wikipedia/en/b/bc/Wiki.png',
317331 'Complete an upload that failed due to warnings:',
318332 ' api.php?action=upload&filename=Wiki.png&sessionkey=sessionkey&ignorewarnings=1',
 333+ 'Begin a chunked upload:',
 334+ ' api.php?action=upload&filename=Wiki.png&enablechunks=1'
319335 );
320336 }
321337
Index: trunk/phase3/includes/AutoLoader.php
@@ -121,9 +121,6 @@
122122 'HTMLInfoField' => 'includes/HTMLForm.php',
123123 'Http' => 'includes/HttpFunctions.php',
124124 'HttpRequest' => 'includes/HttpFunctions.php',
125 - 'curlHttpRequest' => 'includes/HttpFunctions.php',
126 - 'phpHttpRequest' => 'includes/HttpFunctions.php',
127 - 'simpleFileWriter' => 'includes/HttpFunctions.php',
128125 'IEContentAnalyzer' => 'includes/IEContentAnalyzer.php',
129126 'ImageGallery' => 'includes/ImageGallery.php',
130127 'ImageHistoryList' => 'includes/ImagePage.php',
@@ -215,6 +212,9 @@
216213 'SqlBagOStuff' => 'includes/BagOStuff.php',
217214 'SquidUpdate' => 'includes/SquidUpdate.php',
218215 'Status' => 'includes/Status.php',
 216+ 'StubUser' => 'includes/StubObject.php',
 217+ 'StubUserLang' => 'includes/StubObject.php',
 218+ 'StubObject' => 'includes/StubObject.php',
219219 'StringUtils' => 'includes/StringUtils.php',
220220 'TablePager' => 'includes/Pager.php',
221221 'ThumbnailImage' => 'includes/MediaTransformOutput.php',
@@ -231,6 +231,7 @@
232232 'UploadFromStash' => 'includes/upload/UploadFromStash.php',
233233 'UploadFromFile' => 'includes/upload/UploadFromFile.php',
234234 'UploadFromUrl' => 'includes/upload/UploadFromUrl.php',
 235+ 'UploadFromChunks' => 'includes/upload/UploadFromChunks.php',
235236 'User' => 'includes/User.php',
236237 'UserArray' => 'includes/UserArray.php',
237238 'UserArrayFromResult' => 'includes/UserArray.php',

Follow-up revisions

RevisionCommit summaryAuthorDate
r61407follow up r61355mah05:20, 23 January 2010
r61778For r61355:...tstarling06:06, 1 February 2010
r61779follow up r61355, initial, incomplete dealing with TimStarlings CRmah07:05, 1 February 2010
r62177follow-up r61355 a bit more, still have several bits to fix (e.g. json handling)mah09:41, 9 February 2010
r62291follow-up r61355...mah05:35, 11 February 2010

Comments

#Comment by Catrope (talk | contribs)   15:01, 22 January 2010
+			'enablechunks' => null,
+			'chunksessionkey' => null,
+			'chunk' => null,
+			'done' => false,
...
+			'enablechunks' => 'Set to use chunk mode; see [http://firefogg.org/dev/chunk_post.html http://firefogg.org/dev/chunk_post.html] for protocol',

You're adding four parameters but documenting only one of them; please document the rest as well. Also, enablechunks is a boolean parameter and should be declared with => false rather than => null, like the done parameter. If the chunk parameter is a boolean in nature (can't tell offhand), this applies to that one too.

Also, it seems like one or more of the chunk-related parameters may be required in chunk mode, in which case their presence should be checked.

-	'curlHttpRequest' => 'includes/HttpFunctions.php',
-	'phpHttpRequest' => 'includes/HttpFunctions.php',
-	'simpleFileWriter' => 'includes/HttpFunctions.php',

These changes seem unrelated, although if they really are classes that are only referenced inside HttpFunctions.php, they should indeed be removed; they were also out of alphabetical order.

#Comment by Bryan (talk | contribs)   21:01, 22 January 2010

I don't really like how you give a $request as parameter to initialize() instead of $done, $filename and $chunkSessionkey. This is more flexible for potential future uses.

UploadBase::initialize is exposed purposely to set mandatory variables.

In general I think we need to look critically to how initialization of the Upload objects is done before 1.16 is released. I'm in general not entirely convinced that the current way is the proper way to do it.


#Comment by MarkAHershberger (talk | contribs)   04:42, 23 January 2010

It needs more than just $done, $filename, and $chunkSessionKey, but ok.

#Comment by Bryan (talk | contribs)   21:02, 22 January 2010
+			exit( 0 );

Won't that skip all MediaWiki::restInPeace stuff?

#Comment by Catrope (talk | contribs)   21:41, 22 January 2010

Yes. I think $wgOut->disable(); is appropriate here.

#Comment by MarkAHershberger (talk | contribs)   03:55, 23 January 2010

Thanks, I basically just copied that and it did look ugly. I'll try to apply your advice.

#Comment by Platonides (talk | contribs)   13:12, 26 January 2010

Sending the editToken to the user expecting him to give it back in the url? I would use a specific token here.

#Comment by Tim Starling (talk | contribs)   05:32, 1 February 2010

Whole file review. In UploadFromChunks.php:

if ( !$sessionKey || empty( $sessionKey ) ) {

This is nonsensical since the two parts here do the same thing, just one of them has errors suppressed and one doesn't. Also it will fail once every 2^31 requests since $sessionKey will be 0. Suggest changing the data source from $request->getText() to $request->getVal() and using $sessionKey === null, and documenting this behaviour.

$this->status = Status::newFromFatal( 'Missing session data.' );

Wrong parameters again, same as HttpFunctions.php. One more instance. These error cases all need to be tested.

wfDebug( "\n\n\performUpload(chunked): sum:" . $comment . ' c: ' . $pageText . ' w:' . $watch );

Incomprehensible debug log entry. The debug log is there to help both sysadmins and developers, not the one guy on the planet who knows what "c" stands for.

			ob_clean();
			echo FormatJson::encode( array(
				'uploadUrl' => wfExpandUrl( wfScript( 'api' ) ) . "?action=upload&" .
				"token={$token}&format=json&enablechunks=true&chunksessionkey=" .
				$this->setupChunkSession( $comment, $pageText, $watch ) ) );
			$wgOut->disable();

It's not appropriate to be echoing things here, and formatting responses. Data should be made available to the API caller, which can handle JSON formatting and output buffering using the standard API interface. If Firefogg cannot understand normal API responses, then the api.php entry point should not be used. The URL should be constructed using wfArrayToCGI().

			return $status;
		} else {

I think it's better to save an indenting level by removing the "else".

			if ( $comment == '' )
				$comment = $this->comment;

			if ( $pageText == '' )
				$pageText = $this->pageText;

I'm not sure if this is intentionally suppressing empty comments or if it's meant to be $comment === null.

In ApiUpload.php

				if ( !$this->mUpload->status->isOK() ) {
					return $this->dieUsageMsg( $this->mUpload->status->getWikiText(),
						'chunked-error' );

dieUsageMsg() only accepts a single parameter and it's not either of those.

		// Finish up the exec command:
		$this->doExecUpload();
	}

	protected function doExecUpload() {

Pointless function split. Suggest recombining them.

		// FIXME: This should be in a try .. finally block with performUpload

PHP does not have "finally" like in Java, maybe Michael meant this figuratively. But nothing in performUpload() should be throwing exceptions on any normal error condition, so I'm not sure what the point in catching them would be.

		$verification = $this->mUpload->verifyUpload();

It's certainly wrong to be running verifyUpload() on a chunk. It only makes sense to run it on the whole file. Since UploadFromChunks sets $this->mTempPath to the path of the current chunk, not the path of the temporary file like what UploadFromStash does, the full file is never checked and this is an exploitable XSS vulnerability. All you have to do is straddle a script across a chunk boundary.

		if ( !$this->mParams['ignorewarnings'] ) {
			$warnings = $this->mUpload->checkWarnings();

It doesn't make sense to call checkWarnings() for every chunk either. This does things like computing the SHA-1 hash of the file and checking if there is a duplicate already uploaded.

		// Append imageinfo to the result
		$imParam = ApiQueryImageInfo::getPropertyNames();
		$result['imageinfo'] = ApiQueryImageInfo::getInfo( $file,
				array_flip( $imParam ), $this->getResult() );

Yet another thing that doesn't make sense for chunk upload.


#Comment by Catrope (talk | contribs)   14:34, 1 February 2010

Additional comments about dieUsageMsg():

  • The single parameter accepted is array( 'messagekey', param1, param2, ... ). You can grab an array of such arrays with $status->getErrorsArray() and use the first element of that array, provided the function setting the error in $status passes errors as a message key with params and doesn't try to wfMsg() the error itself.
  • dieUsage() has two mandatory parameters, the error text and the error code. This seems to be what you intended, but since the upload backend gives you a Status object you should use dieUsageMsg() instead: this is clearer to the client than passing non-machine-readable wikitext with a 'chunked-error' code.
  • dieUsageMsg() relies on ApiBase::$messageMap to translate these messagekey-params arrays to API errors. Check that the message keys returned from the backend functions are present in the message map and add any missing ones. Missing message keys aren't the end of the world: they won't crash your code, but they'll result in 'unknownerror' errors.
  • dieUsage() and dieUsageMsg() are functions that never return (much like exit()), so don't use return $this->dieUsageMsg( ... );

Also, test error cases if you're not already doing that. The broken usage of dieUsageMsg() strongly suggests that that error case wasn't tested.

#Comment by MarkAHershberger (talk | contribs)   05:38, 11 February 2010

see r62291 for all relevant fixes

#Comment by MarkAHershberger (talk | contribs)   05:39, 11 February 2010

err... not all, but the changeset that makes uploadchunks actually work against firefogg

#Comment by MarkAHershberger (talk | contribs)   20:21, 22 February 2010

see r62806 for the rest of the fixes.

Status & tagging log