MediaWiki
REL1_19
|
00001 <?php 00030 class ApiUpload extends ApiBase { 00031 00035 protected $mUpload = null; 00036 00037 protected $mParams; 00038 00039 public function __construct( $main, $action ) { 00040 parent::__construct( $main, $action ); 00041 } 00042 00043 public function execute() { 00044 // Check whether upload is enabled 00045 if ( !UploadBase::isEnabled() ) { 00046 $this->dieUsageMsg( 'uploaddisabled' ); 00047 } 00048 00049 $user = $this->getUser(); 00050 00051 // Parameter handling 00052 $this->mParams = $this->extractRequestParams(); 00053 $request = $this->getMain()->getRequest(); 00054 // Add the uploaded file to the params array 00055 $this->mParams['file'] = $request->getFileName( 'file' ); 00056 $this->mParams['chunk'] = $request->getFileName( 'chunk' ); 00057 00058 // Copy the session key to the file key, for backward compatibility. 00059 if( !$this->mParams['filekey'] && $this->mParams['sessionkey'] ) { 00060 $this->mParams['filekey'] = $this->mParams['sessionkey']; 00061 } 00062 00063 // Select an upload module 00064 if ( !$this->selectUploadModule() ) { 00065 // This is not a true upload, but a status request or similar 00066 return; 00067 } 00068 if ( !isset( $this->mUpload ) ) { 00069 $this->dieUsage( 'No upload module set', 'nomodule' ); 00070 } 00071 00072 // First check permission to upload 00073 $this->checkPermissions( $user ); 00074 00075 // Fetch the file 00076 $status = $this->mUpload->fetchFile(); 00077 if ( !$status->isGood() ) { 00078 $errors = $status->getErrorsArray(); 00079 $error = array_shift( $errors[0] ); 00080 $this->dieUsage( 'Error fetching file from remote source', $error, 0, $errors[0] ); 00081 } 00082 00083 // Check if the uploaded file is sane 00084 if ( $this->mParams['chunk'] ) { 00085 $maxSize = $this->mUpload->getMaxUploadSize( ); 00086 if( $this->mParams['filesize'] > $maxSize ) { 00087 $this->dieUsage( 'The file you submitted was too large', 'file-too-large' ); 00088 } 00089 } else { 00090 $this->verifyUpload(); 00091 } 00092 00093 // Check if the user has the rights to modify or overwrite the requested title 00094 // (This check is irrelevant if stashing is already requested, since the errors 00095 // can always be fixed by changing the title) 00096 if ( ! $this->mParams['stash'] ) { 00097 $permErrors = $this->mUpload->verifyTitlePermissions( $user ); 00098 if ( $permErrors !== true ) { 00099 $this->dieRecoverableError( $permErrors[0], 'filename' ); 00100 } 00101 } 00102 // Get the result based on the current upload context: 00103 $result = $this->getContextResult(); 00104 00105 if ( $result['result'] === 'Success' ) { 00106 $result['imageinfo'] = $this->mUpload->getImageInfo( $this->getResult() ); 00107 } 00108 00109 $this->getResult()->addValue( null, $this->getModuleName(), $result ); 00110 00111 // Cleanup any temporary mess 00112 $this->mUpload->cleanupTempFile(); 00113 } 00117 private function getContextResult(){ 00118 $warnings = $this->getApiWarnings(); 00119 if ( $warnings ) { 00120 // Get warnings formated in result array format 00121 return $this->getWarningsResult( $warnings ); 00122 } elseif ( $this->mParams['chunk'] ) { 00123 // Add chunk, and get result 00124 return $this->getChunkResult(); 00125 } elseif ( $this->mParams['stash'] ) { 00126 // Stash the file and get stash result 00127 return $this->getStashResult(); 00128 } 00129 // This is the most common case -- a normal upload with no warnings 00130 // performUpload will return a formatted properly for the API with status 00131 return $this->performUpload(); 00132 } 00136 private function getStashResult(){ 00137 $result = array (); 00138 // Some uploads can request they be stashed, so as not to publish them immediately. 00139 // In this case, a failure to stash ought to be fatal 00140 try { 00141 $result['result'] = 'Success'; 00142 $result['filekey'] = $this->performStash(); 00143 $result['sessionkey'] = $result['filekey']; // backwards compatibility 00144 } catch ( MWException $e ) { 00145 $this->dieUsage( $e->getMessage(), 'stashfailed' ); 00146 } 00147 return $result; 00148 } 00153 private function getWarningsResult( $warnings ){ 00154 $result = array(); 00155 $result['result'] = 'Warning'; 00156 $result['warnings'] = $warnings; 00157 // in case the warnings can be fixed with some further user action, let's stash this upload 00158 // and return a key they can use to restart it 00159 try { 00160 $result['filekey'] = $this->performStash(); 00161 $result['sessionkey'] = $result['filekey']; // backwards compatibility 00162 } catch ( MWException $e ) { 00163 $result['warnings']['stashfailed'] = $e->getMessage(); 00164 } 00165 return $result; 00166 } 00170 private function getChunkResult(){ 00171 $result = array(); 00172 00173 $result['result'] = 'Continue'; 00174 $request = $this->getMain()->getRequest(); 00175 $chunkPath = $request->getFileTempname( 'chunk' ); 00176 $chunkSize = $request->getUpload( 'chunk' )->getSize(); 00177 if ($this->mParams['offset'] == 0) { 00178 try { 00179 $result['filekey'] = $this->performStash(); 00180 } catch ( MWException $e ) { 00181 // FIXME: Error handling here is wrong/different from rest of this 00182 $this->dieUsage( $e->getMessage(), 'stashfailed' ); 00183 } 00184 } else { 00185 $status = $this->mUpload->addChunk($chunkPath, $chunkSize, 00186 $this->mParams['offset']); 00187 if ( !$status->isGood() ) { 00188 $this->dieUsage( $status->getWikiText(), 'stashfailed' ); 00189 return ; 00190 } 00191 $result['filekey'] = $this->mParams['filekey']; 00192 // Check we added the last chunk: 00193 if( $this->mParams['offset'] + $chunkSize == $this->mParams['filesize'] ) { 00194 $status = $this->mUpload->concatenateChunks(); 00195 if ( !$status->isGood() ) { 00196 $this->dieUsage( $status->getWikiText(), 'stashfailed' ); 00197 return ; 00198 } 00199 $result['result'] = 'Success'; 00200 } 00201 } 00202 $result['offset'] = $this->mParams['offset'] + $chunkSize; 00203 return $result; 00204 } 00205 00212 function performStash() { 00213 try { 00214 $stashFile = $this->mUpload->stashFile(); 00215 00216 if ( !$stashFile ) { 00217 throw new MWException( 'Invalid stashed file' ); 00218 } 00219 $fileKey = $stashFile->getFileKey(); 00220 } catch ( MWException $e ) { 00221 $message = 'Stashing temporary file failed: ' . get_class( $e ) . ' ' . $e->getMessage(); 00222 wfDebug( __METHOD__ . ' ' . $message . "\n"); 00223 throw new MWException( $message ); 00224 } 00225 return $fileKey; 00226 } 00227 00237 function dieRecoverableError( $error, $parameter, $data = array() ) { 00238 try { 00239 $data['filekey'] = $this->performStash(); 00240 $data['sessionkey'] = $data['filekey']; 00241 } catch ( MWException $e ) { 00242 $data['stashfailed'] = $e->getMessage(); 00243 } 00244 $data['invalidparameter'] = $parameter; 00245 00246 $parsed = $this->parseMsg( $error ); 00247 $this->dieUsage( $parsed['info'], $parsed['code'], 0, $data ); 00248 } 00249 00257 protected function selectUploadModule() { 00258 $request = $this->getMain()->getRequest(); 00259 00260 // chunk or one and only one of the following parameters is needed 00261 if( !$this->mParams['chunk'] ) { 00262 $this->requireOnlyOneParameter( $this->mParams, 00263 'filekey', 'file', 'url', 'statuskey' ); 00264 } 00265 00266 if ( $this->mParams['statuskey'] ) { 00267 $this->checkAsyncDownloadEnabled(); 00268 00269 // Status request for an async upload 00270 $sessionData = UploadFromUrlJob::getSessionData( $this->mParams['statuskey'] ); 00271 if ( !isset( $sessionData['result'] ) ) { 00272 $this->dieUsage( 'No result in session data', 'missingresult' ); 00273 } 00274 if ( $sessionData['result'] == 'Warning' ) { 00275 $sessionData['warnings'] = $this->transformWarnings( $sessionData['warnings'] ); 00276 $sessionData['sessionkey'] = $this->mParams['statuskey']; 00277 } 00278 $this->getResult()->addValue( null, $this->getModuleName(), $sessionData ); 00279 return false; 00280 00281 } 00282 00283 // The following modules all require the filename parameter to be set 00284 if ( is_null( $this->mParams['filename'] ) ) { 00285 $this->dieUsageMsg( array( 'missingparam', 'filename' ) ); 00286 } 00287 00288 if ( $this->mParams['chunk'] ) { 00289 // Chunk upload 00290 $this->mUpload = new UploadFromChunks(); 00291 if( isset( $this->mParams['filekey'] ) ){ 00292 // handle new chunk 00293 $this->mUpload->continueChunks( 00294 $this->mParams['filename'], 00295 $this->mParams['filekey'], 00296 $request->getUpload( 'chunk' ) 00297 ); 00298 } else { 00299 // handle first chunk 00300 $this->mUpload->initialize( 00301 $this->mParams['filename'], 00302 $request->getUpload( 'chunk' ) 00303 ); 00304 } 00305 } elseif ( isset( $this->mParams['filekey'] ) ) { 00306 // Upload stashed in a previous request 00307 if ( !UploadFromStash::isValidKey( $this->mParams['filekey'] ) ) { 00308 $this->dieUsageMsg( 'invalid-file-key' ); 00309 } 00310 00311 $this->mUpload = new UploadFromStash( $this->getUser() ); 00312 00313 $this->mUpload->initialize( $this->mParams['filekey'], $this->mParams['filename'] ); 00314 } elseif ( isset( $this->mParams['file'] ) ) { 00315 $this->mUpload = new UploadFromFile(); 00316 $this->mUpload->initialize( 00317 $this->mParams['filename'], 00318 $request->getUpload( 'file' ) 00319 ); 00320 } elseif ( isset( $this->mParams['url'] ) ) { 00321 // Make sure upload by URL is enabled: 00322 if ( !UploadFromUrl::isEnabled() ) { 00323 $this->dieUsageMsg( 'copyuploaddisabled' ); 00324 } 00325 00326 $async = false; 00327 if ( $this->mParams['asyncdownload'] ) { 00328 $this->checkAsyncDownloadEnabled(); 00329 00330 if ( $this->mParams['leavemessage'] && !$this->mParams['ignorewarnings'] ) { 00331 $this->dieUsage( 'Using leavemessage without ignorewarnings is not supported', 00332 'missing-ignorewarnings' ); 00333 } 00334 00335 if ( $this->mParams['leavemessage'] ) { 00336 $async = 'async-leavemessage'; 00337 } else { 00338 $async = 'async'; 00339 } 00340 } 00341 $this->mUpload = new UploadFromUrl; 00342 $this->mUpload->initialize( $this->mParams['filename'], 00343 $this->mParams['url'], $async ); 00344 } 00345 00346 return true; 00347 } 00348 00354 protected function checkPermissions( $user ) { 00355 // Check whether the user has the appropriate permissions to upload anyway 00356 $permission = $this->mUpload->isAllowed( $user ); 00357 00358 if ( $permission !== true ) { 00359 if ( !$user->isLoggedIn() ) { 00360 $this->dieUsageMsg( array( 'mustbeloggedin', 'upload' ) ); 00361 } else { 00362 $this->dieUsageMsg( 'badaccess-groups' ); 00363 } 00364 } 00365 } 00366 00370 protected function verifyUpload( ) { 00371 global $wgFileExtensions; 00372 00373 $verification = $this->mUpload->verifyUpload( ); 00374 if ( $verification['status'] === UploadBase::OK ) { 00375 return; 00376 } 00377 00378 // TODO: Move them to ApiBase's message map 00379 switch( $verification['status'] ) { 00380 // Recoverable errors 00381 case UploadBase::MIN_LENGTH_PARTNAME: 00382 $this->dieRecoverableError( 'filename-tooshort', 'filename' ); 00383 break; 00384 case UploadBase::ILLEGAL_FILENAME: 00385 $this->dieRecoverableError( 'illegal-filename', 'filename', 00386 array( 'filename' => $verification['filtered'] ) ); 00387 break; 00388 case UploadBase::FILENAME_TOO_LONG: 00389 $this->dieRecoverableError( 'filename-toolong', 'filename' ); 00390 break; 00391 case UploadBase::FILETYPE_MISSING: 00392 $this->dieRecoverableError( 'filetype-missing', 'filename' ); 00393 break; 00394 case UploadBase::WINDOWS_NONASCII_FILENAME: 00395 $this->dieRecoverableError( 'windows-nonascii-filename', 'filename' ); 00396 break; 00397 00398 // Unrecoverable errors 00399 case UploadBase::EMPTY_FILE: 00400 $this->dieUsage( 'The file you submitted was empty', 'empty-file' ); 00401 break; 00402 case UploadBase::FILE_TOO_LARGE: 00403 $this->dieUsage( 'The file you submitted was too large', 'file-too-large' ); 00404 break; 00405 00406 case UploadBase::FILETYPE_BADTYPE: 00407 $this->dieUsage( 'This type of file is banned', 'filetype-banned', 00408 0, array( 00409 'filetype' => $verification['finalExt'], 00410 'allowed' => $wgFileExtensions 00411 ) ); 00412 break; 00413 case UploadBase::VERIFICATION_ERROR: 00414 $this->getResult()->setIndexedTagName( $verification['details'], 'detail' ); 00415 $this->dieUsage( 'This file did not pass file verification', 'verification-error', 00416 0, array( 'details' => $verification['details'] ) ); 00417 break; 00418 case UploadBase::HOOK_ABORTED: 00419 $this->dieUsage( "The modification you tried to make was aborted by an extension hook", 00420 'hookaborted', 0, array( 'error' => $verification['error'] ) ); 00421 break; 00422 default: 00423 $this->dieUsage( 'An unknown error occurred', 'unknown-error', 00424 0, array( 'code' => $verification['status'] ) ); 00425 break; 00426 } 00427 } 00428 00429 00437 protected function getApiWarnings() { 00438 $warnings = array(); 00439 00440 if ( !$this->mParams['ignorewarnings'] ) { 00441 $warnings = $this->mUpload->checkWarnings(); 00442 } 00443 return $this->transformWarnings( $warnings ); 00444 } 00445 00446 protected function transformWarnings( $warnings ) { 00447 if ( $warnings ) { 00448 // Add indices 00449 $result = $this->getResult(); 00450 $result->setIndexedTagName( $warnings, 'warning' ); 00451 00452 if ( isset( $warnings['duplicate'] ) ) { 00453 $dupes = array(); 00454 foreach ( $warnings['duplicate'] as $dupe ) { 00455 $dupes[] = $dupe->getName(); 00456 } 00457 $result->setIndexedTagName( $dupes, 'duplicate' ); 00458 $warnings['duplicate'] = $dupes; 00459 } 00460 00461 if ( isset( $warnings['exists'] ) ) { 00462 $warning = $warnings['exists']; 00463 unset( $warnings['exists'] ); 00464 $warnings[$warning['warning']] = $warning['file']->getName(); 00465 } 00466 } 00467 return $warnings; 00468 } 00469 00470 00477 protected function performUpload() { 00478 // Use comment as initial page text by default 00479 if ( is_null( $this->mParams['text'] ) ) { 00480 $this->mParams['text'] = $this->mParams['comment']; 00481 } 00482 00483 $file = $this->mUpload->getLocalFile(); 00484 $watch = $this->getWatchlistValue( $this->mParams['watchlist'], $file->getTitle() ); 00485 00486 // Deprecated parameters 00487 if ( $this->mParams['watch'] ) { 00488 $watch = true; 00489 } 00490 00491 // No errors, no warnings: do the upload 00492 $status = $this->mUpload->performUpload( $this->mParams['comment'], 00493 $this->mParams['text'], $watch, $this->getUser() ); 00494 00495 if ( !$status->isGood() ) { 00496 $error = $status->getErrorsArray(); 00497 00498 if ( count( $error ) == 1 && $error[0][0] == 'async' ) { 00499 // The upload can not be performed right now, because the user 00500 // requested so 00501 return array( 00502 'result' => 'Queued', 00503 'statuskey' => $error[0][1], 00504 ); 00505 } else { 00506 $this->getResult()->setIndexedTagName( $error, 'error' ); 00507 00508 $this->dieUsage( 'An internal error occurred', 'internal-error', 0, $error ); 00509 } 00510 } 00511 00512 $file = $this->mUpload->getLocalFile(); 00513 00514 $result['result'] = 'Success'; 00515 $result['filename'] = $file->getName(); 00516 00517 return $result; 00518 } 00519 00523 protected function checkAsyncDownloadEnabled() { 00524 global $wgAllowAsyncCopyUploads; 00525 if ( !$wgAllowAsyncCopyUploads ) { 00526 $this->dieUsage( 'Asynchronous copy uploads disabled', 'asynccopyuploaddisabled'); 00527 } 00528 } 00529 00530 public function mustBePosted() { 00531 return true; 00532 } 00533 00534 public function isWriteMode() { 00535 return true; 00536 } 00537 00538 public function getAllowedParams() { 00539 $params = array( 00540 'filename' => array( 00541 ApiBase::PARAM_TYPE => 'string', 00542 ), 00543 'comment' => array( 00544 ApiBase::PARAM_DFLT => '' 00545 ), 00546 'text' => null, 00547 'token' => null, 00548 'watch' => array( 00549 ApiBase::PARAM_DFLT => false, 00550 ApiBase::PARAM_DEPRECATED => true, 00551 ), 00552 'watchlist' => array( 00553 ApiBase::PARAM_DFLT => 'preferences', 00554 ApiBase::PARAM_TYPE => array( 00555 'watch', 00556 'preferences', 00557 'nochange' 00558 ), 00559 ), 00560 'ignorewarnings' => false, 00561 'file' => null, 00562 'url' => null, 00563 'filekey' => null, 00564 'sessionkey' => array( 00565 ApiBase::PARAM_DFLT => null, 00566 ApiBase::PARAM_DEPRECATED => true, 00567 ), 00568 'stash' => false, 00569 00570 'filesize' => null, 00571 'offset' => null, 00572 'chunk' => null, 00573 00574 'asyncdownload' => false, 00575 'leavemessage' => false, 00576 'statuskey' => null, 00577 ); 00578 00579 return $params; 00580 } 00581 00582 public function getParamDescription() { 00583 $params = array( 00584 'filename' => 'Target filename', 00585 'token' => 'Edit token. You can get one of these through prop=info', 00586 'comment' => 'Upload comment. Also used as the initial page text for new files if "text" is not specified', 00587 'text' => 'Initial page text for new files', 00588 'watch' => 'Watch the page', 00589 'watchlist' => 'Unconditionally add or remove the page from your watchlist, use preferences or do not change watch', 00590 'ignorewarnings' => 'Ignore any warnings', 00591 'file' => 'File contents', 00592 'url' => 'URL to fetch the file from', 00593 'filekey' => 'Key that identifies a previous upload that was stashed temporarily.', 00594 'sessionkey' => 'Same as filekey, maintained for backward compatibility.', 00595 'stash' => 'If set, the server will not add the file to the repository and stash it temporarily.', 00596 00597 'chunk' => 'Chunk contents', 00598 'offset' => 'Offset of chunk in bytes', 00599 'filesize' => 'Filesize of entire upload', 00600 00601 'asyncdownload' => 'Make fetching a URL asynchronous', 00602 'leavemessage' => 'If asyncdownload is used, leave a message on the user talk page if finished', 00603 'statuskey' => 'Fetch the upload status for this file key', 00604 ); 00605 00606 return $params; 00607 00608 } 00609 00610 public function getDescription() { 00611 return array( 00612 'Upload a file, or get the status of pending uploads. Several methods are available:', 00613 ' * Upload file contents directly, using the "file" parameter', 00614 ' * Have the MediaWiki server fetch a file from a URL, using the "url" parameter', 00615 ' * Complete an earlier upload that failed due to warnings, using the "filekey" parameter', 00616 'Note that the HTTP POST must be done as a file upload (i.e. using multipart/form-data) when', 00617 'sending the "file". Also you must get and send an edit token before doing any upload stuff' 00618 ); 00619 } 00620 00621 public function getPossibleErrors() { 00622 return array_merge( parent::getPossibleErrors(), 00623 $this->getRequireOnlyOneParameterErrorMessages( array( 'filekey', 'file', 'url', 'statuskey' ) ), 00624 array( 00625 array( 'uploaddisabled' ), 00626 array( 'invalid-file-key' ), 00627 array( 'uploaddisabled' ), 00628 array( 'mustbeloggedin', 'upload' ), 00629 array( 'badaccess-groups' ), 00630 array( 'code' => 'fetchfileerror', 'info' => '' ), 00631 array( 'code' => 'nomodule', 'info' => 'No upload module set' ), 00632 array( 'code' => 'empty-file', 'info' => 'The file you submitted was empty' ), 00633 array( 'code' => 'filetype-missing', 'info' => 'The file is missing an extension' ), 00634 array( 'code' => 'filename-tooshort', 'info' => 'The filename is too short' ), 00635 array( 'code' => 'overwrite', 'info' => 'Overwriting an existing file is not allowed' ), 00636 array( 'code' => 'stashfailed', 'info' => 'Stashing temporary file failed' ), 00637 array( 'code' => 'internal-error', 'info' => 'An internal error occurred' ), 00638 array( 'code' => 'asynccopyuploaddisabled', 'info' => 'Asynchronous copy uploads disabled' ), 00639 ) 00640 ); 00641 } 00642 00643 public function needsToken() { 00644 return true; 00645 } 00646 00647 public function getTokenSalt() { 00648 return ''; 00649 } 00650 00651 public function getExamples() { 00652 return array( 00653 'api.php?action=upload&filename=Wiki.png&url=http%3A//upload.wikimedia.org/wikipedia/en/b/bc/Wiki.png' 00654 => 'Upload from a URL', 00655 'api.php?action=upload&filename=Wiki.png&filekey=filekey&ignorewarnings=1' 00656 => 'Complete an upload that failed due to warnings', 00657 ); 00658 } 00659 00660 public function getHelpUrls() { 00661 return 'https://www.mediawiki.org/wiki/API:Upload'; 00662 } 00663 00664 public function getVersion() { 00665 return __CLASS__ . ': $Id$'; 00666 } 00667 }