MediaWiki
master
|
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 if ( !$this->mUpload->getTitle() ) { 00090 $this->dieUsage( 'Invalid file title supplied', 'internal-error' ); 00091 } 00092 } else { 00093 $this->verifyUpload(); 00094 } 00095 00096 // Check if the user has the rights to modify or overwrite the requested title 00097 // (This check is irrelevant if stashing is already requested, since the errors 00098 // can always be fixed by changing the title) 00099 if ( ! $this->mParams['stash'] ) { 00100 $permErrors = $this->mUpload->verifyTitlePermissions( $user ); 00101 if ( $permErrors !== true ) { 00102 $this->dieRecoverableError( $permErrors[0], 'filename' ); 00103 } 00104 } 00105 // Get the result based on the current upload context: 00106 $result = $this->getContextResult(); 00107 00108 if ( $result['result'] === 'Success' ) { 00109 $result['imageinfo'] = $this->mUpload->getImageInfo( $this->getResult() ); 00110 } 00111 00112 $this->getResult()->addValue( null, $this->getModuleName(), $result ); 00113 00114 // Cleanup any temporary mess 00115 $this->mUpload->cleanupTempFile(); 00116 } 00121 private function getContextResult(){ 00122 $warnings = $this->getApiWarnings(); 00123 if ( $warnings && !$this->mParams['ignorewarnings'] ) { 00124 // Get warnings formated in result array format 00125 return $this->getWarningsResult( $warnings ); 00126 } elseif ( $this->mParams['chunk'] ) { 00127 // Add chunk, and get result 00128 return $this->getChunkResult( $warnings ); 00129 } elseif ( $this->mParams['stash'] ) { 00130 // Stash the file and get stash result 00131 return $this->getStashResult( $warnings ); 00132 } 00133 // This is the most common case -- a normal upload with no warnings 00134 // performUpload will return a formatted properly for the API with status 00135 return $this->performUpload( $warnings ); 00136 } 00142 private function getStashResult( $warnings ){ 00143 $result = array (); 00144 // Some uploads can request they be stashed, so as not to publish them immediately. 00145 // In this case, a failure to stash ought to be fatal 00146 try { 00147 $result['result'] = 'Success'; 00148 $result['filekey'] = $this->performStash(); 00149 $result['sessionkey'] = $result['filekey']; // backwards compatibility 00150 if ( $warnings && count( $warnings ) > 0 ) { 00151 $result['warnings'] = $warnings; 00152 } 00153 } catch ( MWException $e ) { 00154 $this->dieUsage( $e->getMessage(), 'stashfailed' ); 00155 } 00156 return $result; 00157 } 00163 private function getWarningsResult( $warnings ){ 00164 $result = array(); 00165 $result['result'] = 'Warning'; 00166 $result['warnings'] = $warnings; 00167 // in case the warnings can be fixed with some further user action, let's stash this upload 00168 // and return a key they can use to restart it 00169 try { 00170 $result['filekey'] = $this->performStash(); 00171 $result['sessionkey'] = $result['filekey']; // backwards compatibility 00172 } catch ( MWException $e ) { 00173 $result['warnings']['stashfailed'] = $e->getMessage(); 00174 } 00175 return $result; 00176 } 00182 private function getChunkResult( $warnings ){ 00183 $result = array(); 00184 00185 $result['result'] = 'Continue'; 00186 if ( $warnings && count( $warnings ) > 0 ) { 00187 $result['warnings'] = $warnings; 00188 } 00189 $request = $this->getMain()->getRequest(); 00190 $chunkPath = $request->getFileTempname( 'chunk' ); 00191 $chunkSize = $request->getUpload( 'chunk' )->getSize(); 00192 if ($this->mParams['offset'] == 0) { 00193 $result['filekey'] = $this->performStash(); 00194 } else { 00195 $status = $this->mUpload->addChunk($chunkPath, $chunkSize, 00196 $this->mParams['offset']); 00197 if ( !$status->isGood() ) { 00198 $this->dieUsage( $status->getWikiText(), 'stashfailed' ); 00199 return array(); 00200 } 00201 00202 // Check we added the last chunk: 00203 if( $this->mParams['offset'] + $chunkSize == $this->mParams['filesize'] ) { 00204 $status = $this->mUpload->concatenateChunks(); 00205 00206 if ( !$status->isGood() ) { 00207 $this->dieUsage( $status->getWikiText(), 'stashfailed' ); 00208 return array(); 00209 } 00210 00211 // We have a new filekey for the fully concatenated file. 00212 $result['filekey'] = $this->mUpload->getLocalFile()->getFileKey(); 00213 00214 // Remove chunk from stash. (Checks against user ownership of chunks.) 00215 $this->mUpload->stash->removeFile( $this->mParams['filekey'] ); 00216 00217 $result['result'] = 'Success'; 00218 00219 } else { 00220 00221 // Continue passing through the filekey for adding further chunks. 00222 $result['filekey'] = $this->mParams['filekey']; 00223 } 00224 } 00225 $result['offset'] = $this->mParams['offset'] + $chunkSize; 00226 return $result; 00227 } 00228 00235 function performStash() { 00236 try { 00237 $stashFile = $this->mUpload->stashFile(); 00238 00239 if ( !$stashFile ) { 00240 throw new MWException( 'Invalid stashed file' ); 00241 } 00242 $fileKey = $stashFile->getFileKey(); 00243 } catch ( MWException $e ) { 00244 $message = 'Stashing temporary file failed: ' . get_class( $e ) . ' ' . $e->getMessage(); 00245 wfDebug( __METHOD__ . ' ' . $message . "\n"); 00246 throw new MWException( $message ); 00247 } 00248 return $fileKey; 00249 } 00250 00260 function dieRecoverableError( $error, $parameter, $data = array() ) { 00261 try { 00262 $data['filekey'] = $this->performStash(); 00263 $data['sessionkey'] = $data['filekey']; 00264 } catch ( MWException $e ) { 00265 $data['stashfailed'] = $e->getMessage(); 00266 } 00267 $data['invalidparameter'] = $parameter; 00268 00269 $parsed = $this->parseMsg( $error ); 00270 $this->dieUsage( $parsed['info'], $parsed['code'], 0, $data ); 00271 } 00272 00280 protected function selectUploadModule() { 00281 $request = $this->getMain()->getRequest(); 00282 00283 // chunk or one and only one of the following parameters is needed 00284 if( !$this->mParams['chunk'] ) { 00285 $this->requireOnlyOneParameter( $this->mParams, 00286 'filekey', 'file', 'url', 'statuskey' ); 00287 } 00288 00289 if ( $this->mParams['statuskey'] ) { 00290 $this->checkAsyncDownloadEnabled(); 00291 00292 // Status request for an async upload 00293 $sessionData = UploadFromUrlJob::getSessionData( $this->mParams['statuskey'] ); 00294 if ( !isset( $sessionData['result'] ) ) { 00295 $this->dieUsage( 'No result in session data', 'missingresult' ); 00296 } 00297 if ( $sessionData['result'] == 'Warning' ) { 00298 $sessionData['warnings'] = $this->transformWarnings( $sessionData['warnings'] ); 00299 $sessionData['sessionkey'] = $this->mParams['statuskey']; 00300 } 00301 $this->getResult()->addValue( null, $this->getModuleName(), $sessionData ); 00302 return false; 00303 00304 } 00305 00306 // The following modules all require the filename parameter to be set 00307 if ( is_null( $this->mParams['filename'] ) ) { 00308 $this->dieUsageMsg( array( 'missingparam', 'filename' ) ); 00309 } 00310 00311 if ( $this->mParams['chunk'] ) { 00312 // Chunk upload 00313 $this->mUpload = new UploadFromChunks(); 00314 if( isset( $this->mParams['filekey'] ) ){ 00315 // handle new chunk 00316 $this->mUpload->continueChunks( 00317 $this->mParams['filename'], 00318 $this->mParams['filekey'], 00319 $request->getUpload( 'chunk' ) 00320 ); 00321 } else { 00322 // handle first chunk 00323 $this->mUpload->initialize( 00324 $this->mParams['filename'], 00325 $request->getUpload( 'chunk' ) 00326 ); 00327 } 00328 } elseif ( isset( $this->mParams['filekey'] ) ) { 00329 // Upload stashed in a previous request 00330 if ( !UploadFromStash::isValidKey( $this->mParams['filekey'] ) ) { 00331 $this->dieUsageMsg( 'invalid-file-key' ); 00332 } 00333 00334 $this->mUpload = new UploadFromStash( $this->getUser() ); 00335 00336 $this->mUpload->initialize( $this->mParams['filekey'], $this->mParams['filename'] ); 00337 } elseif ( isset( $this->mParams['file'] ) ) { 00338 $this->mUpload = new UploadFromFile(); 00339 $this->mUpload->initialize( 00340 $this->mParams['filename'], 00341 $request->getUpload( 'file' ) 00342 ); 00343 } elseif ( isset( $this->mParams['url'] ) ) { 00344 // Make sure upload by URL is enabled: 00345 if ( !UploadFromUrl::isEnabled() ) { 00346 $this->dieUsageMsg( 'copyuploaddisabled' ); 00347 } 00348 00349 if ( !UploadFromUrl::isAllowedHost( $this->mParams['url'] ) ) { 00350 $this->dieUsageMsg( 'copyuploadbaddomain' ); 00351 } 00352 00353 $async = false; 00354 if ( $this->mParams['asyncdownload'] ) { 00355 $this->checkAsyncDownloadEnabled(); 00356 00357 if ( $this->mParams['leavemessage'] && !$this->mParams['ignorewarnings'] ) { 00358 $this->dieUsage( 'Using leavemessage without ignorewarnings is not supported', 00359 'missing-ignorewarnings' ); 00360 } 00361 00362 if ( $this->mParams['leavemessage'] ) { 00363 $async = 'async-leavemessage'; 00364 } else { 00365 $async = 'async'; 00366 } 00367 } 00368 $this->mUpload = new UploadFromUrl; 00369 $this->mUpload->initialize( $this->mParams['filename'], 00370 $this->mParams['url'], $async ); 00371 } 00372 00373 return true; 00374 } 00375 00381 protected function checkPermissions( $user ) { 00382 // Check whether the user has the appropriate permissions to upload anyway 00383 $permission = $this->mUpload->isAllowed( $user ); 00384 00385 if ( $permission !== true ) { 00386 if ( !$user->isLoggedIn() ) { 00387 $this->dieUsageMsg( array( 'mustbeloggedin', 'upload' ) ); 00388 } else { 00389 $this->dieUsageMsg( 'badaccess-groups' ); 00390 } 00391 } 00392 } 00393 00397 protected function verifyUpload( ) { 00398 global $wgFileExtensions; 00399 00400 $verification = $this->mUpload->verifyUpload( ); 00401 if ( $verification['status'] === UploadBase::OK ) { 00402 return; 00403 } 00404 00405 // TODO: Move them to ApiBase's message map 00406 switch( $verification['status'] ) { 00407 // Recoverable errors 00408 case UploadBase::MIN_LENGTH_PARTNAME: 00409 $this->dieRecoverableError( 'filename-tooshort', 'filename' ); 00410 break; 00411 case UploadBase::ILLEGAL_FILENAME: 00412 $this->dieRecoverableError( 'illegal-filename', 'filename', 00413 array( 'filename' => $verification['filtered'] ) ); 00414 break; 00415 case UploadBase::FILENAME_TOO_LONG: 00416 $this->dieRecoverableError( 'filename-toolong', 'filename' ); 00417 break; 00418 case UploadBase::FILETYPE_MISSING: 00419 $this->dieRecoverableError( 'filetype-missing', 'filename' ); 00420 break; 00421 case UploadBase::WINDOWS_NONASCII_FILENAME: 00422 $this->dieRecoverableError( 'windows-nonascii-filename', 'filename' ); 00423 break; 00424 00425 // Unrecoverable errors 00426 case UploadBase::EMPTY_FILE: 00427 $this->dieUsage( 'The file you submitted was empty', 'empty-file' ); 00428 break; 00429 case UploadBase::FILE_TOO_LARGE: 00430 $this->dieUsage( 'The file you submitted was too large', 'file-too-large' ); 00431 break; 00432 00433 case UploadBase::FILETYPE_BADTYPE: 00434 $extradata = array( 00435 'filetype' => $verification['finalExt'], 00436 'allowed' => $wgFileExtensions 00437 ); 00438 $this->getResult()->setIndexedTagName( $extradata['allowed'], 'ext' ); 00439 00440 $msg = "Filetype not permitted: "; 00441 if ( isset( $verification['blacklistedExt'] ) ) { 00442 $msg .= join( ', ', $verification['blacklistedExt'] ); 00443 $extradata['blacklisted'] = array_values( $verification['blacklistedExt'] ); 00444 $this->getResult()->setIndexedTagName( $extradata['blacklisted'], 'ext' ); 00445 } else { 00446 $msg .= $verification['finalExt']; 00447 } 00448 $this->dieUsage( $msg, 'filetype-banned', 0, $extradata ); 00449 break; 00450 case UploadBase::VERIFICATION_ERROR: 00451 $this->getResult()->setIndexedTagName( $verification['details'], 'detail' ); 00452 $this->dieUsage( 'This file did not pass file verification', 'verification-error', 00453 0, array( 'details' => $verification['details'] ) ); 00454 break; 00455 case UploadBase::HOOK_ABORTED: 00456 $this->dieUsage( "The modification you tried to make was aborted by an extension hook", 00457 'hookaborted', 0, array( 'error' => $verification['error'] ) ); 00458 break; 00459 default: 00460 $this->dieUsage( 'An unknown error occurred', 'unknown-error', 00461 0, array( 'code' => $verification['status'] ) ); 00462 break; 00463 } 00464 } 00465 00466 00474 protected function getApiWarnings() { 00475 $warnings = $this->mUpload->checkWarnings(); 00476 00477 return $this->transformWarnings( $warnings ); 00478 } 00479 00480 protected function transformWarnings( $warnings ) { 00481 if ( $warnings ) { 00482 // Add indices 00483 $result = $this->getResult(); 00484 $result->setIndexedTagName( $warnings, 'warning' ); 00485 00486 if ( isset( $warnings['duplicate'] ) ) { 00487 $dupes = array(); 00488 foreach ( $warnings['duplicate'] as $dupe ) { 00489 $dupes[] = $dupe->getName(); 00490 } 00491 $result->setIndexedTagName( $dupes, 'duplicate' ); 00492 $warnings['duplicate'] = $dupes; 00493 } 00494 00495 if ( isset( $warnings['exists'] ) ) { 00496 $warning = $warnings['exists']; 00497 unset( $warnings['exists'] ); 00498 $warnings[$warning['warning']] = $warning['file']->getName(); 00499 } 00500 } 00501 return $warnings; 00502 } 00503 00504 00512 protected function performUpload( $warnings ) { 00513 // Use comment as initial page text by default 00514 if ( is_null( $this->mParams['text'] ) ) { 00515 $this->mParams['text'] = $this->mParams['comment']; 00516 } 00517 00518 $file = $this->mUpload->getLocalFile(); 00519 $watch = $this->getWatchlistValue( $this->mParams['watchlist'], $file->getTitle() ); 00520 00521 // Deprecated parameters 00522 if ( $this->mParams['watch'] ) { 00523 $watch = true; 00524 } 00525 00526 // No errors, no warnings: do the upload 00527 $status = $this->mUpload->performUpload( $this->mParams['comment'], 00528 $this->mParams['text'], $watch, $this->getUser() ); 00529 00530 if ( !$status->isGood() ) { 00531 $error = $status->getErrorsArray(); 00532 00533 if ( count( $error ) == 1 && $error[0][0] == 'async' ) { 00534 // The upload can not be performed right now, because the user 00535 // requested so 00536 return array( 00537 'result' => 'Queued', 00538 'statuskey' => $error[0][1], 00539 ); 00540 } else { 00541 $this->getResult()->setIndexedTagName( $error, 'error' ); 00542 00543 $this->dieUsage( 'An internal error occurred', 'internal-error', 0, $error ); 00544 } 00545 } 00546 00547 $file = $this->mUpload->getLocalFile(); 00548 00549 $result['result'] = 'Success'; 00550 $result['filename'] = $file->getName(); 00551 if ( $warnings && count( $warnings ) > 0 ) { 00552 $result['warnings'] = $warnings; 00553 } 00554 00555 return $result; 00556 } 00557 00561 protected function checkAsyncDownloadEnabled() { 00562 global $wgAllowAsyncCopyUploads; 00563 if ( !$wgAllowAsyncCopyUploads ) { 00564 $this->dieUsage( 'Asynchronous copy uploads disabled', 'asynccopyuploaddisabled'); 00565 } 00566 } 00567 00568 public function mustBePosted() { 00569 return true; 00570 } 00571 00572 public function isWriteMode() { 00573 return true; 00574 } 00575 00576 public function getAllowedParams() { 00577 $params = array( 00578 'filename' => array( 00579 ApiBase::PARAM_TYPE => 'string', 00580 ), 00581 'comment' => array( 00582 ApiBase::PARAM_DFLT => '' 00583 ), 00584 'text' => null, 00585 'token' => array( 00586 ApiBase::PARAM_TYPE => 'string', 00587 ApiBase::PARAM_REQUIRED => true 00588 ), 00589 'watch' => array( 00590 ApiBase::PARAM_DFLT => false, 00591 ApiBase::PARAM_DEPRECATED => true, 00592 ), 00593 'watchlist' => array( 00594 ApiBase::PARAM_DFLT => 'preferences', 00595 ApiBase::PARAM_TYPE => array( 00596 'watch', 00597 'preferences', 00598 'nochange' 00599 ), 00600 ), 00601 'ignorewarnings' => false, 00602 'file' => null, 00603 'url' => null, 00604 'filekey' => null, 00605 'sessionkey' => array( 00606 ApiBase::PARAM_DFLT => null, 00607 ApiBase::PARAM_DEPRECATED => true, 00608 ), 00609 'stash' => false, 00610 00611 'filesize' => null, 00612 'offset' => null, 00613 'chunk' => null, 00614 00615 'asyncdownload' => false, 00616 'leavemessage' => false, 00617 'statuskey' => null, 00618 ); 00619 00620 return $params; 00621 } 00622 00623 public function getParamDescription() { 00624 $params = array( 00625 'filename' => 'Target filename', 00626 'token' => 'Edit token. You can get one of these through prop=info', 00627 'comment' => 'Upload comment. Also used as the initial page text for new files if "text" is not specified', 00628 'text' => 'Initial page text for new files', 00629 'watch' => 'Watch the page', 00630 'watchlist' => 'Unconditionally add or remove the page from your watchlist, use preferences or do not change watch', 00631 'ignorewarnings' => 'Ignore any warnings', 00632 'file' => 'File contents', 00633 'url' => 'URL to fetch the file from', 00634 'filekey' => 'Key that identifies a previous upload that was stashed temporarily.', 00635 'sessionkey' => 'Same as filekey, maintained for backward compatibility.', 00636 'stash' => 'If set, the server will not add the file to the repository and stash it temporarily.', 00637 00638 'chunk' => 'Chunk contents', 00639 'offset' => 'Offset of chunk in bytes', 00640 'filesize' => 'Filesize of entire upload', 00641 00642 'asyncdownload' => 'Make fetching a URL asynchronous', 00643 'leavemessage' => 'If asyncdownload is used, leave a message on the user talk page if finished', 00644 'statuskey' => 'Fetch the upload status for this file key', 00645 ); 00646 00647 return $params; 00648 00649 } 00650 00651 public function getResultProperties() { 00652 return array( 00653 '' => array( 00654 'result' => array( 00655 ApiBase::PROP_TYPE => array( 00656 'Success', 00657 'Warning', 00658 'Continue', 00659 'Queued' 00660 ), 00661 ), 00662 'filekey' => array( 00663 ApiBase::PROP_TYPE => 'string', 00664 ApiBase::PROP_NULLABLE => true 00665 ), 00666 'sessionkey' => array( 00667 ApiBase::PROP_TYPE => 'string', 00668 ApiBase::PROP_NULLABLE => true 00669 ), 00670 'offset' => array( 00671 ApiBase::PROP_TYPE => 'integer', 00672 ApiBase::PROP_NULLABLE => true 00673 ), 00674 'statuskey' => array( 00675 ApiBase::PROP_TYPE => 'string', 00676 ApiBase::PROP_NULLABLE => true 00677 ), 00678 'filename' => array( 00679 ApiBase::PROP_TYPE => 'string', 00680 ApiBase::PROP_NULLABLE => true 00681 ) 00682 ) 00683 ); 00684 } 00685 00686 public function getDescription() { 00687 return array( 00688 'Upload a file, or get the status of pending uploads. Several methods are available:', 00689 ' * Upload file contents directly, using the "file" parameter', 00690 ' * Have the MediaWiki server fetch a file from a URL, using the "url" parameter', 00691 ' * Complete an earlier upload that failed due to warnings, using the "filekey" parameter', 00692 'Note that the HTTP POST must be done as a file upload (i.e. using multipart/form-data) when', 00693 'sending the "file". Also you must get and send an edit token before doing any upload stuff' 00694 ); 00695 } 00696 00697 public function getPossibleErrors() { 00698 return array_merge( parent::getPossibleErrors(), 00699 $this->getRequireOnlyOneParameterErrorMessages( array( 'filekey', 'file', 'url', 'statuskey' ) ), 00700 array( 00701 array( 'uploaddisabled' ), 00702 array( 'invalid-file-key' ), 00703 array( 'uploaddisabled' ), 00704 array( 'mustbeloggedin', 'upload' ), 00705 array( 'badaccess-groups' ), 00706 array( 'code' => 'fetchfileerror', 'info' => '' ), 00707 array( 'code' => 'nomodule', 'info' => 'No upload module set' ), 00708 array( 'code' => 'empty-file', 'info' => 'The file you submitted was empty' ), 00709 array( 'code' => 'filetype-missing', 'info' => 'The file is missing an extension' ), 00710 array( 'code' => 'filename-tooshort', 'info' => 'The filename is too short' ), 00711 array( 'code' => 'overwrite', 'info' => 'Overwriting an existing file is not allowed' ), 00712 array( 'code' => 'stashfailed', 'info' => 'Stashing temporary file failed' ), 00713 array( 'code' => 'internal-error', 'info' => 'An internal error occurred' ), 00714 array( 'code' => 'asynccopyuploaddisabled', 'info' => 'Asynchronous copy uploads disabled' ), 00715 array( 'fileexists-forbidden' ), 00716 array( 'fileexists-shared-forbidden' ), 00717 ) 00718 ); 00719 } 00720 00721 public function needsToken() { 00722 return true; 00723 } 00724 00725 public function getTokenSalt() { 00726 return ''; 00727 } 00728 00729 public function getExamples() { 00730 return array( 00731 'api.php?action=upload&filename=Wiki.png&url=http%3A//upload.wikimedia.org/wikipedia/en/b/bc/Wiki.png' 00732 => 'Upload from a URL', 00733 'api.php?action=upload&filename=Wiki.png&filekey=filekey&ignorewarnings=1' 00734 => 'Complete an upload that failed due to warnings', 00735 ); 00736 } 00737 00738 public function getHelpUrls() { 00739 return 'https://www.mediawiki.org/wiki/API:Upload'; 00740 } 00741 00742 public function getVersion() { 00743 return __CLASS__ . ': $Id$'; 00744 } 00745 }