* 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.
'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',
'sendMail' => 'EmaillingJob',
'enotifNotify' => 'EnotifNotifyJob',
'fixDoubleRedirect' => 'DoubleRedirectJob',
+ 'uploadFromUrl' => 'UploadFromUrlJob',
);
/**
--- /dev/null
+<?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();
+ }
+}
'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.' ),
);
/**
} 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
$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' ) );
$user = $this->getUser();
+ $form = new UserrightsPage;
$r['user'] = $user->getName();
list( $r['added'], $r['removed'] ) =
$form->doSaveUserGroups(
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;
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.
if( $this->isEmptyFile() ) {
return array( 'status' => self::EMPTY_FILE );
}
-
+
/**
* Honor $wgMaxUploadSize
*/
*/
$verification = $this->verifyFile();
if( $verification !== true ) {
- if( !is_array( $verification ) ) {
- $verification = array( $verification );
- }
return array(
'status' => self::VERIFICATION_ERROR,
'details' => $verification
}
/**
- * 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");
}
}
+ 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' );
}
}
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 ) );
* @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 );
}
/**
- * 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 );
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";
}
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 );
+ }
}
*/
class UploadFromUrl extends UploadBase {
protected $mTempDownloadPath;
+ protected $comment, $watchList;
/**
* Checks if the user is allowed to use the upload-by-URL feature. If the
/**
* 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();
}
/**
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;
}
}
'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.',
'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.',
'uploadedimage',
'overwroteimage',
'uploaddisabled',
+ 'copyuploaddisabled',
'uploaddisabledtext',
'php-uploaddisabledtext',
'uploadscripted',
$tables[] = 'filearchive';
$tables[] = 'logging';
$tables[] = 'updatelog';
+ $tables[] = 'iwlinks';
return true;
}
--- /dev/null
+<?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());
+ }
+}