I have to download big file (1xx MB) using PHP.
How can i download this without wasting memory (RAM) for temporary file ?
When i use
$something=file_get_contents('http://somehost.example/file.zip');
file_put_contents($something,'myfile.zip');
I need to have so much memory that size of that file.
Maybe it's possible to download it using any other way ?
For example in parts (for example 1024b), write to disk, and download another part repeating until file will be fully downloaded ?
Copy the file one small chunk at a time
/**
* Copy remote file over HTTP one small chunk at a time.
*
* #param $infile The full URL to the remote file
* #param $outfile The path where to save the file
*/
function copyfile_chunked($infile, $outfile) {
$chunksize = 10 * (1024 * 1024); // 10 Megs
/**
* parse_url breaks a part a URL into it's parts, i.e. host, path,
* query string, etc.
*/
$parts = parse_url($infile);
$i_handle = fsockopen($parts['host'], 80, $errstr, $errcode, 5);
$o_handle = fopen($outfile, 'wb');
if ($i_handle == false || $o_handle == false) {
return false;
}
if (!empty($parts['query'])) {
$parts['path'] .= '?' . $parts['query'];
}
/**
* Send the request to the server for the file
*/
$request = "GET {$parts['path']} HTTP/1.1\r\n";
$request .= "Host: {$parts['host']}\r\n";
$request .= "User-Agent: Mozilla/5.0\r\n";
$request .= "Keep-Alive: 115\r\n";
$request .= "Connection: keep-alive\r\n\r\n";
fwrite($i_handle, $request);
/**
* Now read the headers from the remote server. We'll need
* to get the content length.
*/
$headers = array();
while(!feof($i_handle)) {
$line = fgets($i_handle);
if ($line == "\r\n") break;
$headers[] = $line;
}
/**
* Look for the Content-Length header, and get the size
* of the remote file.
*/
$length = 0;
foreach($headers as $header) {
if (stripos($header, 'Content-Length:') === 0) {
$length = (int)str_replace('Content-Length: ', '', $header);
break;
}
}
/**
* Start reading in the remote file, and writing it to the
* local file one chunk at a time.
*/
$cnt = 0;
while(!feof($i_handle)) {
$buf = '';
$buf = fread($i_handle, $chunksize);
$bytes = fwrite($o_handle, $buf);
if ($bytes == false) {
return false;
}
$cnt += $bytes;
/**
* We're done reading when we've reached the conent length
*/
if ($cnt >= $length) break;
}
fclose($i_handle);
fclose($o_handle);
return $cnt;
}
Adjust the $chunksize variable to your needs. This has only been mildly tested. It could easily break for a number of reasons.
Usage:
copyfile_chunked('http://somesite.com/somefile.jpg', '/local/path/somefile.jpg');
you can shell out to a wget using exec() this will result in the lowest memory usage.
<?php
exec("wget -o outputfilename.tar.gz http://pathtofile/file.tar.gz")
?>
You can also try using fopen() and fread() and fwrite(). That way you onlly download x bytes into memory at a time.
Related
For Windows Chrome (and probably many other browsers), this code works for serving an mp3 in an audio element:
/**
*
* #param string $filename
* #return \Illuminate\Http\Response|\Illuminate\Contracts\Routing\ResponseFactory
*/
public function getMp3($filename) {
$fileContents = Storage::disk(\App\Helpers\CoachingCallsHelper::DISK)->get($filename);
$fileSize = Storage::disk(\App\Helpers\CoachingCallsHelper::DISK)->size($filename);
$shortlen = $fileSize - 1;
$headers = [
'Accept-Ranges' => 'bytes',
'Content-Range' => 'bytes 0-' . $shortlen . '/' . $fileSize,
'Content-Type' => "audio/mpeg"
];
Log::debug('$headers=' . json_encode($headers));
$response = response($fileContents, 200, $headers);
return $response;
}
But when I use an iPhone to browse to the same page, the mp3 file does not show the total duration, and when I play it, it says "Live broadcast".
I've tried to follow suggestions from various answers of this question (HTML5 <audio> Safari live broadcast vs not) and other articles I've read, but none seem to have an effect.
No matter how I change the headers, the mp3 seems to function as desired on Windows and does not work on iOS.
How can I debug what I'm doing wrong?
Here is HTML:
<audio controls preload="auto">
<source src="{{$coachingCall->getMp3Url()}}" type="audio/mpeg"/>
<p>Your browser doesnt support embedded HTML5 audio. Here is a link to the audio instead.</p>
</audio>
MP3 files don't have timestamps, and therefore no inherent length that can be known ahead of time. Chrome is just guessing, based on the bitrate at the beginning of the file and the byte size of the file. It doesn't really know.
Some players don't bother guessing.
Also, all browsers on iOS are Safari under the hood, thanks to some incredibly restrictive policies by Apple. Therefore, Chrome on iOS is really just a wrapper for a Safari web view.
Whoa, that was a very difficult problem to solve. (It took me days.)
And I learned that it wasn't just iOS that was having problems: Safari on Mac hadn't been working either.
Now I think everything works on every browser I've tested.
I'm really glad I found this example to follow.
Here is my answer:
/**
*
* #param string $disk
* #param string $filename
* #return \Illuminate\Http\Response|\Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\StreamedResponse
*/
public static function getMediaFile($disk, $filename) {
$rangeHeader = request()->header('Range');
$fileContents = Storage::disk($disk)->get($filename);
$fullFilePath = Storage::disk($disk)->path($filename); //https://stackoverflow.com/a/49532280/470749
$headers = ['Content-Type' => Storage::disk($disk)->mimeType($fullFilePath)];
if ($rangeHeader) {
return self::getResponseStream($disk, $fullFilePath, $fileContents, $rangeHeader, $headers);
} else {
$httpStatusCode = 200;
return response($fileContents, $httpStatusCode, $headers);
}
}
/**
*
* #param string $disk
* #param string $fullFilePath
* #param string $fileContents
* #param string $rangeRequestHeader
* #param array $responseHeaders
* #return \Symfony\Component\HttpFoundation\StreamedResponse
*/
public static function getResponseStream($disk, $fullFilePath, $fileContents, $rangeRequestHeader, $responseHeaders) {
$stream = Storage::disk($disk)->readStream($fullFilePath);
$fileSize = strlen($fileContents);
$fileSizeMinusOneByte = $fileSize - 1; //because it is 0-indexed. https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.16
list($param, $rangeHeader) = explode('=', $rangeRequestHeader);
if (strtolower(trim($param)) !== 'bytes') {
abort(400, "Invalid byte range request"); //Note, this is not how https://stackoverflow.com/a/29997555/470749 did it
}
list($from, $to) = explode('-', $rangeHeader);
if ($from === '') {
$end = $fileSizeMinusOneByte;
$start = $end - intval($from);
} elseif ($to === '') {
$start = intval($from);
$end = $fileSizeMinusOneByte;
} else {
$start = intval($from);
$end = intval($to);
}
$length = $end - $start + 1;
$httpStatusCode = 206;
$responseHeaders['Content-Range'] = sprintf('bytes %d-%d/%d', $start, $end, $fileSize);
$responseStream = response()->stream(function() use ($stream, $start, $length) {
fseek($stream, $start, SEEK_SET);
echo fread($stream, $length);
fclose($stream);
}, $httpStatusCode, $responseHeaders);
return $responseStream;
}
I can't comment since I just made my account, so... complementing RYAN's
Just found out that you can save some loading time removing the
$fileContents = Storage::disk($disk)->get($filename);
And replacing it with
$fileSize = Storage::disk($disk)->size($filename);
Passing the size directly to the getResponseStream function, instead of downloading the whole content into a variable and then measuring the length.
Thank you Ryan, saved me a lot of precious time with the stinky safari.
I am handling imap in php via socket. Everything works perfect. I build mail swift object, then convert it to string and send to imap with append command, it works, I get OK [APPENDUID 1 4497] (Success), but if I attach file greater than about 3kb - imap server responds me nothing, and the mail doesn't append! What is the problem? May be I should make append something like part by part? I mean cut mail body for several parts and make several fwrite()?
These are several parts of my code. But I think problem is not in the code, but in some common imap stuff:
/**
* #param string $command
* #param string $successPattern
* #param bool $withCounter
* #return false|string
*/
protected function sendCommand($command, $successPattern = '', $withCounter = true)
{
$counter = $withCounter ? "{$this->commandHash}{$this->commandCounter} " : "";
$successPattern = !$successPattern ? $counter . 'OK' : $successPattern;
fwrite($this->stream, "{$counter}{$command}\r\n");
$this->commandCounter++;
$previousLine = '';
$buf = '';
$time = time();
while ((time() - $time) < $this->timeOut) {
$newLine = fread($this->stream, 4096);
if(!strlen($newLine)) continue;
$buf .= $newLine;
file_put_contents("/LOG2.txt", $newLine, FILE_APPEND);
if (strripos($previousLine.$newLine, $successPattern) !== FALSE){
$this->responseContainer->setLastResponseText($buf);
return $buf;
}
if (strripos($previousLine.$newLine, $this->commandHash . ($this->commandCounter - 1) . ' NO') !== FALSE
|| strripos($previousLine.$newLine, $this->commandHash . ($this->commandCounter - 1) . ' BAD') !== FALSE){
$this->responseContainer->setLastErrorText($buf);
return false;
}
$previousLine = $newLine;
}
var_dump(" Time out");
$this->responseContainer->setLastErrorText("{$command} {$counter} Time out");
return false;
}
/**
* #param $mailString
* #param string $folder
* #return false|int
*/
public function append($mailString, $folder = "INBOX")
{
if($this->sendCommand("APPEND {$folder} {".strlen($mailString)."}", "go ahead")){
$response = $this->sendCommand($mailString, '', false);
return $this->imapParser->parseAppendResult($response);
}
return false;
}
$message = new \Swift_Message();
$message->setSubject($mail->subject);
$message->setFrom($mail->from);
$message->setTo($mail->to);
$message->addPart($mail->body, 'text/html');
foreach($mail->files as $file){
$attachment = \Swift_Attachment::fromPath($file->getPath());
$attachment->setFilename($file->name.".".$file->type);
$message->attach($attachment);
}
$appendUid = $this->imapService->getCommander()->append($message->toString());
Creating stream:
/**
* #param string $host
* #param integer $port
* #return bool
*/
protected function createStream($host, $port)
{
//#todo proxy authentication
if($this->stream = #stream_socket_client("ssl://{$host}:{$port}", $errno, $errstr, $this->timeOut, STREAM_CLIENT_CONNECT, $this->context)) {
stream_set_timeout($this->stream, $this->timeOut);
return $this->stream;
}
$this->responseContainer->setLastErrorText("Failed connection to imap. Without proxy. " . $errstr);
return false;
}
I add a new media image (using amazon-s3-and-cloudfront and amazon-web-services wordpress plugins) and I need to clear cache of this image.
I use smush PRO to compress image: it compress image only locally so I need to re-put images on S3.
This is my code
global $as3cf;
if ( ! $as3cf instanceof Amazon_S3_And_CloudFront ) return;
$results = new WP_Query( $query );
$attachments=(array)$results->get_posts();
if(!empty($attachments)){
foreach($attachments as $attachment){
$amazons3_info=get_post_meta($attachment->ID,'amazonS3_info');
#$as3cf->delete_attachment($attachment->ID);
$new_files = $as3cf->upload_attachment_to_s3($attachment->ID);
if(is_wp_error($new_files) && isset($amazons3_info) && !empty($amazons3_info)){
update_post_meta($attachment->ID,'amazonS3_info',$amazons3_info);
}
update_post_meta($attachment->ID,'my-smpro-smush',$new_files);
}
}
The variable $new_files contains something like that
a:3:{s:6:"bucket";s:21:"static.example.com";s:3:"key";s:63:"wp-content/uploads/2016/12/334ca0545d748d0fe135eb30212154db.jpg";s:6:"region";s:9:"eu-west-1";}
So now i need to clear image.
Someone can help me?
I also try https://github.com/subchild/CloudFront-PHP-Invalidator/blob/master/CloudFront.php but it doesn't work.
It seems that your question is not about S3, but about CloudFront.
You can use AWS SDK for PHP to invalidate any object or objects with createInvalidation: http://docs.aws.amazon.com/aws-sdk-php/v2/api/class-Aws.CloudFront.CloudFrontClient.html#_createInvalidation
If you don't want to use SDK for some reason, here is a good example of plain POST request to invalidate CloudFront cache:
<?php
/**
* Super-simple AWS CloudFront Invalidation Script
*
* Steps:
* 1. Set your AWS access_key
* 2. Set your AWS secret_key
* 3. Set your CloudFront Distribution ID
* 4. Define the batch of paths to invalidate
* 5. Run it on the command-line with: php cf-invalidate.php
*
* The author disclaims copyright to this source code.
*
* Details on what's happening here are in the CloudFront docs:
* http://docs.amazonwebservices.com/AmazonCloudFront/latest/DeveloperGuide/Invalidation.html
*
*/
$access_key = 'AWS_ACCESS_KEY';
$secret_key = 'AWS_SECRET_KEY';
$distribution = 'DISTRIBUTION_ID';
$epoch = date('U');
$xml = <<<EOD
<InvalidationBatch>
<Path>/index.html</Path>
<Path>/blog/index.html</Path>
<CallerReference>{$distribution}{$epoch}</CallerReference>
</InvalidationBatch>
EOD;
/**
* You probably don't need to change anything below here.
*/
$len = strlen($xml);
$date = gmdate('D, d M Y G:i:s T');
$sig = base64_encode(
hash_hmac('sha1', $date, $secret_key, true)
);
$msg = "POST /2010-11-01/distribution/{$distribution}/invalidation HTTP/1.0\r\n";
$msg .= "Host: cloudfront.amazonaws.com\r\n";
$msg .= "Date: {$date}\r\n";
$msg .= "Content-Type: text/xml; charset=UTF-8\r\n";
$msg .= "Authorization: AWS {$access_key}:{$sig}\r\n";
$msg .= "Content-Length: {$len}\r\n\r\n";
$msg .= $xml;
$fp = fsockopen('ssl://cloudfront.amazonaws.com', 443,
$errno, $errstr, 30
);
if (!$fp) {
die("Connection failed: {$errno} {$errstr}\n");
}
fwrite($fp, $msg);
$resp = '';
while(! feof($fp)) {
$resp .= fgets($fp, 1024);
}
fclose($fp);
echo $resp;
Source: https://gist.github.com/claylo/1009169
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.
Here is my problem,
I want to encrypt JSON files that may be very long in some cases. (Sometimes containing images in Base64 format).
On the following test servers, everything works:
Raspberry Pi 3
Dell Poweredge T110
IIS on Windows 10
Synology DS1815 +
On the other hand, on the following servers, (Which are intended to be used..) the encryption does not work with more than 65535 characters, the server seems to crash.
Synology RS212
Synology DS112 +
Is there a restriction on the CPU?
Can a parameter of php.ini affect?
I tested exactly the same code on multiple servers, and on both Synology mentioned, it does not work ...
Here is my class of encryption / decryption:
class PHP_AES_Cipher {
private static $OPENSSL_CIPHER_NAME = "AES-256-CBC"; //Name of OpenSSL Cipher
private static $CIPHER_KEY_LEN = 32;
static function encrypt($key, $iv, $data) {
if (strlen($key) < PHP_AES_Cipher::$CIPHER_KEY_LEN) {
$key = str_pad("$key", PHP_AES_Cipher::$CIPHER_KEY_LEN, "0");
} else if (strlen($key) > PHP_AES_Cipher::$CIPHER_KEY_LEN) {
$key = substr($str, 0, PHP_AES_Cipher::$CIPHER_KEY_LEN);
}
$encodedEncryptedData = base64_encode(openssl_encrypt($data, PHP_AES_Cipher::$OPENSSL_CIPHER_NAME, $key, OPENSSL_RAW_DATA, $iv));
$encodedIV = base64_encode($iv);
$encryptedPayload = $encodedEncryptedData.":".$encodedIV;
return $encryptedPayload;
}
static function decrypt($key, $data) {
if (strlen($key) < PHP_AES_Cipher::$CIPHER_KEY_LEN) {
$key = str_pad("$key", PHP_AES_Cipher::$CIPHER_KEY_LEN, "0");
} else if (strlen($key) > PHP_AES_Cipher::$CIPHER_KEY_LEN) {
$key = substr($str, 0, PHP_AES_Cipher::$CIPHER_KEY_LEN);
}
$parts = explode(':', $data); //Separate Encrypted data from iv.
$decryptedData = openssl_decrypt(base64_decode($parts[0]), PHP_AES_Cipher::$OPENSSL_CIPHER_NAME, $key, OPENSSL_RAW_DATA, base64_decode($parts[1]));
return $decryptedData;
}
}
I use it like this:
$data = PHP_AES_Cipher::encrypt($key, $iv, $data);
and
$data = PHP_AES_Cipher::decrypt($key, $iv, $data);
Assuming everything works on some servers, I think the code has no problems. I already checked the Apache and PHP logs, nothing to report.
I have been searching for days without understanding the cause of the problem.
In hope that someone can help me :-)
Chunk it,
This is what I do (Uses PHPSecLib2 )
/**
* AES encrypt large files using streams and chunking
*
* #param resource $stream
* #param resource $outputStream
* #param string $key
* #throws SecExecption
*/
function streamSymEncode($stream, &$outputStream, $key, $chunkSize = 10240){
if(!is_resource($stream)) throw new Execption('Resource expected[input]');
rewind($stream); //make sure the stream is rewound
if(!is_resource($outputStream)) throw new Execption('Resource expected[output]');
$Cipher = new AES(AES::MODE_CBC);
$Cipher->setKey($key);
//create the IV
$iv = Random::string($Cipher->getBlockLength() >> 3);
$Cipher->setIV($iv);
if(strlen($iv_base64 = rtrim(base64_encode($iv), '=')) != 22) throw new Execption('IV lenght check fail');
fwrite($outputStream, $iv_base64.'$'); //add the IV for later use when we decrypt
while(!feof($stream)){
$chunk = fread($stream, $chunkSize);
fwrite($outputStream, rtrim(base64_encode($Cipher->encrypt($chunk)),'=').':');
}
$stat = fstat($outputStream);
ftruncate($outputStream, $stat['size'] - 1); //trim off the last character, hanging ':'
}
/**
* AES decrypt large files that were previously encrypted using streams and chunking
*
* #param resource $stream
* #param resource $outputStream
* #param string $key
* #throws SecExecption
*/
function streamSymDecode($stream, &$outputStream, $key){
if(!is_resource($stream)) throw new Execption('Resource expected[input]');
rewind($stream); //make sure the stream is rewound
if(!is_resource($outputStream)) throw new Execption('Resource expected[output]');
$Cipher = new AES(AES::MODE_CBC);
$Cipher->setKey($key);
$iv = base64_decode(fread($stream, 22) . '==');
$Cipher->setIV($iv);
fread($stream, 1); //advance 1 for the $
$readLine = function(&$stream){
$line = '';
while(false !== ($char = fgetc($stream))){
if($char == ':') break;
$line .= $char;
}
return $line;
};
while(!feof($stream)){
$chunk = $readLine($stream);
$decrypted = $Cipher->decrypt(base64_decode($chunk.'=='));
if(!$decrypted) throw new Execption('Failed to decode!');
fwrite($outputStream, $decrypted);
}
}
It takes two File stream resources like what you get from fopen and a key. Then it uses the same ecryption but chunks the file into $chunkSize separates them with : and when it decodes, it splits it back into chunks and re-assembles everything.
It winds up like this (for example)
IV$firstChunk:secondChunk:thirdChunk
This way you don't run out of memory trying to encrypt large files.
Please Note this was part of a lager class I use so I had to trim some things and make a few changes, that I haven't tested.
https://github.com/phpseclib/phpseclib
Cheers.