Permalink
Browse files

[Http] Adding support for Content-Type in POST upload and multiple PO…

…ST uploads for the same filename.

You can specify the Content-Type of each separate multipart upload.  The
Content-Type of each file is automatically guessed if possible, and if
content-type is not explicitly set to false. Resolves #55.

[BC] Refactoring how POST files are stored on an EntityEnclosingRequest.
They are no longer mixed with the POST fields, but rather stored in an
array of separate POST files, each array key corresponding to a POST
field name, and array value containing an array of
associative arrays containing the 'file' and 'type' for each POST file.

You can still specify files and fields when using a client or request
factory, but no longer with an EntityEnclosingRequest->setPostField()
call.

Adding new POST data operations for easier manipulation of POST data.

You can now set empty POST fields.  Closes #56.

The body of a request is only shown on EntityEnclosingRequest objects
that do not use POST files.
  • Loading branch information...
1 parent 0fcd635 commit 29f6faeb7913f25d4cbb6a9b2bf168ca0ade71d9 @mtdowling mtdowling committed May 23, 2012
Showing with 239 additions and 84 deletions.
  1. +4 −3 Client.php
  2. +22 −1 Curl/CurlHandle.php
  3. +135 −51 Message/EntityEnclosingRequest.php
  4. +58 −28 Message/EntityEnclosingRequestInterface.php
  5. +20 −1 Message/RequestFactory.php
