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 |
1 | 227 | + native |
Index: trunk/phase3/includes/api/ApiUpload.php |
— | — | @@ -60,7 +60,7 @@ |
61 | 61 | |
62 | 62 | // One and only one of the following parameters is needed |
63 | 63 | $this->requireOnlyOneParameter( $this->mParams, |
64 | | - 'sessionkey', 'file', 'url' ); |
| 64 | + 'sessionkey', 'file', 'url', 'enablechunks' ); |
65 | 65 | |
66 | 66 | if ( $this->mParams['sessionkey'] ) { |
67 | 67 | /** |
— | — | @@ -69,20 +69,28 @@ |
70 | 70 | // Check the session key |
71 | 71 | if ( !isset( $_SESSION['wsUploadData'][$this->mParams['sessionkey']] ) ) |
72 | 72 | return $this->dieUsageMsg( array( 'invalid-session-key' ) ); |
73 | | - |
| 73 | + |
74 | 74 | $this->mUpload = new UploadFromStash(); |
75 | 75 | $this->mUpload->initialize( $this->mParams['filename'], |
76 | 76 | $_SESSION['wsUploadData'][$this->mParams['sessionkey']] ); |
77 | 77 | } else { |
78 | 78 | /** |
79 | | - * Upload from url or file |
| 79 | + * Upload from url, etc |
80 | 80 | * Parameter filename is required |
81 | 81 | */ |
82 | 82 | if ( !isset( $this->mParams['filename'] ) ) |
83 | 83 | $this->dieUsageMsg( array( 'missingparam', 'filename' ) ); |
84 | 84 | |
85 | 85 | // 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'] ) ) { |
87 | 95 | $this->mUpload = new UploadFromFile(); |
88 | 96 | $this->mUpload->initialize( |
89 | 97 | $this->mParams['filename'], |
— | — | @@ -90,15 +98,15 @@ |
91 | 99 | $request->getFileSize( 'file' ) |
92 | 100 | ); |
93 | 101 | } elseif ( isset( $this->mParams['url'] ) ) { |
94 | | - // make sure upload by url is enabled: |
| 102 | + // make sure upload by url is enabled: |
95 | 103 | if ( !$wgAllowCopyUploads ) |
96 | 104 | $this->dieUsageMsg( array( 'uploaddisabled' ) ); |
97 | | - |
| 105 | + |
98 | 106 | // make sure the current user can upload |
99 | 107 | if ( ! $wgUser->isAllowed( 'upload_by_url' ) ) |
100 | 108 | $this->dieUsageMsg( array( 'badaccess-groups' ) ); |
101 | | - |
102 | | - |
| 109 | + |
| 110 | + |
103 | 111 | $this->mUpload = new UploadFromUrl(); |
104 | 112 | $this->mUpload->initialize( $this->mParams['filename'], |
105 | 113 | $this->mParams['url'] ); |
— | — | @@ -116,7 +124,6 @@ |
117 | 125 | |
118 | 126 | // Finish up the exec command: |
119 | 127 | $this->doExecUpload(); |
120 | | - |
121 | 128 | } |
122 | 129 | |
123 | 130 | protected function doExecUpload() { |
— | — | @@ -228,7 +235,7 @@ |
229 | 236 | // Use comment as initial page text by default |
230 | 237 | if ( is_null( $this->mParams['text'] ) ) |
231 | 238 | $this->mParams['text'] = $this->mParams['comment']; |
232 | | - |
| 239 | + |
233 | 240 | // No errors, no warnings: do the upload |
234 | 241 | $status = $this->mUpload->performUpload( $this->mParams['comment'], |
235 | 242 | $this->mParams['text'], $this->mParams['watch'], $wgUser ); |
— | — | @@ -271,6 +278,10 @@ |
272 | 279 | 'watch' => false, |
273 | 280 | 'ignorewarnings' => false, |
274 | 281 | 'file' => null, |
| 282 | + 'enablechunks' => null, |
| 283 | + 'chunksessionkey' => null, |
| 284 | + 'chunk' => null, |
| 285 | + 'done' => false, |
275 | 286 | 'url' => null, |
276 | 287 | 'sessionkey' => null, |
277 | 288 | ); |
— | — | @@ -290,6 +301,7 @@ |
291 | 302 | 'watch' => 'Watch the page', |
292 | 303 | 'ignorewarnings' => 'Ignore any warnings', |
293 | 304 | 'file' => 'File contents', |
| 305 | + 'enablechunks' => 'Set to use chunk mode; see http://firefogg.org/dev/chunk_post.html for protocol', |
294 | 306 | 'url' => 'Url to fetch the file from', |
295 | 307 | 'sessionkey' => array( |
296 | 308 | 'Session key returned by a previous upload that failed due to warnings', |
— | — | @@ -301,9 +313,11 @@ |
302 | 314 | return array( |
303 | 315 | 'Upload a file, or get the status of pending uploads. Several methods are available:', |
304 | 316 | ' * 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', |
305 | 319 | ' * Complete an earlier upload that failed due to warnings, using the "sessionkey" parameter', |
306 | 320 | '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', |
308 | 322 | 'done in the same login session as the query that originally returned the key (i.e. do not', |
309 | 323 | 'log out and then log back in). Also you must get and send an edit token before doing any upload stuff.' |
310 | 324 | ); |
— | — | @@ -315,6 +329,8 @@ |
316 | 330 | ' api.php?action=upload&filename=Wiki.png&url=http%3A//upload.wikimedia.org/wikipedia/en/b/bc/Wiki.png', |
317 | 331 | 'Complete an upload that failed due to warnings:', |
318 | 332 | ' 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' |
319 | 335 | ); |
320 | 336 | } |
321 | 337 | |
Index: trunk/phase3/includes/AutoLoader.php |
— | — | @@ -121,9 +121,6 @@ |
122 | 122 | 'HTMLInfoField' => 'includes/HTMLForm.php', |
123 | 123 | 'Http' => 'includes/HttpFunctions.php', |
124 | 124 | 'HttpRequest' => 'includes/HttpFunctions.php', |
125 | | - 'curlHttpRequest' => 'includes/HttpFunctions.php', |
126 | | - 'phpHttpRequest' => 'includes/HttpFunctions.php', |
127 | | - 'simpleFileWriter' => 'includes/HttpFunctions.php', |
128 | 125 | 'IEContentAnalyzer' => 'includes/IEContentAnalyzer.php', |
129 | 126 | 'ImageGallery' => 'includes/ImageGallery.php', |
130 | 127 | 'ImageHistoryList' => 'includes/ImagePage.php', |
— | — | @@ -215,6 +212,9 @@ |
216 | 213 | 'SqlBagOStuff' => 'includes/BagOStuff.php', |
217 | 214 | 'SquidUpdate' => 'includes/SquidUpdate.php', |
218 | 215 | 'Status' => 'includes/Status.php', |
| 216 | + 'StubUser' => 'includes/StubObject.php', |
| 217 | + 'StubUserLang' => 'includes/StubObject.php', |
| 218 | + 'StubObject' => 'includes/StubObject.php', |
219 | 219 | 'StringUtils' => 'includes/StringUtils.php', |
220 | 220 | 'TablePager' => 'includes/Pager.php', |
221 | 221 | 'ThumbnailImage' => 'includes/MediaTransformOutput.php', |
— | — | @@ -231,6 +231,7 @@ |
232 | 232 | 'UploadFromStash' => 'includes/upload/UploadFromStash.php', |
233 | 233 | 'UploadFromFile' => 'includes/upload/UploadFromFile.php', |
234 | 234 | 'UploadFromUrl' => 'includes/upload/UploadFromUrl.php', |
| 235 | + 'UploadFromChunks' => 'includes/upload/UploadFromChunks.php', |
235 | 236 | 'User' => 'includes/User.php', |
236 | 237 | 'UserArray' => 'includes/UserArray.php', |
237 | 238 | 'UserArrayFromResult' => 'includes/UserArray.php', |