* New UploadFromUrlJob class to handle Upload-by-Copy
authorMark A. Hershberger <mah@users.mediawiki.org>
Sat, 17 Apr 2010 02:43:13 +0000 (02:43 +0000)
committerMark A. Hershberger <mah@users.mediawiki.org>
Sat, 17 Apr 2010 02:43:13 +0000 (02:43 +0000)
* Define variable for ApiUserrights.php that wasn't defined before.
* Add convertVerifyErrorToStatus and getVerificationErrorCode to
  UploadBase to translate error consts since UploadFromUrl will
  need a message to display to end-users.
* refactor mime-checking out of UploadBase::verifyFile into
  UploadBase::verifyMimeType
* Make UploadBase::verifyFile always return arrays for errors
* Use HttpFunctions instead of custom curl handler for async downloading
* TODO: Need a way to feed errors back to the requestor
* TODO: Need to add watchlist param handling and warnings checks.

12 files changed:
includes/AutoLoader.php
includes/DefaultSettings.php
includes/UploadFromUrlJob.php [new file with mode: 0644]
includes/api/ApiBase.php
includes/api/ApiUpload.php
includes/api/ApiUserrights.php
includes/upload/UploadBase.php
includes/upload/UploadFromUrl.php
languages/messages/MessagesEn.php
maintenance/language/messages.inc
maintenance/tests/MediaWikiParserTest.php
maintenance/tests/UploadFromUrlTest.php [new file with mode: 0644]