View
@@ -226,9 +226,10 @@ public function createRequest($method = RequestInterface::GET, $uri = null, $hea
$templateVars = null;
} else {
if (count($uri) != 2 || !is_array($uri[1])) {
- throw new InvalidArgumentException('You must provide a URI'
- . ' template followed by an array of template variables'
- . ' when using an array for a URI template');
+ throw new InvalidArgumentException(
+ 'You must provide a URI template followed by an array of template variables '
+ . 'when using an array for a URI template'
+ );
}
list($uri, $templateVars) = $uri;
}
View
@@ -104,10 +104,31 @@ public static function factory(RequestInterface $request)
break;
case 'POST':
$curlOptions[CURLOPT_POST] = true;
+
// Special handling for POST specific fields and files
if (count($request->getPostFiles())) {
- $curlOptions[CURLOPT_POSTFIELDS] = $request->getPostFields()->getAll();
+
+ $fields = $request->getPostFields()->getAll();
+ foreach ($request->getPostFiles() as $key => $data) {
+ $prefixKeys = count($data) > 1;
+ foreach ($data as $index => $file) {
+ $path = '@' . $file['file'];
+ // Add the Content-Type if it's set
+ if ($file['type']) {
+ $path .= ";type={$file['type']}";
+ }
+ // Allow multiple files in the same key
+ if ($prefixKeys) {
+ $fields["{$key}[{$index}]"] = $path;
+ } else {
+ $fields[$key] = $path;
+ }
+ }
+ }
+
+ $curlOptions[CURLOPT_POSTFIELDS] = $fields;
$request->removeHeader('Content-Length');
+
} elseif (count($request->getPostFields())) {
$curlOptions[CURLOPT_POSTFIELDS] = (string) $request->getPostFields();
$request->removeHeader('Content-Length');
@@ -22,6 +22,11 @@ class EntityEnclosingRequest extends Request implements EntityEnclosingRequestIn
protected $postFields;
/**
+ * @var array POST files to send with the request
+ */
+ protected $postFiles = array();
+
+ /**
* {@inheritdoc}
*/
public function __construct($method, $url, $headers = array())
@@ -38,23 +43,24 @@ public function __construct($method, $url, $headers = array())
*/
public function __toString()
{
- return parent::__toString()
- . (count($this->getPostFields()) ? $this->postFields : $this->body);
+ // Only attempt to inclue the POST data if it's only fields
+ if (count($this->postFields) && empty($this->postFiles)) {
+ return parent::__toString() . (string) $this->postFields;
+ }
+
+ return parent::__toString() . $this->body;
}
/**
* Set the body of the request
*
- * @param string|resource|EntityBody $body Body to use in the entity body
- * of the request
- * @param string $contentType Content-Type to set. Leave null
- * to use an existing Content-Type or to guess the Content-Type
- * @param bool $tryChunkedTransfer Set to TRUE to try to use
- * Tranfer-Encoding chunked
+ * @param string|resource|EntityBody $body Body to use in the entity body of the request
+ * @param string $contentType Content-Type to set. Leave null to use an existing
+ * Content-Type or to guess the Content-Type
+ * @param bool $tryChunkedTransfer Set to TRUE to try to use Transfer-Encoding chunked
*
* @return EntityEnclosingRequest
- * @throws RequestException if the protocol is < 1.1 and Content-Length can
- * not be determined
+ * @throws RequestException if the protocol is < 1.1 and Content-Length can not be determined
*/
public function setBody($body, $contentType = null, $tryChunkedTransfer = false)
{
@@ -100,7 +106,7 @@ public function getBody()
/**
* Get a POST field from the request
*
- * @param string $field Field to retrive
+ * @param string $field Field to retrieve
*
* @return mixed|null
*/
@@ -120,20 +126,19 @@ public function getPostFields()
}
/**
- * Returns an associative array of POST field names and file paths
+ * Set a POST field value
*
- * @return array
+ * @param string $key Key to set
+ * @param string $value Value to set
+ *
+ * @return EntityEnclosingRequest
*/
- public function getPostFiles()
+ public function setPostField($key, $value)
{
- $files = array();
- foreach ($this->postFields as $key => $value) {
- if (is_string($value) && substr($value, 0, 1) == '@') {
- $files[$key] = substr($value, 1);
- }
- }
+ $this->postFields->set($key, $value);
+ $this->processPostFields();
- return $files;
+ return $this;
}
/**
@@ -152,69 +157,148 @@ public function addPostFields($fields)
}
/**
- * Set a POST field value
+ * Remove a POST field or file by name
*
- * @param string $key Key to set
- * @param string $value Value to set
+ * @param string $field Name of the POST field or file to remove
*
* @return EntityEnclosingRequest
*/
- public function setPostField($key, $value)
+ public function removePostField($field)
{
- $this->postFields->set($key, $value);
+ $this->postFields->remove($field);
$this->processPostFields();
return $this;
}
/**
- * Add POST files to use in the upload
+ * Returns an associative array of POST field names to an array of
+ * (path, Content-Type)
+ *
+ * @return array
+ */
+ public function getPostFiles()
+ {
+ return $this->postFiles;
+ }
+
+ /**
+ * Get a POST file from the request
+ *
+ * @param string $fieldName POST fields to retrieve
+ *
+ * @return array|null Returns an array wrapping an array of (field name, path, and Content-Type)
+ */
+ public function getPostFile($fieldName)
+ {
+ return isset($this->postFiles[$fieldName]) ? $this->postFiles[$fieldName] : null;
+ }
+
+ /**
+ * Remove a POST file from the request
*
- * @param array $files An array of filenames to POST
+ * @param string $fieldName POST file field name to remove
+ *
+ * @return EntityEnclosingRequest
+ */
+ public function removePostFile($fieldName)
+ {
+ unset($this->postFiles[$fieldName]);
+ $this->processPostFields();
+
+ return $this;
+ }
+
+ /**
+ * Add a POST file to the upload
+ *
+ * @param string $fieldName POST field to use (e.g. file). Used to reference content from the server.
+ * @param string $path Full path to the file. Do not include the @ symbol.
+ * @param string $contentType Optional Content-Type to add to the Content-Disposition.
+ * Default behavior is to guess. Set to false to not specify.
+ * @param bool $process Set to false to not process POST fields immediately.
*
* @return EntityEnclosingRequest
* @throws RequestException if the file cannot be read
*/
- public function addPostFiles(array $files)
+ public function addPostFile($fieldName, $path, $contentType = null, $process = true)
{
- foreach ((array) $files as $key => $file) {
+ if (!is_string($path)) {
+ throw new RequestException('The path to a file must be a string');
+ }
- if (!is_string($file) || empty($file)) {
- continue;
- }
+ // Adding an empty file will cause cURL to error out
+ if (!empty($path)) {
- // Convert non-associative array keys into 'file'
- if (is_numeric($key)) {
- $key = 'file';
+ $fieldName = (string) $fieldName ?: 'file';
+
+ if (!is_readable($path)) {
+ throw new RequestException('File cannot be opened for reading: ' . $path);
}
- // PHP's curl bindings require a leading @ for POST files
- if ($file[0] != '@') {
- $file = '@' . $file;
+ if ($contentType === null && class_exists('finfo', false)) {
+ $finfo = new \finfo(FILEINFO_MIME_TYPE);
+ $contentType = $finfo->file($path);
}
- if (!is_readable(substr($file, 1))) {
- throw new RequestException('File cannot be opened for reading: ' . $file);
+ $row = array(
+ 'file' => $path,
+ 'type' => $contentType
+ );
+
+ if (!isset($this->postFiles[$fieldName])) {
+ $this->postFiles[$fieldName] = array($row);
+ } else {
+ $this->postFiles[$fieldName][] = $row;
}
- $this->postFields->add($key, $file);
+ if ($process) {
+ $this->processPostFields();
+ }
}
- $this->processPostFields();
-
return $this;
}
/**
- * Remove a POST field or file by name
+ * Add POST files to use in the upload
*
- * @param string $field Name of the POST field or file to remove
+ * @param array $files An array of POST fields => filenames. If a filename
+ * is an array, it must contain a 'file' and 'type' key mapping to the
+ * path to the file and the Content-Type of the file.
*
* @return EntityEnclosingRequest
+ * @throws RequestException if the file cannot be read
*/
- public function removePostField($field)
+ public function addPostFiles(array $files)
{
- $this->postFields->remove($field);
+ foreach ($files as $key => $file) {
+
+ // Convert non-associative array keys into 'file'
+ if (is_numeric($key)) {
+ $key = 'file';
+ }
+
+ // If an array is passed for the file, then it contains
+ // Content-Disposition parameters.
+ if (is_array($file) && array_key_exists('type', $file) && array_key_exists('file', $file)) {
+ $contentType = $file['type'];
+ $file = $file['file'];
+ } elseif (is_string($file)) {
+ $contentType = null;
+ } else {
+ throw new RequestException('File must be a string or array');
+ }
+
+ // Remove the leading @ symbol
+ if (strpos($file, '@') !== false) {
+ $file = substr($file, 1);
+ }
+
+ // Add the POST file and remove any leading @ symbols
+ $this->addPostFile($key, $file, $contentType, false);
+ }
+
$this->processPostFields();
return $this;
@@ -225,12 +309,12 @@ public function removePostField($field)
*/
protected function processPostFields()
{
- if (0 == count($this->getPostFiles())) {
+ if (empty($this->postFiles)) {
$this->setHeader('Content-Type', 'application/x-www-form-urlencoded');
$this->removeHeader('Expect');
} else {
- $this->setHeader('Expect', '100-Continue');
- $this->setHeader('Content-Type', 'multipart/form-data');
+ $this->setHeader('Expect', '100-Continue')
+ ->setHeader('Content-Type', 'multipart/form-data');
$this->postFields->setEncodeFields(false)->setEncodeValues(false);
}
}
Oops, something went wrong.

0 comments on commit 29f6fae

Please sign in to comment.