I can't seem to find a real answer to this problem so here I go:
How do you parse raw HTTP request data in multipart/form-data format in PHP? I know that raw POST is automatically parsed if formatted correctly, but the data I'm referring to is coming from a PUT request, which is not being parsed automatically by PHP. The data is multipart and looks something like:
------------------------------b2449e94a11c
Content-Disposition: form-data; name="user_id"
3
------------------------------b2449e94a11c
Content-Disposition: form-data; name="post_id"
5
------------------------------b2449e94a11c
Content-Disposition: form-data; name="image"; filename="/tmp/current_file"
Content-Type: application/octet-stream
�����JFIF���������... a bunch of binary data
I'm sending the data with libcurl like so (pseudo code):
curl_setopt_array(
CURLOPT_POSTFIELDS => array(
'user_id' => 3,
'post_id' => 5,
'image' => '#/tmp/current_file'),
CURLOPT_CUSTOMREQUEST => 'PUT'
);
If I drop the CURLOPT_CUSTOMREQUEST bit, the request is handled as a POST on the server and everything is parsed just fine.
Is there a way to manually invoke PHPs HTTP data parser or some other nice way of doing this?
And yes, I have to send the request as PUT :)
Edit - please read first: this answer is still getting regular hits 7 years later. I have never used this code since then and do not know if there is a better way to do it these days. Please view the comments below and know that there are many scenarios where this code will not work. Use at your own risk.
--
Ok, so with Dave and Everts suggestions I decided to parse the raw request data manually. I didn't find any other way to do this after searching around for about a day.
I got some help from this thread. I didn't have any luck tampering with the raw data like they do in the referenced thread, as that will break the files being uploaded. So it's all regex. This wasnt't tested very well, but seems to be working for my work case. Without further ado and in the hope that this may help someone else someday:
function parse_raw_http_request(array &$a_data)
{
// read incoming data
$input = file_get_contents('php://input');
// grab multipart boundary from content type header
preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches);
$boundary = $matches[1];
// split content by boundary and get rid of last -- element
$a_blocks = preg_split("/-+$boundary/", $input);
array_pop($a_blocks);
// loop data blocks
foreach ($a_blocks as $id => $block)
{
if (empty($block))
continue;
// you'll have to var_dump $block to understand this and maybe replace \n or \r with a visibile char
// parse uploaded files
if (strpos($block, 'application/octet-stream') !== FALSE)
{
// match "name", then everything after "stream" (optional) except for prepending newlines
preg_match('/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s', $block, $matches);
}
// parse all other fields
else
{
// match "name" and optional value in between newline sequences
preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $block, $matches);
}
$a_data[$matches[1]] = $matches[2];
}
}
Usage by reference (in order not to copy around the data too much):
$a_data = array();
parse_raw_http_request($a_data);
var_dump($a_data);
I used Chris's example function and added some needed functionality, such as R Porter's need for array's of $_FILES. Hope it helps some people.
Here is the class & example usage
<?php
include_once('class.stream.php');
$data = array();
new stream($data);
$_PUT = $data['post'];
$_FILES = $data['file'];
/* Handle moving the file(s) */
if (count($_FILES) > 0) {
foreach($_FILES as $key => $value) {
if (!is_uploaded_file($value['tmp_name'])) {
/* Use getimagesize() or fileinfo() to validate file prior to moving here */
rename($value['tmp_name'], '/path/to/uploads/'.$value['name']);
} else {
move_uploaded_file($value['tmp_name'], '/path/to/uploads/'.$value['name']);
}
}
}
I would suspect the best way to go about it is 'doing it yourself', although you might find inspiration in multipart email parsers that use a similar (if not the exact same) format.
Grab the boundary from the Content-Type HTTP header, and use that to explode the various parts of the request. If the request is very large, keep in mind that you might store the entire request in memory, possibly even multiple times.
The related RFC is RFC2388, which fortunately is pretty short.
I'm surprised no one mentioned parse_str or mb_parse_str:
$result = [];
$rawPost = file_get_contents('php://input');
mb_parse_str($rawPost, $result);
var_dump($result);
http://php.net/manual/en/function.mb-parse-str.php
I haven't dealt with http headers much, but found this bit of code that might help
function http_parse_headers( $header )
{
$retVal = array();
$fields = explode("\r\n", preg_replace('/\x0D\x0A[\x09\x20]+/', ' ', $header));
foreach( $fields as $field ) {
if( preg_match('/([^:]+): (.+)/m', $field, $match) ) {
$match[1] = preg_replace('/(?<=^|[\x09\x20\x2D])./e', 'strtoupper("\0")', strtolower(trim($match[1])));
if( isset($retVal[$match[1]]) ) {
$retVal[$match[1]] = array($retVal[$match[1]], $match[2]);
} else {
$retVal[$match[1]] = trim($match[2]);
}
}
}
return $retVal;
}
From http://php.net/manual/en/function.http-parse-headers.php
Here is a universal solution working with arbitrary multipart/form-data content and tested for POST, PUT, and PATCH:
/**
* Parse arbitrary multipart/form-data content
* Note: null result or null values for headers or value means error
* #return array|null [{"headers":array|null,"value":string|null}]
* #param string|null $boundary
* #param string|null $content
*/
function parse_multipart_content(?string $content, ?string $boundary): ?array {
if(empty($content) || empty($boundary)) return null;
$sections = array_map("trim", explode("--$boundary", $content));
$parts = [];
foreach($sections as $section) {
if($section === "" || $section === "--") continue;
$fields = explode("\r\n\r\n", $section);
if(preg_match_all("/([a-z0-9-_]+)\s*:\s*([^\r\n]+)/iu", $fields[0] ?? "", $matches, PREG_SET_ORDER) === 2) {
$headers = [];
foreach($matches as $match) $headers[$match[1]] = $match[2];
} else $headers = null;
$parts[] = ["headers" => $headers, "value" => $fields[1] ?? null];
}
return empty($parts) ? null : $parts;
}
Update
The function was updated to support arrays in form fields. That is fields like level1[level2] will be translated into proper (multidimensional) arrays.
I've just added a small function to my HTTP20 library, that can help with this. It is made to parse form data for PUT, DELETE and PATCH and add it to respective static variable to simulate $_POST global.
For now it's just for text fields, though, no binary support, since I currently do not have a good use case in my project to properly test it and I'd prefer not to share something I can't test extensively. But if I do get to it at some point - I will update this answer.
Here is the code:
public function multiPartFormParse(): void
{
#Get method
$method = $_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'] ?? $_SERVER['REQUEST_METHOD'] ?? null;
#Get Content-Type
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
#Exit if not one of the supported methods or wrong content-type
if (!in_array($method, ['PUT', 'DELETE', 'PATCH']) || preg_match('/^multipart\/form-data; boundary=.*$/ui', $contentType) !== 1) {
return;
}
#Get boundary value
$boundary = preg_replace('/(^multipart\/form-data; boundary=)(.*$)/ui', '$2', $contentType);
#Get input stream
$formData = file_get_contents('php://input');
#Exit if failed to get the input or if it's not compliant with the RFC2046
if ($formData === false || preg_match('/^\s*--'.$boundary.'.*\s*--'.$boundary.'--\s*$/muis', $formData) !== 1) {
return;
}
#Strip ending boundary
$formData = preg_replace('/(^\s*--'.$boundary.'.*)(\s*--'.$boundary.'--\s*$)/muis', '$1', $formData);
#Split data into array of fields
$formData = preg_split('/\s*--'.$boundary.'\s*Content-Disposition: form-data;\s*/muis', $formData, 0, PREG_SPLIT_NO_EMPTY);
#Convert to associative array
$parsedData = [];
foreach ($formData as $field) {
$name = preg_replace('/(name=")(?<name>[^"]+)("\s*)(?<value>.*$)/mui', '$2', $field);
$value = preg_replace('/(name=")(?<name>[^"]+)("\s*)(?<value>.*$)/mui', '$4', $field);
#Check if we have multiple keys
if (str_contains($name, '[')) {
#Explode keys into array
$keys = explode('[', trim($name));
$name = '';
#Build JSON array string from keys
foreach ($keys as $key) {
$name .= '{"' . rtrim($key, ']') . '":';
}
#Add the value itself (as string, since in this case it will always be a string) and closing brackets
$name .= '"' . trim($value) . '"' . str_repeat('}', count($keys));
#Convert into actual PHP array
$array = json_decode($name, true);
#Check if we actually got an array and did not fail
if (!is_null($array)) {
#"Merge" the array into existing data. Doing recursive replace, so that new fields will be added, and in case of duplicates, only the latest will be used
$parsedData = array_replace_recursive($parsedData, $array);
}
} else {
#Single key - simple processing
$parsedData[trim($name)] = trim($value);
}
}
#Update static variable based on method value
self::${'_'.strtoupper($method)} = $parsedData;
}
Obviously you can safely remove method check and assignment to a static, if you do not those.
Have you looked at fopen("php://input", "r") for parsing the content?
Headers can also be found as $_SERVER['HTTP_*'], names are always uppercased and dashes become underscores, eg $_SERVER['HTTP_ACCEPT_LANGUAGE'].
Related
I have to add a GET variable to a url. But the URL might already have GET variables. What's the most efficient way to add this new variable?
Example URLs:
http://domain.com/
http://domain.com/index.html?name=jones
I need to add: tag=xyz:
http://domain.com/?tag=xyz
http://domain.com/index.html?name=jones&tag=xyz
What's the most efficient way to know whether to prepend my string with a ? or &?
Here's a version of the function I have so far:
// where arrAdditions looks like array('key1'=>'value1','key2'=>'value2');
function appendUrlQueryString($url, $arrAdditions) {
$arrQueryStrings = array();
foreach ($arrAdditions as $k=>$v) {
$arrQueryStrings[] = $k . '=' . $v;
}
$strAppend = implode('&',$arrQueryStrings);
if (strpos($url, '?') !== false) {
$url .= '&' . $strAppend;
} else {
$url = '?' . $strAppend;
}
return $url;
}
But, is simply looking for the ? in the existing url problematic? For example, is it possible that a url includes a ? but not actual queries, perhaps as an escaped character?
Take a look at PHP PECL's http_build_url. Said by the doc page:
Build a URL.
The parts of the second URL will be merged into the first according to the flags argument.
Addition:
If you don't have PECL installed, we can jump through some hoops. This approach is somewhat solid right up until we try to rebuild the new URL. Stock PHP (minus PECL) doesn't have a reverse of parse_url(). Making it harder, parse_url() removes some of the grammar from a URL in the resulting parts array so we have to put them back in when we reassemble. http_build_url() can take care of this for us, but if it were available, you wouldn't be reading this portion as it's what I originally recommended. Anyway, here's that code:
<?php
/**
* addQueryParam - given a URL and some new params for its query string, return the modified URL
*
* #see http://us1.php.net/parse_url
* #see http://us1.php.net/parse_str
* #throws Exception on bad input
* #param STRING $url A parseable URL to add query params to
* #param MIXED $input_query_vars - STRING of & separated pairs of = separated key values OR ASSOCIATIVE ARRAY of STRING keys => STRING values
* #return STRING new URL
*/
function addQueryParam ($url, $input_query_vars) {
// Parse new parameters
if (is_string($input_query_vars)) {
parse_str($input_query_vars, $input_query_vars);
}
// Ensure array of parameters now available
if (!is_array($input_query_vars)) {
throw new Exception(__FUNCTION__ . ' expects associative array or query string as second parameter.');
}
// Break up given URL
$url_parts = parse_url($url);
// Test for proper URL parse
if (!is_array($url_parts)) {
throw new Exception(__FUNCTION__ . ' expects parseable URL as first parameter');
}
// Produce array of original query vars
$original_query_vars = array();
if (isset($url_parts['query']) && $url_parts['query'] !== '') {
parse_str($url_parts['query'], $original_query_vars);
}
// Merge new params inot original
$new_query_vars = array_merge($original_query_vars, $input_query_vars);
// replace the original query string
$url_parts['query'] = http_build_query($new_query_vars);
// Put URL grammar back in place
if (!empty($url_parts['scheme'])) {
$url_parts['scheme'] .= '://';
}
if (!empty($url_parts['query'])) {
$url_parts['query'] = '?' . $url_parts['query'];
}
if (!empty($url_parts['fragment'])) {
$url_parts['fragment'] = '#' . $url_parts['fragment'];
}
// Put it all back together and return it
return implode('', $url_parts);
}
// Your demo URLs
$url1 = 'http://domain.com/';
$url2 = 'http://domain.com/index.html?name=jones';
//Some usage (I did this from CLI)
echo $url1, "\n";
echo addQueryParam($url1, 'tag=xyz'), "\n";
echo addQueryParam($url1, array('tag' => 'xyz')), "\n";
echo $url2, "\n";
echo addQueryParam($url2, 'tag=xyz'), "\n";
echo addQueryParam($url2, array('tag' => 'xyz')), "\n";
echo addQueryParam($url2, array('name' => 'foo', 'tag' => 'xyz')), "\n";
To check if parameter already exists you could try parse_str().
This will parse your URL and put variables into an array.
It will give you some issues if you will pass full URL:
$url = "http://domain.com/index.html?name=jones&tag=xyz";
parse_str($url', $arr);
print_r($arr);
will give you
Array ( [http://domain_com/index_html?name] => jones [tag] => xyz )
but with
$url = "name=jones&tag=xyz";
you will get
Array ( [name] => jones [tag] => xyz )
You could explode full URL by '?' and check the second part. After the check you could know how to modify your URL. But I'm not sure this would work all the time.
$url_one = "http://www.stackoverflow.com?action=submit&id=example";
$new_params = "user=john&pass=123";
$final_url = $url_one."&".$new_param
s;
Now $final_url has old url and new params added to it.
After making a gzip deflate request in PHP, I receive the deflated string in offset chunks, which looks like the following
Example shortened greatly to show format:
00001B4E
¾”kŒj…Øæ’ìÑ«F1ìÊ`+ƒQì¹UÜjùJƒZ\µy¡ÓUžGr‡J&=KLËÙÍ~=ÍkR
0000102F
ñÞœÞôΑüo[¾”+’Ñ8#à»0±R-4VÕ’n›êˆÍ.MCŽ…ÏÖr¿3M—èßñ°r¡\+
00000000
I'm unable to inflate that presumably because of the chunked format. I can confirm the data is not corrupt after manually removing the offsets with a Hex editor and reading the gzip archive. I'm wondering if there's a proper method to parse this chunked gzip deflated response into a readable string?
I might be able to split these offsets and join the data together in one string to call gzinflate, but it seems there must be an easier way.
The proper method to deflate a chunked response is roughly as follows:
initialise string to hold result
for each chunk {
check that the stated chunk length equals the string length of the chunk
append the chunk data to the result variable
}
Here's a handy PHP function to do that for you (FIXED):
function unchunk_string ($str) {
// A string to hold the result
$result = '';
// Split input by CRLF
$parts = explode("\r\n", $str);
// These vars track the current chunk
$chunkLen = 0;
$thisChunk = '';
// Loop the data
while (($part = array_shift($parts)) !== NULL) {
if ($chunkLen) {
// Add the data to the string
// Don't forget, the data might contain a literal CRLF
$thisChunk .= $part."\r\n";
if (strlen($thisChunk) == $chunkLen) {
// Chunk is complete
$result .= $thisChunk;
$chunkLen = 0;
$thisChunk = '';
} else if (strlen($thisChunk) == $chunkLen + 2) {
// Chunk is complete, remove trailing CRLF
$result .= substr($thisChunk, 0, -2);
$chunkLen = 0;
$thisChunk = '';
} else if (strlen($thisChunk) > $chunkLen) {
// Data is malformed
return FALSE;
}
} else {
// If we are not in a chunk, get length of the new one
if ($part === '') continue;
if (!$chunkLen = hexdec($part)) break;
}
}
// Return the decoded data of FALSE if it is incomplete
return ($chunkLen) ? FALSE : $result;
}
To decode a String use gzinflate, Zend_Http_Client lib will help to do this kind of common tasks, its wasy to use, Refer Zend_Http_Response code if you need to do it on your own
The solution from user #user1309276 really helped me! Received from the server a gzip-compressed json response with transfer-encoding: chunked header. None of the solutions helped. This solution works like magic for me! It just remove the first 10 bytes.
$data = json_decode(gzinflate(substr($response->getContent(), 10)), true);
I'm writing a RESTful API. I'm having trouble with uploading images using the different verbs.
Consider:
I have an object which can be created/modified/deleted/viewed via a post/put/delete/get request to a URL. The request is multi part form when there is a file to upload, or application/xml when there's just text to process.
To handle the image uploads which are associated with the object I am doing something like:
if(isset($_FILES['userfile'])) {
$data = $this->image_model->upload_image();
if($data['error']){
$this->response(array('error' => $error['error']));
}
$xml_data = (array)simplexml_load_string( urldecode($_POST['xml']) );
$object = (array)$xml_data['object'];
} else {
$object = $this->body('object');
}
The major problem here is when trying to handle a put request, obviously $_POST doesn't contain the put data (as far as I can tell!).
For reference this is how I'm building the requests:
curl -F userfile=#./image.png -F xml="<xml><object>stuff to edit</object></xml>"
http://example.com/object -X PUT
Does anyone have any ideas how I can access the xml variable in my PUT request?
First of all, $_FILES is not populated when handling PUT requests. It is only populated by PHP when handling POST requests.
You need to parse it manually. That goes for "regular" fields as well:
// Fetch content and determine boundary
$raw_data = file_get_contents('php://input');
$boundary = substr($raw_data, 0, strpos($raw_data, "\r\n"));
// Fetch each part
$parts = array_slice(explode($boundary, $raw_data), 1);
$data = array();
foreach ($parts as $part) {
// If this is the last part, break
if ($part == "--\r\n") break;
// Separate content from headers
$part = ltrim($part, "\r\n");
list($raw_headers, $body) = explode("\r\n\r\n", $part, 2);
// Parse the headers list
$raw_headers = explode("\r\n", $raw_headers);
$headers = array();
foreach ($raw_headers as $header) {
list($name, $value) = explode(':', $header);
$headers[strtolower($name)] = ltrim($value, ' ');
}
// Parse the Content-Disposition to get the field name, etc.
if (isset($headers['content-disposition'])) {
$filename = null;
preg_match(
'/^(.+); *name="([^"]+)"(; *filename="([^"]+)")?/',
$headers['content-disposition'],
$matches
);
list(, $type, $name) = $matches;
isset($matches[4]) and $filename = $matches[4];
// handle your fields here
switch ($name) {
// this is a file upload
case 'userfile':
file_put_contents($filename, $body);
break;
// default for all other files is to populate $data
default:
$data[$name] = substr($body, 0, strlen($body) - 2);
break;
}
}
}
At each iteration, the $data array will be populated with your parameters, and the $headers array will be populated with the headers for each part (e.g.: Content-Type, etc.), and $filename will contain the original filename, if supplied in the request and is applicable to the field.
Take note the above will only work for multipart content types. Make sure to check the request Content-Type header before using the above to parse the body.
Please don't delete this again, it's helpful to a majority of people coming here! All previous answers were partial answers that don't cover the solution as a majority of people asking this question would want.
This takes what has been said above and additionally handles multiple file uploads and places them in $_FILES as someone would expect. To get this to work, you have to add 'Script PUT /put.php' to your Virtual Host for the project per Documentation. I also suspect I'll have to setup a cron to cleanup any '.tmp' files.
private function _parsePut( )
{
global $_PUT;
/* PUT data comes in on the stdin stream */
$putdata = fopen("php://input", "r");
/* Open a file for writing */
// $fp = fopen("myputfile.ext", "w");
$raw_data = '';
/* Read the data 1 KB at a time
and write to the file */
while ($chunk = fread($putdata, 1024))
$raw_data .= $chunk;
/* Close the streams */
fclose($putdata);
// Fetch content and determine boundary
$boundary = substr($raw_data, 0, strpos($raw_data, "\r\n"));
if(empty($boundary)){
parse_str($raw_data,$data);
$GLOBALS[ '_PUT' ] = $data;
return;
}
// Fetch each part
$parts = array_slice(explode($boundary, $raw_data), 1);
$data = array();
foreach ($parts as $part) {
// If this is the last part, break
if ($part == "--\r\n") break;
// Separate content from headers
$part = ltrim($part, "\r\n");
list($raw_headers, $body) = explode("\r\n\r\n", $part, 2);
// Parse the headers list
$raw_headers = explode("\r\n", $raw_headers);
$headers = array();
foreach ($raw_headers as $header) {
list($name, $value) = explode(':', $header);
$headers[strtolower($name)] = ltrim($value, ' ');
}
// Parse the Content-Disposition to get the field name, etc.
if (isset($headers['content-disposition'])) {
$filename = null;
$tmp_name = null;
preg_match(
'/^(.+); *name="([^"]+)"(; *filename="([^"]+)")?/',
$headers['content-disposition'],
$matches
);
list(, $type, $name) = $matches;
//Parse File
if( isset($matches[4]) )
{
//if labeled the same as previous, skip
if( isset( $_FILES[ $matches[ 2 ] ] ) )
{
continue;
}
//get filename
$filename = $matches[4];
//get tmp name
$filename_parts = pathinfo( $filename );
$tmp_name = tempnam( ini_get('upload_tmp_dir'), $filename_parts['filename']);
//populate $_FILES with information, size may be off in multibyte situation
$_FILES[ $matches[ 2 ] ] = array(
'error'=>0,
'name'=>$filename,
'tmp_name'=>$tmp_name,
'size'=>strlen( $body ),
'type'=>$value
);
//place in temporary directory
file_put_contents($tmp_name, $body);
}
//Parse Field
else
{
$data[$name] = substr($body, 0, strlen($body) - 2);
}
}
}
$GLOBALS[ '_PUT' ] = $data;
return;
}
For whom using Apiato (Laravel) framework:
create new Middleware like file below, then declair this file in your laravel kernel file within the protected $middlewareGroups variable (inside web or api, whatever you want) like this:
protected $middlewareGroups = [
'web' => [],
'api' => [HandlePutFormData::class],
];
<?php
namespace App\Ship\Middlewares\Http;
use Closure;
use Symfony\Component\HttpFoundation\ParameterBag;
/**
* #author Quang Pham
*/
class HandlePutFormData
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
*
* #return mixed
*/
public function handle($request, Closure $next)
{
if ($request->method() == 'POST' or $request->method() == 'GET') {
return $next($request);
}
if (preg_match('/multipart\/form-data/', $request->headers->get('Content-Type')) or
preg_match('/multipart\/form-data/', $request->headers->get('content-type'))) {
$parameters = $this->decode();
$request->merge($parameters['inputs']);
$request->files->add($parameters['files']);
}
return $next($request);
}
public function decode()
{
$files = [];
$data = [];
// Fetch content and determine boundary
$rawData = file_get_contents('php://input');
$boundary = substr($rawData, 0, strpos($rawData, "\r\n"));
// Fetch and process each part
$parts = $rawData ? array_slice(explode($boundary, $rawData), 1) : [];
foreach ($parts as $part) {
// If this is the last part, break
if ($part == "--\r\n") {
break;
}
// Separate content from headers
$part = ltrim($part, "\r\n");
list($rawHeaders, $content) = explode("\r\n\r\n", $part, 2);
$content = substr($content, 0, strlen($content) - 2);
// Parse the headers list
$rawHeaders = explode("\r\n", $rawHeaders);
$headers = array();
foreach ($rawHeaders as $header) {
list($name, $value) = explode(':', $header);
$headers[strtolower($name)] = ltrim($value, ' ');
}
// Parse the Content-Disposition to get the field name, etc.
if (isset($headers['content-disposition'])) {
$filename = null;
preg_match(
'/^form-data; *name="([^"]+)"(; *filename="([^"]+)")?/',
$headers['content-disposition'],
$matches
);
$fieldName = $matches[1];
$fileName = (isset($matches[3]) ? $matches[3] : null);
// If we have a file, save it. Otherwise, save the data.
if ($fileName !== null) {
$localFileName = tempnam(sys_get_temp_dir(), 'sfy');
file_put_contents($localFileName, $content);
$files = $this->transformData($files, $fieldName, [
'name' => $fileName,
'type' => $headers['content-type'],
'tmp_name' => $localFileName,
'error' => 0,
'size' => filesize($localFileName)
]);
// register a shutdown function to cleanup the temporary file
register_shutdown_function(function () use ($localFileName) {
unlink($localFileName);
});
} else {
$data = $this->transformData($data, $fieldName, $content);
}
}
}
$fields = new ParameterBag($data);
return ["inputs" => $fields->all(), "files" => $files];
}
private function transformData($data, $name, $value)
{
$isArray = strpos($name, '[]');
if ($isArray && (($isArray + 2) == strlen($name))) {
$name = str_replace('[]', '', $name);
$data[$name][]= $value;
} else {
$data[$name] = $value;
}
return $data;
}
}
Pls note: Those codes above not all mine, some from above comment, some modified by me.
Quoting netcoder reply : "Take note the above will only work for multipart content types"
To work with any content type I have added the following lines to Mr. netcoder's solution :
// Fetch content and determine boundary
$raw_data = file_get_contents('php://input');
$boundary = substr($raw_data, 0, strpos($raw_data, "\r\n"));
/*...... My edit --------- */
if(empty($boundary)){
parse_str($raw_data,$data);
return $data;
}
/* ........... My edit ends ......... */
// Fetch each part
$parts = array_slice(explode($boundary, $raw_data), 1);
$data = array();
............
...............
I've been trying to figure out how to work with this issue without having to break RESTful convention and boy howdie, what a rabbit hole, let me tell you.
I'm adding this anywhere I can find in the hope that it will help somebody out in the future.
I've just lost a day of development firstly figuring out that this was an issue, then figuring out where the issue lay.
As mentioned, this isn't a symfony (or laravel, or any other framework) issue, it's a limitation of PHP.
After trawling through a good few RFCs for php core, the core development team seem somewhat resistant to implementing anything to do with modernising the handling of HTTP requests. The issue was first reported in 2011, it doesn't look any closer to having a native solution.
That said, I managed to find this PECL extension called Always Populate Form Data. I'm not really very familiar with pecl, and couldn't seem to get it working using pear. but I'm using CentOS and Remi PHP which has a yum package.
I ran yum install php-pecl-apfd and it literally fixed the issue straight away (well I had to restart my docker containers but that was a given).
I believe there are other packages in various flavours of linux and I'm sure anybody with more knowledge of pear/pecl/general php extensions could get it running on windows or mac with no issue.
I know this article is old.
But unfortunately, PHP still does not pay attention to form-data other than the Post method.
Thanks to friends (#netcoder, #greendot, #pham-quang) who suggested solutions above.
Using those solutions I wrote a library for this purpose:
composer require alireaza/php-form-data
You can also use composer require alireaza/laravel-form-data in Laravel.
I have a problem right now with CodeIgniter : I use the REST Controller library (which is really awesome) to create an API but I can not get PUT requests...
This is my code :
function user_put() {
$user_id = $this->get("id");
echo $user_id;
$username = $this->put("username");
echo $username;
}
I use curl to make the request :
curl -i -X PUT -d "username=test" http://[...]/user/id/1
The user_id is full but the username variable is empty. Yet it works with the verbs POST and GET.
Have you any idea please?
Thank you !
According to: http://net.tutsplus.com/tutorials/php/working-with-restful-services-in-codeigniter-2/ we should consult https://github.com/philsturgeon/codeigniter-restserver/blob/master/application/libraries/REST_Controller.php#L544 to see that this method:
/**
* Detect method
*
* Detect which method (POST, PUT, GET, DELETE) is being used
*
* #return string
*/
protected function _detect_method() {
$method = strtolower($this->input->server('REQUEST_METHOD'));
if ($this->config->item('enable_emulate_request')) {
if ($this->input->post('_method')) {
$method = strtolower($this->input->post('_method'));
} else if ($this->input->server('HTTP_X_HTTP_METHOD_OVERRIDE')) {
$method = strtolower($this->input->server('HTTP_X_HTTP_METHOD_OVERRIDE'));
}
}
if (in_array($method, array('get', 'delete', 'post', 'put'))) {
return $method;
}
return 'get';
}
looks to see if we've defined the HTTP header HTTP_X_HTTP_METHOD_OVERRIDE and it uses that in favor of the actual verb we've implemented on the web. To use this in a request you would specify the header X-HTTP-Method-Override: method (so X-HTTP-Method-Override: put) to generate a custom method override. Sometimes the framework expects X-HTTP-Method instead of X-HTTP-Method-Override so this varies by framework.
If you were doing such a request via jQuery, you would integrate this chunk into your ajax request:
beforeSend: function (XMLHttpRequest) {
//Specify the HTTP method DELETE to perform a delete operation.
XMLHttpRequest.setRequestHeader("X-HTTP-Method-Override", "DELETE");
}
You can try to detect the method type first and seperate the different cases. If your controller only handles REST functions it could be helpful to put get the required information in the constructor.
switch($_SERVER['REQUEST_METHOD']){
case 'GET':
$var_array=$this->input->get();
...
break;
case 'POST':
$var_array=$this->input->post();
...
break;
case 'PUT':
case 'DELETE':
parse_str(file_get_contents("php://input"),$var_array);
...
break;
default:
echo "I don't know how to handle this request.";
}
In CodeIgniter 4 use getRawInput which will retrieve data and convert it to an array.
$data = $request->getRawInput();
look this issue in github
PUT parameters only work in JSON format
https://github.com/chriskacerguis/codeigniter-restserver/issues/362
Checkout this link in the official Code Igniter Docs Using the Input Stream for Custom Request Methods
This is the Code Igniter way to do it.
Just call the below if the body of the request is form-urlencoded
$var1 = $this->input->input_stream('var_key')
// Or
$var1 = $this->security->xss_clean($this->input->input_stream('var_key'));
Codeigniter put_stream has provided no help, instead I had to use php input stream as following method can be added to helpers, from there you can parse put request in any of the controllers:
function parsePutRequest()
{
// Fetch content and determine boundary
$raw_data = file_get_contents('php://input');
$boundary = substr($raw_data, 0, strpos($raw_data, "\r\n"));
// Fetch each part
$parts = array_slice(explode($boundary, $raw_data), 1);
$data = array();
foreach ($parts as $part) {
// If this is the last part, break
if ($part == "--\r\n") break;
// Separate content from headers
$part = ltrim($part, "\r\n");
list($raw_headers, $body) = explode("\r\n\r\n", $part, 2);
// Parse the headers list
$raw_headers = explode("\r\n", $raw_headers);
$headers = array();
foreach ($raw_headers as $header) {
list($name, $value) = explode(':', $header);
$headers[strtolower($name)] = ltrim($value, ' ');
}
// Parse the Content-Disposition to get the field name, etc.
if (isset($headers['content-disposition'])) {
$filename = null;
preg_match(
'/^(.+); *name="([^"]+)"(; *filename="([^"]+)")?/',
$headers['content-disposition'],
$matches
);
list(, $type, $name) = $matches;
isset($matches[4]) and $filename = $matches[4];
// handle your fields here
switch ($name) {
// this is a file upload
case 'userfile':
file_put_contents($filename, $body);
break;
// default for all other files is to populate $data
default:
$data[$name] = substr($body, 0, strlen($body) - 2);
break;
}
}
}
return $data;
}
CodeIgniter doesn't support reading incoming PUT requests and if it's not essential I would stick to GET/POST for your API as its probably not necessary.
If you do need to read PUT requests take a look at Accessing Incoming PUT Data from PHP.
I had an interesting task today and couldn't find much on the subject. I wanted to share this, and ask for any suggestions on how this could have been done more elegantly. I consider myself a mediocre programmer who really wants to improve so any feedback is highly appreciated. There is also a strange bug I can't figure out. So here goes..and hopefully this helps someone who ever has to do something similar.
A client was redoing a site, moving content around, and had a couple thousand redirects that needed to be made. Marketing sent me an XLS with old URLs in one column, new URLs in the next. These were the actions I took:
Saved the XLS as CSV
Wrote a script which:
Formatted the list as valid 301 redirects
Exported the list to a text file
I then copy / pasted all the new directives into my .htaccess file.
Then, I wrote another script that checked to make sure each of the new links was valid (no 404s). The first script worked exactly as expected. For some reason, I can get the second script to print out all the 404 errors (there were several), but the script doesn't die when it finishes traversing the loop, and it doesn't write to the file, it just hangs in command line. No errors get reported. Any idea what's going on? Here is the code for both scripts:
Formatting 301s:
<?php
$source = "301.csv";
$output = "301.txt";
//grab the contents of the source file as an array, prepare the output file for writing
$sourceArray = file($source);
$handleOutput = fopen($output, "w");
//Set the strings we want to replace in an array. The first array are the original lines and the second are the strings to be replaced
$originalLines = array(
'http://hipaasecurityassessment.com',
','
);
$replacementStrings = array(
'',
' '
);
//Split each item from the array into two strings, one which occurs before the comma and the other which occurs after
function setContent($sourceArray, $originalLines = array(), $replacementStrings = array()){
$outputArray = array();
$text = 'redirect 301 ';
foreach ($sourceArray as $number => $item){
$pattern = '/[,]/';
$item = preg_split($pattern, $item);
$item = array(
$item[0],
preg_replace('#"#', '', $item[1])
);
$item = implode(' ', $item);
$item = str_replace($originalLines, $replacementStrings, $item);
array_push($outputArray,$text,$item);
}
$outputString = implode('', $outputArray);
return $outputString;
}
//Invoke the set content function
$outputString = setContent($sourceArray, $originalLines, $replacementStrings);
//Finally, write to the text file!
fwrite($handleOutput, $outputString);
Checking for 404s:
<?php
$source = "301.txt";
$output = "print404.txt";
//grab the contents of the source file as an array, prepare the output file for writing
$sourceArray = file($source);
$handleOutput = fopen($output, "w");
//Split each item from the array into two strings, one which occurs before the space and the other which occurs after
function getUrls($sourceArray = array()){
$outputArray = array();
foreach ($sourceArray as $number => $item){
$item = str_replace('redirect 301', '', $item);
$pattern = '#[ ]+#';
$item = preg_split($pattern, $item);
$item = array(
$item[0],
$item[1],
$item[2]
);
array_push($outputArray, $item[2]);
}
return $outputArray;
}
//Check each URL for a 404 error via a curl request
function check404($url = array(), $handleOutput){
$handle = curl_init($url);
curl_setopt($handle, CURLOPT_RETURNTRANSFER, TRUE);
$content = curl_exec( $handle );
$response = curl_getinfo( $handle );
$httpCode = curl_getinfo($handle, CURLINFO_HTTP_CODE);
if($httpCode == 404) {
//fwrite($handleOutput, $url);
print $url;
}
};
$outputArray = getUrls($sourceArray);
foreach ($outputArray as $url)
{
$errors = check404($url, $handleOutput);
}
You should have used fgetcsv() for generating the original URL list. This splits up CSV files into an array, simplifying the transformation.
Can't say anything about the 404s or the error cause. But using the wacky curl functions is almost always a bad indicator. For testing purposes I would have used a commandline tool like wget instead so the results can be proof-checked manually.
But maybe you could try PHPs own get_headers() instead. It's supposed to show the raw result headers; shouldn't not follow redirects itself.