Skip to content

Instantly share code, notes, and snippets.

@yookoala
Last active December 9, 2020 15:48
Show Gist options
  • Save yookoala/58d5db5da16c6f404ef169eaf5a50249 to your computer and use it in GitHub Desktop.
Save yookoala/58d5db5da16c6f404ef169eaf5a50249 to your computer and use it in GitHub Desktop.
An example to emulate an HTML form POST request with file uploads

Multipart Upload with PHP

About

This is a simple example to do a "multipart/form-data" POST request in PHP without any fancy library.

Setup

You need to download all files into a folder. Then create an empty folder "uploads" in the same folder to store the uploaded files.

To serve the handle_upload.php file properly, run this command in the folder:

php -S 127.0.0.1:8080 -t .

The file handle_upload.php should be accessible at "http://localhost:8080/handle_upload.php".

When all the aboves are ready, run this:

php upload.php ./test_data.txt

You can try other files than "test_data.txt" to see the upload result.

The uploaded file should be available in the "./uploads" folder you created.

Inspiration

Inspired by the Stackoverflow question here.

<?php
/**
* A genertor that yields multipart form-data fragments (without the ending EOL).
* Would encode all files with base64 to make the request binary-safe.
*
* @param iterable $vars
* Key-value iterable (e.g. assoc array) of string or integer.
* Keys represents the field name.
* @param iterable $files
* Key-value iterable (e.g. assoc array) of file path string.
* Keys represents the field name of file upload.
*
* @return \Generator
* Generator of multipart form-data fragments (without the ending EOL) in assoc format,
* always contains 2 key-values:
* * header: An array of header for a key-value pair.
* * content: A value string (can contain binary content) of the key-value pair.
*/
function generate_multipart_data_parts(iterable $vars, iterable $files=[]): Generator {
// handle normal variables
foreach ($vars as $name => $value) {
$name = urlencode($name);
$value = urlencode($value);
yield [
'header' => ["Content-Disposition: form-data; name=\"{$name}\""],
'content' => $value,
];
}
// handle file contents
foreach ($files as $file_fieldname => $file_path) {
$file_fieldname = urlencode($file_fieldname);
$file_data = file_get_contents($file_path);
yield [
'header' => [
"Content-Disposition: form-data; name=\"{$file_fieldname}\"; filename=\"".basename($file_path)."\"",
"Content-Type: application/octet-stream", // for binary safety
],
'content' => $file_data
];
}
}
/**
* Converts output of generate_multipart_data_parts() into form data.
*
* @param iterable $parts
* An iterator of form fragment arrays. See return data of
* generate_multipart_data_parts().
* @param string|null $boundary
* An optional pre-generated boundary string to use for wrapping data.
* Please reference section 7.2 "The Multipart Content-Type" in RFC1341.
*
* @return array
* An assoc array with 2 items:
* * boundary: the multipart boundary string
* * data: the data string (can contain binary data)
*/
function wrap_multipart_data(iterable $parts, ?string $boundary = null): array {
if (empty($boundary)) {
$boundary = 'boundary' . time();
}
$data = '';
foreach ($parts as $part) {
list('header' => $header, 'content' => $content) = $part;
// Check content for boundary.
// Note: Won't check header and expect the program makes sense there.
if (strstr($content, "\r\n$boundary") !== false) {
throw new \Exception('Error: data contains the multipart boundary');
}
$data .= "--{$boundary}\r\n";
$data .= implode("\r\n", $header) . "\r\n\r\n" . $content . "\r\n";
}
// signal end of request (note the trailing "--")
$data .= "--{$boundary}--\r\n";
return ['boundary' => $boundary, 'data' => $data];
}
<?php
echo "\$_POST ----------\n";
var_dump($_POST);
echo "\n";
echo "\$_FILES ---------\n";
$uploads_dir = __DIR__ . '/uploads';
foreach ($_FILES as $field => $file) {
if ($file['error'] === UPLOAD_ERR_OK) {
$now = sha1(microtime(true));
$tmp_name = $file["tmp_name"];
$pathinfo = pathinfo($file['name']);
while (is_file("{$uploads_dir}/{$field}_{$pathinfo['basename']}_{$now}.{$pathinfo['extension']}")) {
$now .= '(1)';
}
move_uploaded_file($tmp_name, "{$uploads_dir}/{$field}_{$pathinfo['basename']}_{$now}.{$pathinfo['extension']}");
echo "successfully uploaded {$field} ({$file['name']})" . PHP_EOL;
} else {
echo "failed to upload {$field} ({$file['name']})" . PHP_EOL;
}
}
echo "\n";
This is a dummy text file
for upload test.
<?php
require __DIR__ . '/functions.php';
$filepath = $argv[0] ?? __DIR__ . '/test_data.txt';
// build data for a multipart/form-data request
list('boundary' => $boundary, 'data' => $data) = wrap_multipart_data(generate_multipart_data_parts(
// normal form variables
[
'hello' => 'world',
'foo' => 'bar',
],
// files
[
'uploaded_file_test' => $filepath,
]
));
// Send POST REQUEST
$context_options = array(
'http' => array(
'method' => 'POST',
'header' => "Content-type: multipart/form-data; boundary={$boundary}\r\n"
. "Content-Length: " . strlen($data) . "\r\n",
'content' => $data,
'timeout' => 10,
)
);
$context = stream_context_create($context_options);
$result = fopen('http://localhost:8080/handle_upload.php', 'r', false, $context);
$response = stream_get_contents($result);
echo $response;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment