* For backends that support it, custom HTTP headers can be set on files.
* Added a getStreamHeaders() function to MediaHandler to let subclasses
recommend header name/value pairs to be used for responses to GET/HEAD
requests. For example, an OGG handler could set "X-Content-Duration".
* Made LocalFile use this function to set HTTP headers of new uploads.
Change-Id: I1b017e1342513f0097fe6d142aae18e819403293
# Height, width and metadata
$handler = MediaHandler::getHandler( $info['mime'] );
if ( $handler ) {
- $tempImage = (object)array();
+ $tempImage = (object)array(); // XXX (hack for File object)
$info['metadata'] = $handler->getMetadata( $tempImage, $this->path );
$gis = $handler->getImageSize( $tempImage, $this->path, $info['metadata'] );
if ( is_array( $gis ) ) {
* 'content' => <string of new file contents>,
* 'overwrite' => <boolean>,
* 'overwriteSame' => <boolean>,
- * 'disposition' => <Content-Disposition header value>
+ * 'disposition' => <Content-Disposition header value>,
+ * 'headers' => <HTTP header name/value map> # since 1.21
* );
* @endcode
*
* 'dst' => <storage path>,
* 'overwrite' => <boolean>,
* 'overwriteSame' => <boolean>,
- * 'disposition' => <Content-Disposition header value>
+ * 'disposition' => <Content-Disposition header value>,
+ * 'headers' => <HTTP header name/value map> # since 1.21
* )
* @endcode
*
* - overwriteSame : An error will not be given if a file already
* exists at the destination that has the same
* contents as the new contents to be written there.
- * - disposition : When supplied, the backend will add a Content-Disposition
+ * - disposition : If supplied, the backend will return a Content-Disposition
* header when GETs/HEADs of the destination file are made.
- * Backends that don't support file metadata will ignore this.
- * See http://tools.ietf.org/html/rfc6266 (since 1.20).
+ * Backends that don't support metadata ignore this.
+ * See http://tools.ietf.org/html/rfc6266. (since 1.20)
+ * - headers : If supplied, the backend will return these headers when
+ * GETs/HEADs of the destination file are made. Header values
+ * should be smaller than 256 bytes, often options or numbers.
+ * Backends that don't support metadata ignore this. (since 1.21)
*
* $opts is an associative of boolean flags, including:
* - force : Operation precondition errors no longer trigger an abort.
* - nonJournaled : Don't log this operation batch in the file journal.
* This limits the ability of recovery scripts.
* - parallelize : Try to do operations in parallel when possible.
- * - bypassReadOnly : Allow writes in read-only mode (since 1.20).
+ * - bypassReadOnly : Allow writes in read-only mode. (since 1.20)
* - preserveCache : Don't clear the process cache before checking files.
* This should only be used if all entries in the process
- * cache were added after the files were already locked (since 1.20).
+ * cache were added after the files were already locked. (since 1.20)
*
* @remarks Remarks on locking:
* File system paths given to operations should refer to files that are
* 'op' => 'create',
* 'dst' => <storage path>,
* 'content' => <string of new file contents>,
- * 'disposition' => <Content-Disposition header value>
+ * 'disposition' => <Content-Disposition header value>,
+ * 'headers' => <HTTP header name/value map> # since 1.21
* )
* @endcode
* b) Copy a file system file into storage
* 'op' => 'store',
* 'src' => <file system path>,
* 'dst' => <storage path>,
- * 'disposition' => <Content-Disposition header value>
+ * 'disposition' => <Content-Disposition header value>,
+ * 'headers' => <HTTP header name/value map> # since 1.21
* )
* @endcode
* c) Copy a file within storage
* header when GETs/HEADs of the destination file are made.
* Backends that don't support file metadata will ignore this.
* See http://tools.ietf.org/html/rfc6266 (since 1.20).
+ * - headers : If supplied, the backend will return these headers when
+ * GETs/HEADs of the destination file are made. Header values
+ * should be smaller than 256 bytes, often options or numbers.
+ * Backends that don't support metadata ignore this. (since 1.21)
*
* $opts is an associative of boolean flags, including:
* - bypassReadOnly : Allow writes in read-only mode (since 1.20)
* - content : the raw file contents
* - dst : destination storage path
* - disposition : Content-Disposition header value for the destination
+ * - headers : HTTP header name/value map
* - async : Status will be returned immediately if supported.
* If the status is OK, then its value field will be
* set to a FileBackendStoreOpHandle object.
* - src : source path on disk
* - dst : destination storage path
* - disposition : Content-Disposition header value for the destination
+ * - headers : HTTP header name/value map
* - async : Status will be returned immediately if supported.
* If the status is OK, then its value field will be
* set to a FileBackendStoreOpHandle object.
wfProfileIn( __METHOD__ . '-' . $this->name );
$status = Status::newGood();
+ // Fix up custom header name/value pairs...
+ $ops = array_map( array( $this, 'stripInvalidHeadersFromOp' ), $ops );
+
// Build up a list of FileOps...
$performOps = $this->getOperationsInternal( $ops );
wfProfileIn( __METHOD__ . '-' . $this->name );
$status = Status::newGood();
+ // Fix up custom header name/value pairs...
+ $ops = array_map( array( $this, 'stripInvalidHeadersFromOp' ), $ops );
+
$supportedOps = array( 'create', 'store', 'copy', 'move', 'delete', 'null' );
$async = ( $this->parallelize === 'implicit' );
$maxConcurrency = $this->concurrency; // throttle
return array();
}
+ /**
+ * Strip long HTTP headers from a file operation
+ *
+ * @param $op array Same format as doOperation()
+ * @return Array
+ */
+ protected function stripInvalidHeadersFromOp( array $op ) {
+ if ( isset( $op['headers'] ) ) {
+ foreach ( $op['headers'] as $name => $value ) {
+ if ( strlen( $name ) > 255 || strlen( $value ) > 255 ) {
+ trigger_error( "Header '$name: $value' is too long." );
+ unset( $op['headers'][$name] );
+ }
+ }
+ }
+ return $op;
+ }
+
/**
* @see FileBackend::preloadCache()
*/
class CreateFileOp extends FileOp {
protected function allowedParams() {
return array( array( 'content', 'dst' ),
- array( 'overwrite', 'overwriteSame', 'disposition' ) );
+ array( 'overwrite', 'overwriteSame', 'disposition', 'headers' ) );
}
protected function doPrecheck( array &$predicates ) {
*/
protected function allowedParams() {
return array( array( 'src', 'dst' ),
- array( 'overwrite', 'overwriteSame', 'disposition' ) );
+ array( 'overwrite', 'overwriteSame', 'disposition', 'headers' ) );
}
/**
if ( isset( $params['disposition'] ) ) {
$obj->headers['Content-Disposition'] = $this->truncDisp( $params['disposition'] );
}
+ // Set any other custom headers if requested
+ if ( isset( $params['headers'] ) ) {
+ $obj->headers += $params['headers'];
+ }
if ( !empty( $params['async'] ) ) { // deferred
$op = $obj->write_async( $params['content'] );
$status->value = new SwiftFileOpHandle( $this, $params, 'Create', $op );
if ( isset( $params['disposition'] ) ) {
$obj->headers['Content-Disposition'] = $this->truncDisp( $params['disposition'] );
}
+ // Set any other custom headers if requested
+ if ( isset( $params['headers'] ) ) {
+ $obj->headers += $params['headers'];
+ }
if ( !empty( $params['async'] ) ) { // deferred
wfSuppressWarnings();
$fp = fopen( $params['src'], 'rb' );
* Returns a FileRepoStatus object. On success, the value contains "new" or
* "archived", to indicate whether the file was new with that name.
*
+ * Options to $options include:
+ * - headers : name/value map of HTTP headers to use in response to GET/HEAD requests
+ *
* @param $srcPath String: the source file system path, storage path, or URL
* @param $dstRel String: the destination relative path
* @param $archiveRel String: the relative path where the existing file is to
* be archived, if there is one. Relative to the public zone root.
* @param $flags Integer: bitfield, may be FileRepo::DELETE_SOURCE to indicate
* that the source file should be deleted if possible
+ * @param $options Array Optional additional parameters
* @return FileRepoStatus
*/
- public function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) {
+ public function publish(
+ $srcPath, $dstRel, $archiveRel, $flags = 0, array $options = array()
+ ) {
$this->assertWritableRepo(); // fail out if read-only
- $status = $this->publishBatch( array( array( $srcPath, $dstRel, $archiveRel ) ), $flags );
+ $status = $this->publishBatch(
+ array( array( $srcPath, $dstRel, $archiveRel, $options ) ), $flags );
if ( $status->successCount == 0 ) {
$status->ok = false;
}
/**
* Publish a batch of files
*
- * @param $triplets Array: (source, dest, archive) triplets as per publish()
+ * @param $triplets Array: (source, dest, archive) triplets or
+ * (source, dest, archive, options) quartets as per publish().
* @param $flags Integer: bitfield, may be FileRepo::DELETE_SOURCE to indicate
* that the source files should be deleted if possible
* @throws MWException
// Validate each triplet and get the store operation...
foreach ( $triplets as $i => $triplet ) {
list( $srcPath, $dstRel, $archiveRel ) = $triplet;
+ $options = isset( $triplet[3] ) ? $triplet[3] : array();
// Resolve source to a storage path if virtual
$srcPath = $this->resolveToStoragePath( $srcPath );
if ( !$this->validateFilename( $dstRel ) ) {
return $this->newFatal( 'directorycreateerror', $archiveDir );
}
+ // Set any desired headers to be use in GET/HEAD responses
+ $headers = isset( $options['headers'] ) ? $options['headers'] : array();
+
// Archive destination file if it exists.
// This will check if the archive file also exists and fail if does.
// This is a sanity check to avoid data loss. On Windows and Linux,
if ( FileBackend::isStoragePath( $srcPath ) ) {
if ( $flags & self::DELETE_SOURCE ) {
$operations[] = array(
- 'op' => 'move',
- 'src' => $srcPath,
- 'dst' => $dstPath,
- 'overwrite' => true // replace current
+ 'op' => 'move',
+ 'src' => $srcPath,
+ 'dst' => $dstPath,
+ 'overwrite' => true, // replace current
+ 'headers' => $headers
);
} else {
$operations[] = array(
- 'op' => 'copy',
- 'src' => $srcPath,
- 'dst' => $dstPath,
- 'overwrite' => true // replace current
+ 'op' => 'copy',
+ 'src' => $srcPath,
+ 'dst' => $dstPath,
+ 'overwrite' => true, // replace current
+ 'headers' => $headers
);
}
} else { // FS source path
$operations[] = array(
- 'op' => 'store',
- 'src' => $srcPath,
- 'dst' => $dstPath,
- 'overwrite' => true // replace current
+ 'op' => 'store',
+ 'src' => $srcPath,
+ 'dst' => $dstPath,
+ 'overwrite' => true, // replace current
+ 'headers' => $headers
);
if ( $flags & self::DELETE_SOURCE ) {
$sourceFSFilesToDelete[] = $srcPath;
* The archive name should be passed through to recordUpload for database
* registration.
*
+ * Options to $options include:
+ * - headers : name/value map of HTTP headers to use in response to GET/HEAD requests
+ *
* @param $srcPath String: local filesystem path to the source image
* @param $flags Integer: a bitwise combination of:
* File::DELETE_SOURCE Delete the source file, i.e. move
* rather than copy
+ * @param $options Array Optional additional parameters
* @return FileRepoStatus object. On success, the value member contains the
* archive name, or an empty string if it was a new file.
*
* STUB
* Overridden by LocalFile
*/
- function publish( $srcPath, $flags = 0 ) {
+ function publish( $srcPath, $flags = 0, array $options = array() ) {
$this->readOnlyError();
}
/**
* Upload a file and record it in the DB
- * @param $srcPath String: source storage path or virtual URL
+ * @param $srcPath String: source storage path, virtual URL, or filesystem path
* @param $comment String: upload description
* @param $pageText String: text to use for the new description page,
* if a new description page is created
return $this->readOnlyFatalStatus();
}
+ if ( !$props ) {
+ wfProfileIn( __METHOD__ . '-getProps' );
+ $props = FileBackend::isStoragePath( $srcPath )
+ ? $this->repo->getFileProps( $srcPath )
+ : FSFile::getPropsFromPath( $srcPath );
+ wfProfileOut( __METHOD__ . '-getProps' );
+ }
+
+ $options = array();
+ $handler = MediaHandler::getHandler( $props['mime'] );
+ if ( $handler ) {
+ $options['headers'] = $handler->getStreamHeaders( $props['metadata'] );
+ } else {
+ $options['headers'] = array();
+ }
+
// truncate nicely or the DB will do it for us
// non-nicely (dangling multi-byte chars, non-truncated version in cache).
$comment = $wgContLang->truncate( $comment, 255 );
$this->lock(); // begin
- $status = $this->publish( $srcPath, $flags );
+ $status = $this->publish( $srcPath, $flags, $options );
if ( $status->successCount > 0 ) {
# Essentially we are displacing any existing current file and saving
* @param $srcPath String: local filesystem path to the source image
* @param $flags Integer: a bitwise combination of:
* File::DELETE_SOURCE Delete the source file, i.e. move rather than copy
+ * @param $options Array Optional additional parameters
* @return FileRepoStatus object. On success, the value member contains the
* archive name, or an empty string if it was a new file.
*/
- function publish( $srcPath, $flags = 0 ) {
- return $this->publishTo( $srcPath, $this->getRel(), $flags );
+ function publish( $srcPath, $flags = 0, array $options = array() ) {
+ return $this->publishTo( $srcPath, $this->getRel(), $flags, $options );
}
/**
* @param $dstRel String: target relative path
* @param $flags Integer: a bitwise combination of:
* File::DELETE_SOURCE Delete the source file, i.e. move rather than copy
+ * @param $options Array Optional additional parameters
* @return FileRepoStatus object. On success, the value member contains the
* archive name, or an empty string if it was a new file.
*/
- function publishTo( $srcPath, $dstRel, $flags = 0 ) {
+ function publishTo( $srcPath, $dstRel, $flags = 0, array $options = array() ) {
if ( $this->getRepo()->getReadOnlyReason() !== false ) {
return $this->readOnlyFatalStatus();
}
$archiveName = wfTimestamp( TS_MW ) . '!'. $this->getName();
$archiveRel = 'archive/' . $this->getHashPath() . $archiveName;
$flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0;
- $status = $this->repo->publish( $srcPath, $dstRel, $archiveRel, $flags );
+ $status = $this->repo->publish( $srcPath, $dstRel, $archiveRel, $flags, $options );
if ( $status->value == 'new' ) {
$status->value = '';
return array( $ext, $mime );
}
+ /**
+ * Get useful response headers for GET/HEAD requests for a file with the given metadata
+ * @param $metadata mixed Result this handlers getMetadata() for a file
+ * @return Array
+ */
+ public function getStreamHeaders( $metadata ) {
+ return array();
+ }
+
/**
* True if the handled types can be transformed
* @return bool