Tweaked the UploadWizard to work properly with the new backend code, updated tests
$result->addValue( array( 'query', $this->getModuleName() ), null, $imageInfo );
$result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), $modulePrefix );
}
-
+ //TODO: update exception handling here to understand current getFile exceptions
} catch ( UploadStashNotAvailableException $e ) {
$this->dieUsage( "Session not available: " . $e->getMessage(), "nosession" );
} catch ( UploadStashFileNotFoundException $e ) {
$request = $this->getMain()->getRequest();
// Add the uploaded file to the params array
$this->mParams['file'] = $request->getFileName( 'file' );
+
+ // Copy the session key to the file key, for backward compatibility.
+ if( !$this->mParams['filekey'] && $this->mParams['sessionkey'] ) {
+ $this->mParams['filekey'] = $this->mParams['sessionkey'];
+ }
// Select an upload module
if ( !$this->selectUploadModule() ) {
// in case the warnings can be fixed with some further user action, let's stash this upload
// and return a key they can use to restart it
try {
- $result['sessionkey'] = $this->performStash();
+ $result['filekey'] = $this->performStash();
+ $result['sessionkey'] = $result['filekey']; // backwards compatibility
} catch ( MWException $e ) {
$result['warnings']['stashfailed'] = $e->getMessage();
}
// In this case, a failure to stash ought to be fatal
try {
$result['result'] = 'Success';
- $result['sessionkey'] = $this->performStash();
+ $result['filekey'] = $this->performStash();
+ $result['sessionkey'] = $result['filekey']; // backwards compatibility
} catch ( MWException $e ) {
$this->dieUsage( $e->getMessage(), 'stashfailed' );
}
}
/**
- * Stash the file and return the session key
+ * Stash the file and return the file key
* Also re-raises exceptions with slightly more informative message strings (useful for API)
* @throws MWException
- * @return String session key
+ * @return String file key
*/
function performStash() {
try {
- $sessionKey = $this->mUpload->stashSessionFile()->getSessionKey();
+ $fileKey = $this->mUpload->stashFile()->getFileKey();
} catch ( MWException $e ) {
- throw new MWException( 'Stashing temporary file failed: ' . get_class( $e ) . ' ' . $e->getMessage() );
+ $message = 'Stashing temporary file failed: ' . get_class( $e ) . ' ' . $e->getMessage();
+ wfDebug( __METHOD__ . ' ' . $message . "\n");
+ throw new MWException( $message );
}
- return $sessionKey;
+ return $fileKey;
}
/**
*/
function dieRecoverableError( $error, $parameter, $data = array() ) {
try {
- $data['sessionkey'] = $this->performStash();
+ $data['filekey'] = $this->performStash();
+ $data['sessionkey'] = $data['filekey'];
} catch ( MWException $e ) {
$data['stashfailed'] = $e->getMessage();
}
// One and only one of the following parameters is needed
$this->requireOnlyOneParameter( $this->mParams,
- 'sessionkey', 'file', 'url', 'statuskey' );
+ 'filekey', 'file', 'url', 'statuskey' );
if ( $this->mParams['statuskey'] ) {
$this->checkAsyncDownloadEnabled();
$this->dieUsageMsg( array( 'missingparam', 'filename' ) );
}
- if ( $this->mParams['sessionkey'] ) {
+ if ( $this->mParams['filekey'] ) {
// Upload stashed in a previous request
- $sessionData = $request->getSessionData( UploadBase::getSessionKeyName() );
- if ( !UploadFromStash::isValidSessionKey( $this->mParams['sessionkey'], $sessionData ) ) {
- $this->dieUsageMsg( 'invalid-session-key' );
+ if ( !UploadFromStash::isValidKey( $this->mParams['filekey'] ) ) {
+ $this->dieUsageMsg( 'invalid-file-key' );
}
$this->mUpload = new UploadFromStash();
- $this->mUpload->initialize( $this->mParams['filename'],
- $this->mParams['sessionkey'],
- $sessionData[$this->mParams['sessionkey']] );
+ $this->mUpload->initialize( $this->mParams['filekey'], $this->mParams['filename'] );
} elseif ( isset( $this->mParams['file'] ) ) {
$this->mUpload = new UploadFromFile();
'ignorewarnings' => false,
'file' => null,
'url' => null,
- 'sessionkey' => null,
+ 'filekey' => null,
+ 'sessionkey' => array(
+ ApiBase::PARAM_DFLT => null,
+ ApiBase::PARAM_DEPRECATED => true,
+ ),
'stash' => false,
'asyncdownload' => false,
'ignorewarnings' => 'Ignore any warnings',
'file' => 'File contents',
'url' => 'Url to fetch the file from',
- 'sessionkey' => 'Session key that identifies a previous upload that was stashed temporarily.',
+ 'filekey' => 'Key that identifies a previous upload that was stashed temporarily.',
+ 'sessionkey' => 'Same as filekey, maintained for backward compatibility.',
'stash' => 'If set, the server will not add the file to the repository and stash it temporarily.',
'asyncdownload' => 'Make fetching a URL asynchronous',
'leavemessage' => 'If asyncdownload is used, leave a message on the user talk page if finished',
- 'statuskey' => 'Fetch the upload status for this session key',
+ 'statuskey' => 'Fetch the upload status for this file key',
);
return $params;
'Upload a file, or get the status of pending uploads. Several methods are available:',
' * Upload file contents directly, using the "file" parameter',
' * Have the MediaWiki server fetch a file from a URL, using the "url" parameter',
- ' * Complete an earlier upload that failed due to warnings, using the "sessionkey" parameter',
+ ' * Complete an earlier upload that failed due to warnings, using the "filekey" parameter',
'Note that the HTTP POST must be done as a file upload (i.e. using multipart/form-data) when',
- 'sending the "file". Note also that queries using session keys must be',
- 'done in the same login session as the query that originally returned the key (i.e. do not',
- 'log out and then log back in). Also you must get and send an edit token before doing any upload stuff'
+ 'sending the "file". Also you must get and send an edit token before doing any upload stuff'
);
}
public function getPossibleErrors() {
return array_merge( parent::getPossibleErrors(),
- $this->getRequireOnlyOneParameterErrorMessages( array( 'sessionkey', 'file', 'url', 'statuskey' ) ),
+ $this->getRequireOnlyOneParameterErrorMessages( array( 'filekey', 'file', 'url', 'statuskey' ) ),
array(
array( 'uploaddisabled' ),
- array( 'invalid-session-key' ),
+ array( 'invalid-file-key' ),
array( 'uploaddisabled' ),
array( 'mustbeloggedin', 'upload' ),
array( 'badaccess-groups' ),
'Upload from a URL:',
' api.php?action=upload&filename=Wiki.png&url=http%3A//upload.wikimedia.org/wikipedia/en/b/bc/Wiki.png',
'Complete an upload that failed due to warnings:',
- ' api.php?action=upload&filename=Wiki.png&sessionkey=sessionkey&ignorewarnings=1',
+ ' api.php?action=upload&filename=Wiki.png&filekey=filekey&ignorewarnings=1',
);
}
// 1.19
array( 'addTable', 'config', 'patch-config.sql' ),
+ array( 'addTable', 'uploadstash', 'patch-uploadstash.sql' ),
);
}
// 1.19
array( 'addTable', 'config', 'patch-config.sql' ),
+ array( 'addTable', 'uploadstash', 'patch-uploadstash.sql' ),
);
}
const FILE_TOO_LARGE = 12;
const WINDOWS_NONASCII_FILENAME = 13;
- const SESSION_VERSION = 2;
- const SESSION_KEYNAME = 'wsUploadData';
-
- public static function getSessionKeyname() {
- return self::SESSION_KEYNAME;
- }
-
public function getVerificationErrorCode( $error ) {
$code_to_status = array(self::EMPTY_FILE => 'empty-file',
self::FILE_TOO_LARGE => 'file-too-large',
* by design) then we may want to stash the file temporarily, get more information, and publish the file later.
*
* This method will stash a file in a temporary directory for later processing, and save the necessary descriptive info
- * into the user's session.
- * This method returns the file object, which also has a 'sessionKey' property which can be passed through a form or
+ * into the database.
+ * This method returns the file object, which also has a 'fileKey' property which can be passed through a form or
* API request to find this stashed file again.
*
- * @param $key String: (optional) the session key used to find the file info again. If not supplied, a key will be autogenerated.
+ * @param $key String: (optional) the file key used to find the file info again. If not supplied, a key will be autogenerated.
* @return UploadStashFile stashed file
*/
- public function stashSessionFile( $key = null ) {
+ public function stashFile( $key = null ) {
+ // was stashSessionFile
$stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash();
- $data = array(
- 'mFileProps' => $this->mFileProps,
- 'mSourceType' => $this->getSourceType(),
- );
- $file = $stash->stashFile( $this->mTempPath, $data, $key );
+
+ $file = $stash->stashFile( $this->mTempPath, $this->getSourceType(), $key );
$this->mLocalFile = $file;
return $file;
}
/**
- * Stash a file in a temporary directory, returning a key which can be used to find the file again. See stashSessionFile().
+ * Stash a file in a temporary directory, returning a key which can be used to find the file again. See stashFile().
+ *
+ * @param $key String: (optional) the file key used to find the file info again. If not supplied, a key will be autogenerated.
+ * @return String: file key
+ */
+ public function stashFileGetKey( $key = null ) {
+ return $this->stashFile( $key )->getFileKey();
+ }
+
+ /**
+ * alias for stashFileGetKey, for backwards compatibility
*
- * @param $key String: (optional) the session key used to find the file info again. If not supplied, a key will be autogenerated.
- * @return String: session key
+ * @param $key String: (optional) the file key used to find the file info again. If not supplied, a key will be autogenerated.
+ * @return String: file key
*/
public function stashSession( $key = null ) {
- return $this->stashSessionFile( $key )->getSessionKey();
+ return $this->stashFileGetKey( $key );
}
/**
*/
class UploadFromStash extends UploadBase {
+ protected $mFileKey, $mVirtualTempPath, $mFileProps, $mSourceType;
+
+ // an instance of UploadStash
+ private $stash;
+
+ //LocalFile repo
+ private $repo;
+
+ public function __construct( $stash = false, $repo = false ) {
+ if( !$this->repo ) {
+ $this->repo = RepoGroup::singleton()->getLocalRepo();
+ }
- protected $initializePathInfo, $mSessionKey, $mVirtualTempPath,
- $mFileProps, $mSourceType;
-
- public static function isValidSessionKey( $key, $sessionData ) {
- return !empty( $key ) &&
- is_array( $sessionData ) &&
- isset( $sessionData[$key] ) &&
- isset( $sessionData[$key]['version'] ) &&
- $sessionData[$key]['version'] == UploadBase::SESSION_VERSION;
+ if( !$this->stash ) {
+ $this->stash = new UploadStash( $this->repo );
+ }
+
+ return true;
+ }
+
+ public static function isValidKey( $key ) {
+ // this is checked in more detail in UploadStash
+ return preg_match( UploadStash::KEY_FORMAT_REGEX, $key );
}
/**
* @return Boolean
*/
public static function isValidRequest( $request ) {
- $sessionData = $request->getSessionData( UploadBase::SESSION_KEYNAME );
- return self::isValidSessionKey(
- $request->getText( 'wpSessionKey' ),
- $sessionData
- );
+ return self::isValidKey( $request->getText( 'wpFileKey' ) || $request->getText( 'wpSessionKey' ) );
}
- public function initialize( $name, $sessionKey, $sessionData ) {
+ public function initialize( $key, $name = 'upload_file' ) {
/**
* Confirming a temporarily stashed upload.
* We don't want path names to be forged, so we keep
* them in the session on the server and just give
* an opaque key to the user agent.
- */
-
+ */
+ $metadata = $this->stash->getMetadata( $key );
$this->initializePathInfo( $name,
- $this->getRealPath ( $sessionData['mTempPath'] ),
- $sessionData['mFileSize'],
+ $this->getRealPath ( $metadata['us_path'] ),
+ $metadata['us_size'],
false
);
- $this->mSessionKey = $sessionKey;
- $this->mVirtualTempPath = $sessionData['mTempPath'];
- $this->mFileProps = $sessionData['mFileProps'];
- $this->mSourceType = isset( $sessionData['mSourceType'] ) ?
- $sessionData['mSourceType'] : null;
+ $this->mFileKey = $key;
+ $this->mVirtualTempPath = $metadata['us_path'];
+ $this->mFileProps = $this->stash->getFileProps( $key );
+ $this->mSourceType = $metadata['us_source_type'];
}
/**
* @param $request WebRequest
*/
public function initializeFromRequest( &$request ) {
- $sessionKey = $request->getText( 'wpSessionKey' );
- $sessionData = $request->getSessionData( UploadBase::SESSION_KEYNAME );
+ $fileKey = $request->getText( 'wpFileKey' ) || $request->getText( 'wpSessionKey' );
$desiredDestName = $request->getText( 'wpDestFile' );
- if( !$desiredDestName )
- $desiredDestName = $request->getText( 'wpUploadFile' );
- return $this->initialize( $desiredDestName, $sessionKey, $sessionData[$sessionKey] );
+ if( !$desiredDestName ) {
+ $desiredDestName = $request->getText( 'wpUploadFile' ) || $request->getText( 'filename' );
+ }
+ return $this->initialize( $fileKey, $desiredDestName );
}
public function getSourceType() {
/**
* There is no need to stash the image twice
*/
- public function stashSession( $key = null ) {
- if ( !empty( $this->mSessionKey ) ) {
- return $this->mSessionKey;
+ public function stashFile( $key = null ) {
+ if ( !empty( $this->mFileKey ) ) {
+ return $this->mFileKey;
}
- return parent::stashSession();
+ return parent::stashFileGetKey();
+ }
+
+ /**
+ * Alias for stashFile
+ */
+ public function stashSession( $key = null ) {
+ return $this->stashFile( $key );
}
/**
* @return success
*/
public function unsaveUploadedFile() {
- $repo = RepoGroup::singleton()->getLocalRepo();
- $success = $repo->freeTemp( $this->mVirtualTempPath );
- return $success;
+ return $stash->removeFile( $this->mFileKey );
}
}
\ No newline at end of file
* - Several parts of MediaWiki do this in similar ways: UploadBase, UploadWizard, and FirefoggChunkedExtension
* And there are several that reimplement stashing from scratch, in idiosyncratic ways. The idea is to unify them all here.
* Mostly all of them are the same except for storing some custom fields, which we subsume into the data array.
- * - enable applications to find said files later, as long as the session or temp files haven't been purged.
+ * - enable applications to find said files later, as long as the db table or temp files haven't been purged.
* - enable the uploading user (and *ONLY* the uploading user) to access said files, and thumbnails of said files, via a URL.
- * We accomplish this by making the session serve as a URL->file mapping, on the assumption that nobody else can access
- * the session, even the uploading user. See SpecialUploadStash, which implements a web interface to some files stored this way.
+ * We accomplish this using a database table, with ownership checking as you might expect. See SpecialUploadStash, which
+ * implements a web interface to some files stored this way.
*
+ * UploadStash represents the entire stash of temporary files.
+ * UploadStashFile is a filestore for the actual physical disk files.
+ * UploadFromStash extends UploadBase, and represents a single stashed file as it is moved from the stash to the regular file repository
*/
class UploadStash {
// Format of the key for files -- has to be suitable as a filename itself (e.g. ab12cd34ef.jpg)
const KEY_FORMAT_REGEX = '/^[\w-]+\.\w*$/';
+
+ // When a given stashed file can't be loaded, wait for the slaves to catch up. If they're more than MAX_LAG
+ // behind, throw an exception instead. (at what point is broken better than slow?)
+ const MAX_LAG = 30;
/**
* repository that this uses to store temp files
*/
public $repo;
- // array of initialized objects obtained from session (lazily initialized upon getFile())
- private $files = array();
-
- // TODO: Once UploadBase starts using this, switch to use these constants rather than UploadBase::SESSION*
- // const SESSION_VERSION = 2;
- // const SESSION_KEYNAME = 'wsUploadData';
+ // array of initialized repo objects
+ protected $files = array();
+
+ // cache of the file metadata that's stored in the database
+ protected $fileMetadata = array();
+
+ // fileprops cache
+ protected $fileProps = array();
/**
- * Represents the session which contains temporarily stored files.
+ * Represents a temporary filestore, with metadata in the database.
* Designed to be compatible with the session stashing code in UploadBase (should replace it eventually)
*
* @param $repo FileRepo
*/
public function __construct( $repo ) {
-
// this might change based on wiki's configuration.
$this->repo = $repo;
-
- if ( ! isset( $_SESSION ) ) {
- throw new UploadStashNotAvailableException( 'no session variable' );
- }
-
- if ( !isset( $_SESSION[UploadBase::SESSION_KEYNAME] ) ) {
- $_SESSION[UploadBase::SESSION_KEYNAME] = array();
- }
-
}
/**
* Get a file and its metadata from the stash.
- * May throw exception if session data cannot be parsed due to schema change, or key not found.
*
- * @param $key Integer: key
+ * @param $key String: key under which file information is stored
* @throws UploadStashFileNotFoundException
- * @throws UploadStashBadVersionException
+ * @throws UploadStashNotLoggedInException
+ * @throws UploadStashWrongOwnerException
+ * @throws UploadStashBadPathException
* @return UploadStashFile
*/
public function getFile( $key ) {
+ global $wgUser;
+
if ( ! preg_match( self::KEY_FORMAT_REGEX, $key ) ) {
throw new UploadStashBadPathException( "key '$key' is not in a proper format" );
}
- if ( !isset( $this->files[$key] ) ) {
- if ( !isset( $_SESSION[UploadBase::SESSION_KEYNAME][$key] ) ) {
- throw new UploadStashFileNotFoundException( "key '$key' not found in stash" );
- }
+ $userId = $wgUser->getId();
+ if( !$userId ) {
+ throw new UploadStashNotLoggedInException( 'No user is logged in, files must belong to users' );
+ }
- $data = $_SESSION[UploadBase::SESSION_KEYNAME][$key];
- // guards against PHP class changing while session data doesn't
- if ($data['version'] !== UploadBase::SESSION_VERSION ) {
- throw new UploadStashBadVersionException( $data['version'] . " does not match current version " . UploadBase::SESSION_VERSION );
+ if ( !isset( $this->fileMetadata[$key] ) ) {
+ // try this first. if it fails to find the row, check for lag, wait, try again. if its still missing, throw an exception.
+ // this more complex solution keeps things moving for page loads with many requests
+ // (ie. validating image ownership) when replag is high
+ if( !$this->fetchFileMetadata($key) ) {
+ $lag = $dbr->getLag();
+ if( $lag > 0 && $lag <= self::MAX_LAG ) {
+ // if there's not too much replication lag, just wait for the slave to catch up to our last insert.
+ sleep( ceil( $lag ) );
+ } elseif($lag > self::MAX_LAG ) {
+ // that's a lot of lag to introduce into the middle of the UI.
+ throw new UploadStashMaxLagExceededException(
+ 'Couldn\'t load stashed file metadata, and replication lag is above threshold: (MAX_LAG=' . self::MAX_LAG . ')'
+ );
+ }
+
+ // now that the waiting has happened, try again
+ $this->fetchFileMetadata($key);
+ }
+
+ if ( !isset( $this->fileMetadata[$key] ) ) {
+ throw new UploadStashFileNotFoundException( "key '$key' not found in stash" );
}
- // separate the stashData into the path, and then the rest of the data
- $path = $data['mTempPath'];
- unset( $data['mTempPath'] );
+ // create $this->files[$key]
+ $this->initFile( $key );
- $file = new UploadStashFile( $this, $this->repo, $path, $key, $data );
- if ( $file->getSize() === 0 ) {
- throw new UploadStashZeroLengthFileException( "File is zero length" );
+ // fetch fileprops
+ $path = $this->fileMetadata[$key]['us_path'];
+ if ( $this->repo->isVirtualUrl( $path ) ) {
+ $path = $this->repo->resolveVirtualUrl( $path );
}
- $this->files[$key] = $file;
-
+ $this->fileProps[$key] = File::getPropsFromPath( $path );
}
+
+ if( $this->fileMetadata[$key]['us_user'] != $userId ) {
+ throw new UploadStashWrongOwnerException( "This file ($key) doesn't belong to the current user." );
+ }
+
return $this->files[$key];
}
/**
- * Stash a file in a temp directory and record that we did this in the session, along with other metadata.
- * We store data in a flat key-val namespace because that's how UploadBase did it. This also means we have to
- * ensure that the key-val pairs in $data do not overwrite other required fields.
+ * Getter for file metadata.
+ *
+ * @param key String: key under which file information is stored
+ * @return Array
+ */
+ public function getMetadata ( $key ) {
+ $this->getFile( $key );
+ return $this->fileMetadata[$key];
+ }
+
+ /**
+ * Getter for fileProps
+ *
+ * @param key String: key under which file information is stored
+ * @return Array
+ */
+ public function getFileProps ( $key ) {
+ $this->getFile( $key );
+ return $this->fileProps[$key];
+ }
+
+ /**
+ * Stash a file in a temp directory and record that we did this in the database, along with other metadata.
*
* @param $path String: path to file you want stashed
- * @param $data Array: optional, other data you want associated with the file. Do not use 'mTempPath', 'mFileProps', 'mFileSize', or 'version' as keys here
- * @param $key String: optional, unique key for this file in this session. Used for directory hashing when storing, otherwise not important
+ * @param $sourceType String: the type of upload that generated this file (currently, I believe, 'file' or null)
+ * @param $key String: optional, unique key for this file. Used for directory hashing when storing, otherwise not important
* @throws UploadStashBadPathException
* @throws UploadStashFileException
+ * @throws UploadStashNotLoggedInException
* @return UploadStashFile: file, or null on failure
*/
- public function stashFile( $path, $data = array(), $key = null ) {
+ public function stashFile( $path, $sourceType = null, $key = null ) {
+ global $wgUser;
if ( ! file_exists( $path ) ) {
- wfDebug( "UploadStash: tried to stash file at '$path', but it doesn't exist\n" );
+ wfDebug( __METHOD__ . " tried to stash file at '$path', but it doesn't exist\n" );
throw new UploadStashBadPathException( "path doesn't exist" );
}
$fileProps = File::getPropsFromPath( $path );
+ wfDebug( __METHOD__ . " stashing file at '$path'\n" );
+
// we will be initializing from some tmpnam files that don't have extensions.
// most of MediaWiki assumes all uploaded files have good extensions. So, we fix this.
$extension = self::getExtensionForPath( $path );
$key = $fileProps['sha1'] . "." . $extension;
}
+ $this->fileProps[$key] = $fileProps;
+
if ( ! preg_match( self::KEY_FORMAT_REGEX, $key ) ) {
throw new UploadStashBadPathException( "key '$key' is not in a proper format" );
}
+
+ wfDebug( __METHOD__ . " key for '$path': $key\n" );
// if not already in a temporary area, put it there
- $status = $this->repo->storeTemp( basename( $path ), $path );
+ $storeResult = $this->repo->storeTemp( basename( $path ), $path );
- if( ! $status->isOK() ) {
+ if( ! $storeResult->isOK() ) {
// It is a convention in MediaWiki to only return one error per API exception, even if multiple errors
// are available. We use reset() to pick the "first" thing that was wrong, preferring errors to warnings.
- // This is a bit lame, as we may have more info in the $status and we're throwing it away, but to fix it means
+ // This is a bit lame, as we may have more info in the $storeResult and we're throwing it away, but to fix it means
// redesigning API errors significantly.
- // $status->value just contains the virtual URL (if anything) which is probably useless to the caller
- $error = $status->getErrorsArray();
+ // $storeResult->value just contains the virtual URL (if anything) which is probably useless to the caller
+ $error = $storeResult->getErrorsArray();
$error = reset( $error );
if ( ! count( $error ) ) {
- $error = $status->getWarningsArray();
+ $error = $storeResult->getWarningsArray();
$error = reset( $error );
if ( ! count( $error ) ) {
$error = array( 'unknown', 'no error recorded' );
}
throw new UploadStashFileException( "error storing file in '$path': " . implode( '; ', $error ) );
}
- $stashPath = $status->value;
-
- // required info we always store. Must trump any other application info in $data
- // 'mTempPath', 'mFileSize', and 'mFileProps' are arbitrary names
- // chosen for compatibility with UploadBase's way of doing this.
- $requiredData = array(
- 'mTempPath' => $stashPath,
- 'mFileSize' => $fileProps['size'],
- 'mFileProps' => $fileProps,
- 'version' => UploadBase::SESSION_VERSION
+ $stashPath = $storeResult->value;
+
+ // fetch the current user ID
+ $userId = $wgUser->getId();
+ if( !$userId ) {
+ throw new UploadStashNotLoggedInException( "No user is logged in, files must belong to users" );
+ }
+
+ $this->fileMetadata[$key] = array(
+ 'us_user' => $userId,
+ 'us_key' => $key,
+ 'us_orig_path' => $path,
+ 'us_path' => $stashPath,
+ 'us_size' => $fileProps['size'],
+ 'us_sha1' => $fileProps['sha1'],
+ 'us_mime' => $fileProps['mime'],
+ 'us_media_type' => $fileProps['media_type'],
+ 'us_image_width' => $fileProps['width'],
+ 'us_image_height' => $fileProps['height'],
+ 'us_image_bits' => $fileProps['bits'],
+ 'us_source_type' => $sourceType,
+ 'us_timestamp' => wfTimestamp( TS_MW )
+ );
+
+ // insert the file metadata into the db.
+ wfDebug( __METHOD__ . " inserting $stashPath under $key\n" );
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->insert(
+ 'uploadstash',
+ $this->fileMetadata[$key],
+ __METHOD__
);
- // now, merge required info and extra data into the session. (The extra data changes from application to application.
- // UploadWizard wants different things than say FirefoggChunkedUpload.)
- wfDebug( __METHOD__ . " storing under $key\n" );
- $_SESSION[UploadBase::SESSION_KEYNAME][$key] = array_merge( $data, $requiredData );
+ // store the insertid in the class variable so immediate retrieval (possibly laggy) isn't necesary.
+ $this->fileMetadata[$key]['us_id'] = $dbw->insertId();
+ # create the UploadStashFile object for this file.
+ $this->initFile( $key );
+
return $this->getFile( $key );
}
/**
* Remove all files from the stash.
* Does not clean up files in the repo, just the record of them.
+ *
+ * @throws UploadStashNotLoggedInException
* @return boolean: success
*/
public function clear() {
- $_SESSION[UploadBase::SESSION_KEYNAME] = array();
+ global $wgUser;
+
+ $userId = $wgUser->getId();
+ if( !$userId ) {
+ throw new UploadStashNotLoggedInException( 'No user is logged in, files must belong to users' );
+ }
+
+ wfDebug( __METHOD__ . " clearing all rows for user $userId\n" );
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->delete(
+ 'uploadstash',
+ array( 'us_user' => $userId ),
+ __METHOD__
+ );
+
+ # destroy objects.
+ $this->files = array();
+ $this->fileMetadata = array();
+
return true;
}
/**
- * Remove a particular file from the stash.
- * Does not clean up file in the repo, just the record of it.
+ * Remove a particular file from the stash. Also removes it from the repo.
+ *
+ * @throws UploadStashNotLoggedInException
* @return boolean: success
*/
public function removeFile( $key ) {
- unset ( $_SESSION[UploadBase::SESSION_KEYNAME][$key] );
+ global $wgUser;
+
+ $userId = $wgUser->getId();
+ if( !$userId ) {
+ throw new UploadStashNotLoggedInException( 'No user is logged in, files must belong to users' );
+ }
+
+ wfDebug( __METHOD__ . " clearing row $key for user $userId\n" );
+ $dbw = wfGetDB( DB_MASTER );
+
+ // this is a cheap query. it runs on the master so that this function still works when there's lag.
+ // it won't be called all that often.
+ $row = $dbw->selectRow(
+ 'uploadstash',
+ 'us_user',
+ array('us_key' => $key),
+ __METHOD__
+ );
+
+ if( $row->us_user != $userId ) {
+ throw new UploadStashWrongOwnerException( "Can't delete: the file ($key) doesn't belong to this user." );
+ }
+
+ $dbw->delete(
+ 'uploadstash',
+ array( 'us_key' => $key, 'us_user' => $userId ),
+ __METHOD__
+ );
+
+ // TODO: look into UnregisteredLocalFile and find out why the rv here is sometimes wrong (false when file was removed)
+ // for now, ignore.
+ $this->files[$key]->remove();
+
+ unset( $this->files[$key] );
+ unset( $this->fileMetadata[$key] );
+
return true;
}
/**
* List all files in the stash.
+ *
+ * @throws UploadStashNotLoggedInException
+ * @return Array
*/
public function listFiles() {
- return array_keys( $_SESSION[UploadBase::SESSION_KEYNAME] );
+ global $wgUser;
+
+ $userId = $wgUser->getId();
+ if( !$userId ) {
+ throw new UploadStashNotLoggedInException( 'No user is logged in, files must belong to users' );
+ }
+
+ $dbw = wfGetDB( DB_SLAVE );
+ $res = $dbr->select(
+ 'uploadstash',
+ 'us_key',
+ array('us_key' => $key),
+ __METHOD__
+ );
+
+ if( !is_object( $res ) ) {
+ // nothing there.
+ return false;
+ }
+
+ $keys = array();
+ while( $row = $dbr->fetchRow( $res ) ) {
+ array_push( $keys, $row['us_key'] );
+ }
+
+ return $keys;
}
/**
return File::normalizeExtension( $extension );
}
+ /**
+ * Helper function: do the actual database query to fetch file metadata.
+ *
+ * @param $key String: key
+ * @return boolean
+ */
+ protected function fetchFileMetadata( $key ) {
+ // populate $fileMetadata[$key]
+ $dbr = wfGetDB( DB_SLAVE );
+ $row = $dbr->selectRow(
+ 'uploadstash',
+ '*',
+ array('us_key' => $key),
+ __METHOD__
+ );
+
+ if( !is_object( $row ) ) {
+ // key wasn't present in the database. this will happen sometimes.
+ return false;
+ }
+
+ $this->fileMetadata[$key] = array(
+ 'us_user' => $row->us_user,
+ 'us_key' => $row->us_key,
+ 'us_orig_path' => $row->us_orig_path,
+ 'us_path' => $row->us_path,
+ 'us_size' => $row->us_size,
+ 'us_sha1' => $row->us_sha1,
+ 'us_mime' => $row->us_mime,
+ 'us_media_type' => $row->us_media_type,
+ 'us_image_width' => $row->us_image_width,
+ 'us_image_height' => $row->us_image_height,
+ 'us_image_bits' => $row->us_image_bits,
+ 'us_source_type' => $row->us_source_type
+ );
+
+ return true;
+ }
+
+ /**
+ * Helper function: Initialize the UploadStashFile for a given file.
+ *
+ * @param $path String: path to file
+ * @param $key String: key under which to store the object
+ * @throws UploadStashZeroLengthFileException
+ * @return bool
+ */
+ protected function initFile( $key ) {
+ $file = new UploadStashFile( $this->repo, $this->fileMetadata[$key]['us_path'], $key );
+ if ( $file->getSize() === 0 ) {
+ throw new UploadStashZeroLengthFileException( "File is zero length" );
+ }
+ $this->files[$key] = $file;
+ return true;
+ }
}
class UploadStashFile extends UnregisteredLocalFile {
- private $sessionStash;
- private $sessionKey;
- private $sessionData;
+ private $fileKey;
private $urlName;
protected $url;
* A LocalFile wrapper around a file that has been temporarily stashed, so we can do things like create thumbnails for it
* Arguably UnregisteredLocalFile should be handling its own file repo but that class is a bit retarded currently
*
- * @param $stash UploadStash: useful for obtaining config, stashing transformed files
* @param $repo FSRepo: repository where we should find the path
* @param $path String: path to file
* @param $key String: key to store the path and any stashed data under
- * @param $data String: any other data we want stored with this file
* @throws UploadStashBadPathException
* @throws UploadStashFileNotFoundException
*/
- public function __construct( $stash, $repo, $path, $key, $data ) {
- $this->sessionStash = $stash;
- $this->sessionKey = $key;
- $this->sessionData = $data;
+ public function __construct( $repo, $path, $key ) {
+ $this->fileKey = $key;
// resolve mwrepo:// urls
if ( $repo->isVirtualUrl( $path ) ) {
* Get a URL to access the thumbnail
* This is required because the model of how files work requires that
* the thumbnail urls be predictable. However, in our model the URL is not based on the filename
- * (that's hidden in the session)
+ * (that's hidden in the db)
*
* @param $thumbName String: basename of thumbnail file -- however, we don't want to use the file exactly
* @return String: URL to access thumbnail, or URL with partial path
*/
public function getUrlName() {
if ( ! $this->urlName ) {
- $this->urlName = $this->sessionKey;
+ $this->urlName = $this->fileKey;
}
return $this->urlName;
}
}
/**
- * Getter for session key (the session-unique id by which this file's location & metadata is stored in the session)
+ * Getter for file key (the unique id by which this file's location & metadata is stored in the db)
*
- * @return String: session key
+ * @return String: file key
*/
- public function getSessionKey() {
- return $this->sessionKey;
+ public function getFileKey() {
+ return $this->fileKey;
}
/**
* @return Status: success
*/
public function remove() {
+ if( !$this->repo->fileExists( $this->path, FileRepo::FILES_ONLY ) ) {
+ // Maybe the file's already been removed? This could totally happen in UploadBase.
+ return true;
+ }
+
return $this->repo->freeTemp( $this->path );
}
class UploadStashNotAvailableException extends MWException {};
class UploadStashFileNotFoundException extends MWException {};
class UploadStashBadPathException extends MWException {};
-class UploadStashBadVersionException extends MWException {};
class UploadStashFileException extends MWException {};
class UploadStashZeroLengthFileException extends MWException {};
-
+class UploadStashNotLoggedInException extends MWException {};
+class UploadStashWrongOwnerException extends MWException {};
+class UploadStashMaxLagExceededException extends MWException {};
--- /dev/null
+--
+-- Store information about newly uploaded files before they're
+-- moved into the actual filestore
+--
+CREATE TABLE /*_*/uploadstash (
+ us_id int unsigned NOT NULL PRIMARY KEY auto_increment,
+
+ -- the user who uploaded the file.
+ us_user int unsigned NOT NULL,
+
+ -- file key. this is how applications actually search for the file.
+ -- this might go away, or become the primary key.
+ us_key varchar(255) NOT NULL,
+
+ -- the original path
+ us_orig_path varchar(255) NOT NULL,
+
+ -- the temporary path at which the file is actually stored
+ us_path varchar(255) NOT NULL,
+
+ -- which type of upload the file came from (sometimes)
+ us_source_type varchar(50),
+
+ -- the date/time on which the file was added
+ us_timestamp varchar(14) not null,
+
+ -- file properties from File::getPropsFromPath. these may prove unnecessary.
+ --
+ us_size int unsigned NOT NULL,
+ -- this hash comes from File::sha1Base36(), and is 31 characters
+ us_sha1 varchar(31) NOT NULL,
+ us_mime varchar(255),
+ us_media_type varchar(255),
+ -- image-specific properties
+ us_image_width smallint unsigned,
+ us_image_height smallint unsigned,
+ us_image_bits smallint unsigned
+
+) /*$wgDBTableOptions*/;
+
+-- sometimes there's a delete for all of a user's stuff.
+CREATE INDEX /*i*/us_user ON /*_*/uploadstash (us_user);
+-- pick out files by key, enforce key uniqueness
+CREATE UNIQUE INDEX /*i*/us_key ON /*_*/uploadstash (us_key);
CREATE INDEX /*i*/fa_user_timestamp ON /*_*/filearchive (fa_user_text,fa_timestamp);
+--
+-- Store information about newly uploaded files before they're
+-- moved into the actual filestore
+--
+CREATE TABLE /*_*/uploadstash (
+ us_id int unsigned NOT NULL PRIMARY KEY auto_increment,
+
+ -- the user who uploaded the file.
+ us_user int unsigned NOT NULL,
+
+ -- file key. this is how applications actually search for the file.
+ -- this might go away, or become the primary key.
+ us_key varchar(255) NOT NULL,
+
+ -- the original path
+ us_orig_path varchar(255) NOT NULL,
+
+ -- the temporary path at which the file is actually stored
+ us_path varchar(255) NOT NULL,
+
+ -- which type of upload the file came from (sometimes)
+ us_source_type varchar(50),
+
+ -- the date/time on which the file was added
+ us_timestamp varchar(14) not null,
+
+ -- file properties from File::getPropsFromPath. these may prove unnecessary.
+ --
+ us_size int unsigned NOT NULL,
+ -- this hash comes from File::sha1Base36(), and is 31 characters
+ us_sha1 varchar(31) NOT NULL,
+ us_mime varchar(255),
+ us_media_type varchar(255),
+ -- image-specific properties
+ us_image_width smallint unsigned,
+ us_image_height smallint unsigned,
+ us_image_bits smallint unsigned
+
+) /*$wgDBTableOptions*/;
+
+-- sometimes there's a delete for all of a user's stuff.
+CREATE INDEX /*i*/us_user ON /*_*/uploadstash (us_user);
+-- pick out files by key, enforce key uniqueness
+CREATE UNIQUE INDEX /*i*/us_key ON /*_*/uploadstash (us_key);
+
+
--
-- Primarily a summary table for Special:Recentchanges,
-- this table contains some additional info on edits from
), $session );
} catch ( UsageException $e ) {
$exception = true;
- $this->assertEquals( "One of the parameters sessionkey, file, url, statuskey is required",
+ $this->assertEquals( "One of the parameters filekey, file, url, statuskey is required",
$e->getMessage() );
}
$this->assertTrue( $exception, "Got exception" );
$this->assertEquals( 'Success', $result['upload']['result'] );
$this->assertEquals( $fileSize, ( int )$result['upload']['imageinfo']['size'] );
$this->assertEquals( $mimeType, $result['upload']['imageinfo']['mime'] );
- $this->assertTrue( isset( $result['upload']['sessionkey'] ) );
- $sessionkey = $result['upload']['sessionkey'];
+ $this->assertTrue( isset( $result['upload']['filekey'] ) );
+ $this->assertEquals( $result['upload']['sessionkey'], $result['upload']['filekey'] );
+ $filekey = $result['upload']['filekey'];
// it should be visible from Special:UploadStash
// XXX ...but how to test this, with a fake WebRequest with the session?
// now we should try to release the file from stash
$params = array(
'action' => 'upload',
- 'sessionkey' => $sessionkey,
+ 'filekey' => $filekey,
'filename' => $fileName,
'comment' => 'dummy comment',
'text' => "This is the page text for $fileName, altered",
);
- $session[ UploadBase::getSessionKeyname() ] = $_SESSION[ UploadBase::getSessionKeyname() ];
$this->clearFakeUploads();
$exception = false;
<?php
class UploadStashTest extends MediaWikiTestCase {
+ /**
+ * @var Array of UploadStashTestUser
+ */
+ public static $users;
+
public function setUp() {
parent::setUp();
- // Setup a fake session if necessary
- if ( !isset( $_SESSION ) ) {
- $GLOBALS['_SESSION'] = array();
- }
-
// Setup a file for bug 29408
$this->bug29408File = dirname( __FILE__ ) . '/bug29408';
file_put_contents( $this->bug29408File, "\x00" );
+
+ self::$users = array(
+ 'sysop' => new ApiTestUser(
+ 'Uploadstashtestsysop',
+ 'Upload Stash Test Sysop',
+ 'upload_stash_test_sysop@sample.com',
+ array( 'sysop' )
+ ),
+ 'uploader' => new ApiTestUser(
+ 'Uploadstashtestuser',
+ 'Upload Stash Test User',
+ 'upload_stash_test_user@sample.com',
+ array()
+ )
+ );
}
public function testBug29408() {
+ global $wgUser;
+ $wgUser = self::$users['uploader']->user;
+
$repo = RepoGroup::singleton()->getLocalRepo();
$stash = new UploadStash( $repo );
// We'll never reach this point if we hit bug 29408
$this->assertTrue( true, 'Unrecognized file without extension' );
- $file->remove();
+ $stash->removeFile( $file->getFileKey() );
}
public function tearDown() {