index a63a450..d547e58 100644 (file)
@@ -237,6 +237,7 @@ $wgAutoloadLocalClasses = array(
        'UploadFromStash' => 'includes/upload/UploadFromStash.php',
        'UploadFromFile' => 'includes/upload/UploadFromFile.php',
        'UploadFromUrl' => 'includes/upload/UploadFromUrl.php',
+       'UploadFromUrlJob' => 'includes/UploadFromUrlJob.php',
        'User' => 'includes/User.php',
        'UserArray' => 'includes/UserArray.php',
        'UserArrayFromResult' => 'includes/UserArray.php',
index f93d330..6ffe45c 100644 (file)
@@ -1935,6 +1935,7 @@ $wgJobClasses = array(
        'sendMail' => 'EmaillingJob',
        'enotifNotify' => 'EnotifNotifyJob',
        'fixDoubleRedirect' => 'DoubleRedirectJob',
+       'uploadFromUrl' => 'UploadFromUrlJob',
 );
 
 /**
diff --git a/includes/UploadFromUrlJob.php b/includes/UploadFromUrlJob.php
new file mode 100644 (file)
index 0000000..7e1d64a
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+
+/**
+ * Job for email notification mails
+ *
+ * @ingroup JobQueue
+ */
+class UploadFromUrlJob extends Job {
+
+       public function __construct( $title, $params, $id = 0 ) {
+               parent::__construct( 'uploadFromUrl', $title, $params, $id );
+       }
+
+       public function run() {
+               global $wgUser;
+
+               if ( $this->params['userID'] ) {
+                       $wgUser = User::newFromId( $this->params['userID'] );
+               } else {
+                       $wgUser = new User;
+               }
+               $wgUser->mEffectiveGroups[] = 'sysop';
+               $wgUser->mRights = null;
+
+               $upload = new UploadFromUrl();
+               $upload->initializeFromJob( $this );
+
+               return $upload->doUpload();
+       }
+}
index 1d31f6f..f195abd 100644 (file)
@@ -989,6 +989,7 @@ abstract class ApiBase {
                'invalid-session-key' => array( 'code' => 'invalid-session-key', 'info' => 'Not a valid session key' ),
                'nouploadmodule' => array( 'code' => 'nouploadmodule', 'info' => 'No upload module set' ),
                'uploaddisabled' => array( 'code' => 'uploaddisabled', 'info' => 'Uploads are not enabled.  Make sure $wgEnableUploads is set to true in LocalSettings.php and the PHP ini setting file_uploads is true' ),
+               'copyuploaddisabled' => array( 'code' => 'copyuploaddisabled', 'info' => 'Uploads by URL is not enabled.  Make sure $wgAllowCopyUploads is set to true in LocalSettings.php.' ),
        );
 
        /**
index f2c5269..d98965f 100644 (file)
@@ -83,7 +83,7 @@ class ApiUpload extends ApiBase {
                        } elseif ( isset( $this->mParams['url'] ) ) {
                                // make sure upload by URL is enabled:
                                if ( !$wgAllowCopyUploads ) {
-                                       $this->dieUsageMsg( array( 'uploaddisabled' ) );
+                                       $this->dieUsageMsg( array( 'copyuploaddisabled' ) );
                                }
 
                                // make sure the current user can upload
@@ -91,14 +91,12 @@ class ApiUpload extends ApiBase {
                                        $this->dieUsageMsg( array( 'badaccess-groups' ) );
                                }
 
-                               $this->mUpload = new UploadFromUrl();
-                               $this->mUpload->initialize( $this->mParams['filename'],
-                                               $this->mParams['url'] );
+                               $this->mUpload = new UploadFromUrl;
+                               $this->mUpload->initialize( $this->mParams['filename'], $this->mParams['url'],
+                                                                                       $this->mParams['comment'] );
 
-                               $status = $this->mUpload->fetchFile();
-                               if ( !$status->isOK() ) {
-                                       $this->dieUsage( $status->getWikiText(),  'fetchfileerror' );
-                               }
+                               $this->getResult()->addValue( null, $this->getModuleName(), Status::newGood() );
+                               return;
                        }
                } else {
                        $this->dieUsageMsg( array( 'missingparam', 'filename' ) );
index 6eacfd0..dfb5f19 100644 (file)
@@ -43,6 +43,7 @@ class ApiUserrights extends ApiBase {
 
                $user = $this->getUser();
 
+               $form = new UserrightsPage;
                $r['user'] = $user->getName();
                list( $r['added'], $r['removed'] ) =
                        $form->doSaveUserGroups(
index 73031f0..ea40288 100644 (file)
@@ -29,6 +29,7 @@ abstract class UploadBase {
        const FILETYPE_MISSING = 8;
        const FILETYPE_BADTYPE = 9;
        const VERIFICATION_ERROR = 10;
+
        # HOOK_ABORTED is the new name of UPLOAD_VERIFICATION_ERROR
        const UPLOAD_VERIFICATION_ERROR = 11;
        const HOOK_ABORTED = 11;
@@ -41,6 +42,24 @@ abstract class UploadBase {
                return self::SESSION_KEYNAME;
        }
 
+       public function getVerificationErrorCode( $error ) {
+               $code_to_status = array(self::EMPTY_FILE => 'empty-file',
+                                                               self::FILE_TOO_LARGE => 'file-too-large',
+                                                               self::FILETYPE_MISSING => 'filetype-missing',
+                                                               self::FILETYPE_BADTYPE => 'filetype-banned',
+                                                               self::MIN_LENGTH_PARTNAME => 'filename-tooshort',
+                                                               self::ILLEGAL_FILENAME => 'illegal-filename',
+                                                               self::OVERWRITE_EXISTING_FILE => 'overwrite',
+                                                               self::VERIFICATION_ERROR => 'verification-error',
+                                                               self::HOOK_ABORTED =>  'hookaborted',
+               );
+               if( isset( $code_to_status[$error] ) ) {
+                       return $code_to_status[$error];
+               }
+
+               return 'unknown-error';
+       }
+
        /**
         * Returns true if uploads are enabled.
         * Can be override by subclasses.
@@ -201,7 +220,7 @@ abstract class UploadBase {
                if( $this->isEmptyFile() ) {
                        return array( 'status' => self::EMPTY_FILE );
                }
-               
+
                /**
                 * Honor $wgMaxUploadSize
                 */
@@ -217,9 +236,6 @@ abstract class UploadBase {
                 */
                $verification = $this->verifyFile();
                if( $verification !== true ) {
-                       if( !is_array( $verification ) ) {
-                               $verification = array( $verification );
-                       }
                        return array(
                                'status' => self::VERIFICATION_ERROR,
                                'details' => $verification
@@ -278,19 +294,12 @@ abstract class UploadBase {
        }
 
        /**
-        * Verifies that it's ok to include the uploaded file
-        *
-        * @return mixed true of the file is verified, a string or array otherwise.
+        * Verify the mime type
+        * @param $magic MagicMime object
+        * @param $mime string representing the mime
+        * @return mixed true if the file is verified, an array otherwise
         */
-       protected function verifyFile() {
-               $this->mFileProps = File::getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
-               $this->checkMacBinary();
-
-               # magically determine mime type
-               $magic = MimeMagic::singleton();
-               $mime = $magic->guessMimeType( $this->mTempPath, false );
-
-               # check mime type, if desired
+       protected function verifyMimeType( $magic, $mime ) {
                global $wgVerifyMimeType;
                if ( $wgVerifyMimeType ) {
                        wfDebug ( "\n\nmime: <$mime> extension: <{$this->mFinalExtension}>\n\n");
@@ -316,13 +325,35 @@ abstract class UploadBase {
                        }
                }
 
+               return true;
+       }
+
+       /**
+        * Verifies that it's ok to include the uploaded file
+        *
+        * @return mixed true of the file is verified, array otherwise.
+        */
+       protected function verifyFile() {
+               $this->mFileProps = File::getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
+               $this->checkMacBinary();
+
+               # magically determine mime type
+               $magic = MimeMagic::singleton();
+               $mime = $magic->guessMimeType( $this->mTempPath, false );
+
+               # check mime type, if desired
+               $status = $this->verifyMimeType( $magic, $mime );
+               if ( $status !== true ) {
+                       return $status;
+               }
+
                # check for htmlish code and javascript
                if( self::detectScript( $this->mTempPath, $mime, $this->mFinalExtension ) ) {
-                       return 'uploadscripted';
+                       return array( 'uploadscripted' );
                }
                if( $this->mFinalExtension == 'svg' || $mime == 'image/svg+xml' ) {
                        if( self::detectScriptInSvg( $this->mTempPath ) ) {
-                               return 'uploadscripted';
+                               return array( 'uploadscripted' );
                        }
                }
 
@@ -354,7 +385,6 @@ abstract class UploadBase {
                        return true;
                }
                $permErrors = $nt->getUserPermissionsErrors( 'edit', $user );
-               $permErrorsUpload = $nt->getUserPermissionsErrors( 'upload', $user );
                $permErrorsCreate = ( $nt->exists() ? array() : $nt->getUserPermissionsErrors( 'create', $user ) );
                if( $permErrors || $permErrorsUpload || $permErrorsCreate ) {
                        $permErrors = array_merge( $permErrors, wfArrayDiff2( $permErrorsUpload, $permErrors ) );
@@ -440,7 +470,8 @@ abstract class UploadBase {
         * @return mixed Status indicating the whether the upload succeeded.
         */
        public function performUpload( $comment, $pageText, $watch, $user ) {
-               wfDebug( "\n\n\performUpload: sum:" . $comment . ' c: ' . $pageText . ' w:' . $watch );
+               wfDebug( "\n\n\performUpload: sum: " . $comment . ' c: ' . $pageText .
+                       ' w: ' . $watch );
                $status = $this->getLocalFile()->upload( $this->mTempPath, $comment, $pageText,
                        File::DELETE_SOURCE, $this->mFileProps, false, $user );
 
@@ -575,7 +606,8 @@ abstract class UploadBase {
        }
 
        /**
-        * Generate a random session key from stash in cases where we want to start an upload without much information
+        * Generate a random session key from stash in cases where we want
+        * to start an upload without much information
         */
        protected function getSessionKey() {
                $key = mt_rand( 0, 0x7fffffff );
@@ -850,7 +882,8 @@ abstract class UploadBase {
 
                if ( !$wgAntivirusSetup[$wgAntivirus] ) {
                        wfDebug( __METHOD__ . ": unknown virus scanner: $wgAntivirus\n" );
-                       $wgOut->wrapWikiMsg( "<div class=\"error\">\n$1</div>", array( 'virus-badscanner', $wgAntivirus ) );
+                       $wgOut->wrapWikiMsg( "<div class=\"error\">\n$1</div>",
+                               array( 'virus-badscanner', $wgAntivirus ) );
                        return wfMsg( 'virus-unknownscanner' ) . " $wgAntivirus";
                }
 
@@ -1133,4 +1166,9 @@ abstract class UploadBase {
                return ApiQueryImageInfo::getInfo( $file, array_flip( $imParam ), $result );
        }
 
+       public function convertVerifyErrorToStatus( $error ) {
+               $args = func_get_args();
+               array_shift($args);
+               return Status::newFatal( $this->getVerificationErrorCode( $error ), $args );
+       }
 }
index af97d9f..a904cf2 100644 (file)
@@ -10,6 +10,7 @@
  */
 class UploadFromUrl extends UploadBase {
        protected $mTempDownloadPath;
+       protected $comment, $watchList;
 
        /**
         * Checks if the user is allowed to use the upload-by-URL feature. If the
@@ -32,13 +33,53 @@ class UploadFromUrl extends UploadBase {
        /**
         * Entry point for API upload
         */
-       public function initialize( $name, $url, $na, $nb = false ) {
-               global $wgTmpDirectory;
+       public function initialize( $name, $url, $comment, $watchlist ) {
+               global $wgUser;
 
-               $localFile = tempnam( $wgTmpDirectory, 'WEBUPLOAD' );
-               $this->initializePathInfo( $name, $localFile, 0, true );
+               if( !Http::isValidURI( $url ) ) {
+                       return Status::newFatal( 'http-invalid-url' );
+               }
+               $params = array(
+                       "userName" => $wgUser->getName(),
+                       "userID" => $wgUser->getID(),
+                       "url" => trim( $url ),
+                       "timestamp" => wfTimestampNow(),
+                       "comment" => $comment,
+                       "watchlist" => $watchlist);
+
+               $title = Title::newFromText( $name );
+               /* // Check whether the user has the appropriate permissions to upload anyway */
+               /* $permission = $this->isAllowed( $wgUser ); */
+
+               /* if ( $permission !== true ) { */
+               /*      if ( !$wgUser->isLoggedIn() ) { */
+               /*              return Status::newFatal( 'uploadnologintext' ); */
+               /*      } else { */
+               /*              return Status::newFatal( 'badaccess-groups' ); */
+               /*      } */
+               /* } */
+
+               /* $permErrors = $this->verifyPermissions( $wgUser ); */
+               /* if ( $permErrors !== true ) { */
+               /*      return Status::newFatal( 'badaccess-groups' ); */
+               /* } */
 
-               $this->mUrl = trim( $url );
+
+               $job = new UploadFromUrlJob( $title, $params );
+               $job->insert();
+       }
+
+       /**
+        * Initialize a queued download
+        * @param $job Job
+        */
+       public function initializeFromJob( $job ) {
+               $this->mUrl = $job->params['url'];
+               $this->mTempPath = tempnam( $wgTmpDirectory, 'COPYUPLOAD' );
+               $this->mDesiredDestName = $job->title;
+               $this->comment = $job->params['comment'];
+               $this->watchList = $job->params['watchlist'];
+               $this->getTitle();
        }
 
        /**
@@ -66,60 +107,63 @@ class UploadFromUrl extends UploadBase {
                return self::isValidUrl( $request->getVal( 'wpUploadFileURL' ) );
        }
 
-       public static function isValidUrl( $url ) {
-               // Only allow HTTP or FTP for now
-               return (bool)preg_match( '!^(http://|ftp://)!', $url );
+       private function saveTempFile( $req ) {
+               $filename = tempnam( wfTempDir(), 'URL' );
+               if ( $filename === false ) {
+                       return Status::newFatal( 'tmp-create-error' );
+               }
+               if ( file_put_contents( $filename, $req->getContent() ) === false ) {
+                       return Status::newFatal( 'tmp-write-error' );
+               }
+
+               $this->mTempPath = $filename;
+               $this->mFileSize = filesize( $filename );
+
+               return Status::newGood();
        }
 
-       /**
-        * Do the real fetching stuff
-        */
-       function fetchFile() {
-               if( !self::isValidUrl( $this->mUrl ) ) {
-                       return Status::newFatal( 'upload-proto-error' );
+       public function doUpload() {
+               global $wgUser;
+
+               $req = HttpRequest::factory($this->mUrl);
+               $status = $req->execute();
+
+               if( !$status->isOk() ) {
+                       return $status;
                }
 
-               # Open temporary file
-               $this->mCurlDestHandle = @fopen( $this->mTempPath, "wb" );
-               if( $this->mCurlDestHandle === false ) {
-                       # Could not open temporary file to write in
-                       return Status::newFatal( 'upload-file-error' );
+               $status = $this->saveTempFile( $req );
+               $this->mRemoveTempFile = true;
+
+               if( !$status->isOk() ) {
+                       return $status;
                }
-               
-               $options = array(
-                       'method' => 'GET',
-                       'timeout' => 10,
-               );
-               $req = HttpRequest::factory( $this->mUrl, $options );
-               $req->setCallback( array( $this, 'uploadCurlCallback' ) );
-               $status = $req->execute();
-               fclose( $this->mCurlDestHandle );
-               unset( $this->mCurlDestHandle );
-
-               global $wgMaxUploadSize;
-               if ( $this->mFileSize > $wgMaxUploadSize ) {
-                       # Just return an ok, so that the regular verifications can handle 
-                       # the file-too-large error
-                       return Status::newGood();
+
+               $v = $this->verifyUpload();
+               if( $v['status'] !== UploadBase::OK ) {
+                       return $this->convertVerifyErrorToStatus( $v['status'], $v['details'] );
                }
 
-               return $status;
-       }
+               // This has to come from API
+               /* $warnings = $this->checkForWarnings(); */
+               /* if( isset($warnings) ) return $warnings; */
 
-       /**
-        * Callback function for CURL-based web transfer
-        * Write data to file unless we've passed the length limit;
-        * if so, abort immediately.
-        * @access private
-        */
-       function uploadCurlCallback( $ch, $data ) {
-               global $wgMaxUploadSize;
-               $length = strlen( $data );
-               $this->mFileSize += $length;
-               if( $this->mFileSize > $wgMaxUploadSize ) {
-                       return 0;
+               // Use comment as initial page text by default
+               if ( is_null( $this->mParams['text'] ) ) {
+                       $this->mParams['text'] = $this->mParams['comment'];
+               }
+
+               $file = $this->getLocalFile();
+               // This comes from ApiBase
+               /* $watch = $this->getWatchlistValue( $this->mParams['watchlist'], $file->getTitle() ); */
+
+               if ( !$status->isGood() ) {
+                       return $status;
                }
-               fwrite( $this->mCurlDestHandle, $data );
-               return $length;
+
+               $status = $this->getLocalFile()->upload( $this->mTempPath, $this->comment,
+                       $this->pageText, File::DELETE_SOURCE, $this->mFileProps, false, $wgUser );
+
+               return $status;
        }
 }
index 7c2a1db..9b6eb43 100644 (file)
@@ -2055,6 +2055,17 @@ Preferred {{PLURAL:\$3|file type is|file types are}} \$2.",
 'filetype-banned-type'        => "'''\".\$1\"''' is not a permitted file type.
 Permitted {{PLURAL:\$3|file type is|file types are}} \$2.",
 'filetype-missing'            => 'The file has no extension (like ".jpg").',
+'empty-file'                  => 'The file you submitted was empty',
+'file-too-large'              => 'The file you submitted was too large',
+'filename-tooshort'           => 'The filename is too short',
+'filetype-banned'             => 'This type of file is banned',
+'verification-error'          => 'This file did not pass file verification',
+'hookaborted'                 => 'The modification you tried to make was aborted by an extension hook',
+'illegal-filename'            => 'The filename is not allowed',
+'overwrite'                   => 'Overwriting an existing file is not allowed',
+'unknown-error'               => 'An unknown error occured',
+'tmp-create-error'            => 'Couldn\'t create temporary file',
+'tmp-write-error'             => 'Error writing temporary file',
 'large-file'                  => 'It is recommended that files are no larger than $1;
 this file is $2.',
 'largefileserver'             => 'This file is bigger than the server is configured to allow.',
@@ -2094,6 +2105,7 @@ You should check that file's deletion history before proceeding to re-upload it.
 'uploadedimage'               => 'uploaded "[[$1]]"',
 'overwroteimage'              => 'uploaded a new version of "[[$1]]"',
 'uploaddisabled'              => 'Uploads disabled',
+'copyuploaddisabled'          => 'Upload by URL disabled',
 'uploaddisabledtext'          => 'File uploads are disabled.',
 'php-uploaddisabledtext'      => 'File uploads are disabled in PHP.
 Please check the file_uploads setting.',
index de097b2..4fe9433 100644 (file)
@@ -1233,6 +1233,7 @@ $wgMessageStructure = array(
                'uploadedimage',
                'overwroteimage',
                'uploaddisabled',
+               'copyuploaddisabled',
                'uploaddisabledtext',
                'php-uploaddisabledtext',
                'uploadscripted',
index 247d840..f00779f 100644 (file)
@@ -27,6 +27,7 @@ class MediaWikiParserTestSuite extends PHPUnit_Framework_TestSuite {
                $tables[] = 'filearchive';
                $tables[] = 'logging';
                $tables[] = 'updatelog';
+               $tables[] = 'iwlinks';
                return true;
        }
 
diff --git a/maintenance/tests/UploadFromUrlTest.php b/maintenance/tests/UploadFromUrlTest.php
new file mode 100644 (file)
index 0000000..77fd099
--- /dev/null
@@ -0,0 +1,191 @@
+<?php
+
+global $IP;
+require_once( "ApiSetup.php" );
+require_once( dirname( dirname( __FILE__ ) ) . "/deleteArchivedFiles.inc" );
+require_once( dirname( dirname( __FILE__ ) ) . "/deleteArchivedRevisions.inc" );
+
+class nullClass {
+       public function handleOutput(){}
+       public function purgeRedundantText(){}
+}
+
+class UploadFromUrlTest extends ApiSetup {
+
+       function setUp() {
+               global $wgEnableUploads, $wgLocalFileRepo;
+
+               $wgEnableUploads = true;
+               parent::setup();
+               $wgLocalFileRepo = array(
+                       'class' => 'LocalRepo',
+                       'name' => 'local',
+                       'directory' => 'test-repo',
+                       'url' => 'http://example.com/images',
+                       'hashLevels' => 2,
+                       'transformVia404' => false,
+               );
+
+               ini_set( 'log_errors', 1 );
+               ini_set( 'error_reporting', 1 );
+               ini_set( 'display_errors', 1 );
+       }
+
+       function doApiRequest( $params, $data = null ) {
+               $session = isset( $data[2] ) ? $data[2] : array();
+               $_SESSION = $session;
+
+               $req = new FauxRequest( $params, true, $session );
+               $module = new ApiMain( $req, true );
+               $module->execute();
+
+               return array( $module->getResultData(), $req, $_SESSION );
+       }
+
+       function testClearQueue() {
+               while ( $job = Job::pop() ) {}
+               $this->assertFalse($job);
+       }
+
+       function testLogin() {
+               $data = $this->doApiRequest( array(
+                       'action' => 'login',
+                       'lgname' => self::$userName,
+                       'lgpassword' => self::$passWord ) );
+               $this->assertArrayHasKey( "login", $data[0] );
+               $this->assertArrayHasKey( "result", $data[0]['login'] );
+               $this->assertEquals( "NeedToken", $data[0]['login']['result'] );
+               $token = $data[0]['login']['token'];
+
+               $data = $this->doApiRequest( array(
+                       'action' => 'login',
+                       "lgtoken" => $token,
+                       "lgname" => self::$userName,
+                       "lgpassword" => self::$passWord ) );
+
+               $this->assertArrayHasKey( "login", $data[0] );
+               $this->assertArrayHasKey( "result", $data[0]['login'] );
+               $this->assertEquals( "Success", $data[0]['login']['result'] );
+               $this->assertArrayHasKey( 'lgtoken', $data[0]['login'] );
+
+               return $data;
+       }
+
+       /**
+        * @depends testLogin
+        */
+       function testSetupUrlDownload( $data ) {
+               global $wgUser;
+               $wgUser = User::newFromName( self::$userName );
+               $wgUser->load();
+               $data[2]['wsEditToken'] = $data[2]['wsToken'];
+               $token = md5( $data[2]['wsToken'] ) . EDIT_TOKEN_SUFFIX;
+               $exception = false;
+
+               try {
+                       $this->doApiRequest( array(
+                               'action' => 'upload',
+                       ), $data );
+               } catch ( UsageException $e ) {
+                       $exception = true;
+                       $this->assertEquals( "The token parameter must be set", $e->getMessage() );
+               }
+               $this->assertTrue( $exception, "Got exception" );
+
+               $exception = false;
+               try {
+                       $this->doApiRequest( array(
+                               'action' => 'upload',
+                               'token' => $token,
+                       ), $data );
+               } catch ( UsageException $e ) {
+                       $exception = true;
+                       $this->assertEquals( "One of the parameters sessionkey, file, url is required",
+                               $e->getMessage() );
+               }
+               $this->assertTrue( $exception, "Got exception" );
+
+               $exception = false;
+               try {
+                       $this->doApiRequest( array(
+                               'action' => 'upload',
+                               'url' => 'http://www.example.com/test.png',
+                               'token' => $token,
+                       ), $data );
+               } catch ( UsageException $e ) {
+                       $exception = true;
+                       $this->assertEquals( "The filename parameter must be set", $e->getMessage() );
+               }
+               $this->assertTrue( $exception, "Got exception" );
+
+               $wgUser->removeGroup('sysop');
+               $exception = false;
+               try {
+                       $this->doApiRequest( array(
+                               'action' => 'upload',
+                               'url' => 'http://www.example.com/test.png',
+                               'filename' => 'Test.png',
+                               'token' => $token,
+                       ), $data );
+               } catch ( UsageException $e ) {
+                       $exception = true;
+                       $this->assertEquals( "Permission denied", $e->getMessage() );
+               }
+               $this->assertTrue( $exception, "Got exception" );
+
+               $wgUser->addGroup('*');
+               $wgUser->addGroup('sysop');
+               $exception = false;
+               $data = $this->doApiRequest( array(
+                       'action' => 'upload',
+                       'url' => 'http://bits.wikimedia.org/skins-1.5/common/images/poweredby_mediawiki_88x31.png',
+                       'filename' => 'Test.png',
+                       'token' => $token,
+               ), $data );
+
+               $this->assertThat( $data[0]['upload'], $this->isInstanceOf( 'Status' ),
+                       "Got Status Object" );
+               $this->assertTrue( $data[0]['upload']->isOk(), 'Job added');
+
+               $job = Job::pop();
+               $this->assertThat( $job, $this->isInstanceOf( 'UploadFromUrlJob' ),
+                       "Got Job Object" );
+
+               $job = Job::pop_type( 'upload' );
+               $this->assertFalse( $job );
+       }
+
+       /**
+        * @depends testLogin
+        */
+       function testDoDownload( $data ) {
+               global $wgUser;
+               $data[2]['wsEditToken'] = $data[2]['wsToken'];
+               $token = md5( $data[2]['wsToken'] ) . EDIT_TOKEN_SUFFIX;
+
+               $wgUser->addGroup('users');
+               $data = $this->doApiRequest( array(
+                       'action' => 'upload',
+                       'filename' => 'Test.png',
+                       'url' => 'http://bits.wikimedia.org/skins-1.5/common/images/poweredby_mediawiki_88x31.png',
+                       'token' => $token,
+               ), $data );
+
+               $job = Job::pop();
+               $this->assertEquals( 'UploadFromUrlJob', get_class($job) );
+
+               $status = $job->run();
+               $this->assertTrue( $status->isOk() );
+
+               return $data;
+       }
+
+       /**
+        * @depends testDoDownload
+        */
+       function testVerifyDownload( $data ) {
+               $t = Title::newFromText("Test.png", NS_FILE);
+
+               $this->assertTrue($t->exists());
+       }
+}