After updating to Laravel 9.14 with PHP 8.1.4 then streaming has broke that was based on this answer https://stackoverflow.com/a/52598361/6825499.
I can see that it is because of a
Call to undefined method League\Flysystem\AwsS3V3\AwsS3V3Adapter::getClient()
So it seems to have been removed in the newest version from league/flysystem-aws-s3-v3 (3.0.13)
I did find a reference to this SO post which tries to explain there is a workaround now: Get S3Client from storage facade in Laravel 9
This is though too complex for me to understand.
Does anyone know what can be done?
You need to update the PHP version on your server to the latest one. It sounds like the server is still on the PHP 7.x version and that's the issue.
After researching quite a bit I realized that the issue came down to updates described in Laravel document page about upgrading.
I ended up altering the code a bit where I use environment variables to fill out what was before provieded by the adapter in the code.
In the latest version of the flysystem,there is no longer access to the client via the adapter. Due to mentioned reason, the service has been broken.
For a fully working class then you can use below code for laravel version 9
<?php
namespace App\Http;
use Exception;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Http\Response;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use League\Flysystem\Filesystem;
use Storage;
use Symfony\Component\HttpFoundation\StreamedResponse;
class S3FileStream
{
/**
* Name of adapter
*
* #var string
*/
private $adapterName;
/**
* Storage disk
*
* #var FilesystemAdapter
*/
private $disk;
/**
* #var int file end byte
*/
private $end;
/**
* #var string
*/
private $filePath;
/**
* Human-known filename
*
* #var string|null
*/
private $humanName;
/**
* #var bool storing if request is a range (or a full file)
*/
private $isRange = false;
/**
* #var int|null length of bytes requested
*/
private $length = null;
/**
* #var array
*/
private $returnHeaders = [];
/**
* #var int file size
*/
private $size;
/**
* #var string bucket name
*/
private $bucket;
/**
* #var int start byte
*/
private $start;
/**
* S3FileStream constructor.
* #param string $filePath
* #param string $adapter
* #param string $humanName
*/
public function __construct(string $filePath, string $adapter = 's3', ?string $humanName = null)
{
$options = [
'region' => env("AWS_DEFAULT_REGION"),
'version' => 'latest'
];
$this->filePath = $filePath;
$this->adapterName = $adapter;
$this->disk = Storage::disk($this->adapterName);
$this->client = new \Aws\S3\S3Client($options);
$this->humanName = $humanName;
$this->bucket = env("AWS_BUCKET");
//Set to zero until setHeadersAndStream is called
$this->start = 0;
$this->size = 0;
$this->end = 0;
}
/**
* Output file to client.
*/
public function output()
{
return $this->setHeadersAndStream();
}
/**
* Output headers to client.
* #return Response|StreamedResponse
*/
protected function setHeadersAndStream()
{
if (!$this->disk->exists($this->filePath)) {
report(new Exception('S3 File Not Found in S3FileStream - ' . $this->adapterName . ' - ' . $this->disk->path($this->filePath)));
return response('File Not Found', 404);
}
$this->start = 0;
$this->size = $this->disk->size($this->filePath);
$this->end = $this->size - 1;
$this->length = $this->size;
$this->isRange = false;
//Set headers
$this->returnHeaders = [
'Last-Modified' => $this->disk->lastModified($this->filePath),
'Accept-Ranges' => 'bytes',
'Content-Type' => $this->disk->mimeType($this->filePath),
'Content-Disposition' => 'inline; filename=' . ($this->humanName ?? basename($this->filePath) . '.' . Arr::last(explode('.', $this->filePath))),
'Content-Length' => $this->length,
];
//Handle ranges here
if (!is_null(request()->server('HTTP_RANGE'))) {
$cStart = $this->start;
$cEnd = $this->end;
$range = Str::after(request()->server('HTTP_RANGE'), '=');
if (strpos($range, ',') !== false) {
return response('416 Requested Range Not Satisfiable', 416, [
'Content-Range' => 'bytes */' . $this->size,
]);
}
if (substr($range, 0, 1) == '-') {
$cStart = $this->size - intval(substr($range, 1)) - 1;
} else {
$range = explode('-', $range);
$cStart = intval($range[0]);
$cEnd = (isset($range[1]) && is_numeric($range[1])) ? intval($range[1]) : $cEnd;
}
$cEnd = min($cEnd, $this->size - 1);
if ($cStart > $cEnd || $cStart > $this->size - 1) {
return response('416 Requested Range Not Satisfiable', 416, [
'Content-Range' => 'bytes */' . $this->size,
]);
}
$this->start = intval($cStart);
$this->end = intval($cEnd);
$this->length = min($this->end - $this->start + 1, $this->size);
$this->returnHeaders['Content-Length'] = $this->length;
$this->returnHeaders['Content-Range'] = 'bytes ' . $this->start . '-' . $this->end . '/' . $this->size;
$this->isRange = true;
}
return $this->stream();
}
/**
* Stream file to client.
* #throws Exception
* #return StreamedResponse
*/
protected function stream(): StreamedResponse
{
$this->client->registerStreamWrapper();
// Create a stream context to allow seeking
$context = stream_context_create([
's3' => [
'seekable' => true,
],
]);
// Open a stream in read-only mode
if (!($stream = fopen("s3://{$this->bucket}/{$this->filePath}", 'rb', false, $context))) {
throw new Exception('Could not open stream for reading export [' . $this->filePath . ']');
}
if (isset($this->start) && $this->start > 0) {
fseek($stream, $this->start, SEEK_SET);
}
$remainingBytes = $this->length ?? $this->size;
$chunkSize = 100;
$video = response()->stream(
function () use ($stream, $remainingBytes, $chunkSize) {
while (!feof($stream) && $remainingBytes > 0) {
$toGrab = min($chunkSize, $remainingBytes);
echo fread($stream, $toGrab);
$remainingBytes -= $toGrab;
flush();
}
fclose($stream);
},
($this->isRange ? 206 : 200),
$this->returnHeaders
);
return $video;
}
}
I'm storing videos to Amazon S3 via my Laravel app. That works great. But I can't "stream" them.
This is the URL for example: https://website.com/video/342.qt?api_token=a5a18c9f-f5f6-5d66-85e3-aaaaaaa, what should return this movie from S3 called '212.DdsqoK1PlL.qt'
It returns this output when calling the URL:
That's the video, but I was expecting it to run directly in the browser, like this video does: https://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4
The route calls this function, retrieving the non-public file from the S3-disk:
public function document(Document $document)
{
return Storage::disk('s3')->get($document->path);
}
The only difference between the example URL that works and mine is that the example is MP4 and mine .QT, but I tried MP4 also and go the same output in the browser; so no autoplaying video.
I guess the movie that plays directly is streaming the video?..
My website is running on Ubuntu and installed also sudo apt-get install vlc.
I personally am opposed to the idea of redirecting to an S3 URL. I mask all my URLs through a Laravel php wrapper server-side. This is the code I use to do so if anyone else encounters similar issues. This code is written for streaming a video from S3 with Laravel 5.6 (and includes HTTP_RANGE support so it works on iOS too).
I use the class below, placed at App/Http/Responses. To use this class, create a method that does this (this is like a getFile method):
$filestream = new \App\Http\Responses\S3FileStream('file_path_and_name_within_bucket', 'disk_bucket_name', 'output_file_name_when_downloaded');
return $filestream->output();
With any luck, you should be streaming in no time (without revealing an S3 URL)!
S3FileStream.php:
<?php
namespace Http\Responses;
use Exception;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Http\Response;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Storage;
use Symfony\Component\HttpFoundation\StreamedResponse;
class S3FileStream
{
/**
* #var \League\Flysystem\AwsS3v3\AwsS3Adapter
*/
private $adapter;
/**
* Name of adapter
*
* #var string
*/
private $adapterName;
/**
* Storage disk
*
* #var FilesystemAdapter
*/
private $disk;
/**
* #var int file end byte
*/
private $end;
/**
* #var string
*/
private $filePath;
/**
* Human-known filename
*
* #var string|null
*/
private $humanName;
/**
* #var bool storing if request is a range (or a full file)
*/
private $isRange = false;
/**
* #var int|null length of bytes requested
*/
private $length = null;
/**
* #var array
*/
private $returnHeaders = [];
/**
* #var int file size
*/
private $size;
/**
* #var int start byte
*/
private $start;
/**
* S3FileStream constructor.
* #param string $filePath
* #param string $adapter
* #param string $humanName
*/
public function __construct(string $filePath, string $adapter = 's3', ?string $humanName = null)
{
$this->filePath = $filePath;
$this->adapterName = $adapter;
$this->disk = Storage::disk($this->adapterName);
$this->adapter = $this->disk->getAdapter();
$this->humanName = $humanName;
//Set to zero until setHeadersAndStream is called
$this->start = 0;
$this->size = 0;
$this->end = 0;
}
/**
* Output file to client.
*/
public function output()
{
return $this->setHeadersAndStream();
}
/**
* Output headers to client.
* #return Response|StreamedResponse
*/
protected function setHeadersAndStream()
{
if (!$this->disk->exists($this->filePath)) {
report(new Exception('S3 File Not Found in S3FileStream - ' . $this->adapterName . ' - ' . $this->disk->path($this->filePath)));
return response('File Not Found', 404);
}
$this->start = 0;
$this->size = $this->disk->size($this->filePath);
$this->end = $this->size - 1;
$this->length = $this->size;
$this->isRange = false;
//Set headers
$this->returnHeaders = [
'Last-Modified' => $this->disk->lastModified($this->filePath),
'Accept-Ranges' => 'bytes',
'Content-Type' => $this->disk->mimeType($this->filePath),
'Content-Disposition' => 'inline; filename=' . ($this->humanName ?? basename($this->filePath) . '.' . Arr::last(explode('.', $this->filePath))),
'Content-Length' => $this->length,
];
//Handle ranges here
if (!is_null(request()->server('HTTP_RANGE'))) {
$cStart = $this->start;
$cEnd = $this->end;
$range = Str::after(request()->server('HTTP_RANGE'), '=');
if (strpos($range, ',') !== false) {
return response('416 Requested Range Not Satisfiable', 416, [
'Content-Range' => 'bytes */' . $this->size,
]);
}
if (substr($range, 0, 1) == '-') {
$cStart = $this->size - intval(substr($range, 1)) - 1;
} else {
$range = explode('-', $range);
$cStart = intval($range[0]);
$cEnd = (isset($range[1]) && is_numeric($range[1])) ? intval($range[1]) : $cEnd;
}
$cEnd = min($cEnd, $this->size - 1);
if ($cStart > $cEnd || $cStart > $this->size - 1) {
return response('416 Requested Range Not Satisfiable', 416, [
'Content-Range' => 'bytes */' . $this->size,
]);
}
$this->start = intval($cStart);
$this->end = intval($cEnd);
$this->length = min($this->end - $this->start + 1, $this->size);
$this->returnHeaders['Content-Length'] = $this->length;
$this->returnHeaders['Content-Range'] = 'bytes ' . $this->start . '-' . $this->end . '/' . $this->size;
$this->isRange = true;
}
return $this->stream();
}
/**
* Stream file to client.
* #throws Exception
* #return StreamedResponse
*/
protected function stream(): StreamedResponse
{
$this->adapter->getClient()->registerStreamWrapper();
// Create a stream context to allow seeking
$context = stream_context_create([
's3' => [
'seekable' => true,
],
]);
// Open a stream in read-only mode
if (!($stream = fopen("s3://{$this->adapter->getBucket()}/{$this->filePath}", 'rb', false, $context))) {
throw new Exception('Could not open stream for reading export [' . $this->filePath . ']');
}
if (isset($this->start) && $this->start > 0) {
fseek($stream, $this->start, SEEK_SET);
}
$remainingBytes = $this->length ?? $this->size;
$chunkSize = 100;
$video = response()->stream(
function () use ($stream, $remainingBytes, $chunkSize) {
while (!feof($stream) && $remainingBytes > 0) {
$toGrab = min($chunkSize, $remainingBytes);
echo fread($stream, $toGrab);
$remainingBytes -= $toGrab;
flush();
}
fclose($stream);
},
($this->isRange ? 206 : 200),
$this->returnHeaders
);
return $video;
}
}
Like I said on the comments section. I think you should use S3 url (temporary, or public).
You have some options here:
Use laravel temporary url;
Set your file as public + get url;
For more information:
https://laravel.com/docs/5.5/filesystem#storing-files
To set your file visibility as public:
Storage::setVisibility('file.jpg', 'public')
Temporary URL:
$url = Storage::temporaryUrl(
'file1.jpg', Carbon::now()->addMinutes(5)
);
If your file is public, you can use:
Storage::url('file1.jpg');
Is there a way to recover emails from a gmail account through laravel ?
I want to create an inbox mail, but directly from an acount of gmail or outlook.
Thanks for help.
David,
Follow below some code that I use to read emails. I believe that can helps you:
/**
* Get last UID on Emails table
*
* return integer
*/
private function getLastUID()
{
// I have a model Email and I try to get the higher uid on database
return Email::max('uid');
}
/**
* Open Imap instance
*
* #return resource
*/
private function startEmail()
{
return imap_open(env('IMAP'), env('IMAP_EMAIL'), env('IMAP_PASSWORD'), OP_READONLY);
}
/**
* Get Emails from Imap instance
*
* #return array
*/
private function getTodayEmails()
{
$mailbox = $this->startEmail();
$today = Carbon::now()->format('j-M-Y');
//I only search for todays emails, since I have a cron job that runs this task every hour (For my purpose I don't need to check it every minute)
$inbox = imap_search($mailbox,'SINCE '.$today);
/* If there is no email */
if ($inbox === false) return false;
//Sort to insert the new email first
rsort($inbox);
$emails = [];
foreach($inbox as $box) {
/* get information specific to this email */
$overview = imap_fetch_overview($mailbox, $box, 0);
$header = imap_headerinfo($mailbox , $box);
$uid = imap_uid($mailbox , $box);
// Here I check if the email $uid is already on my database, if no, I save it. If yes I break the conditional.
// I highly believe that you have to work on this conditional and in your Model. The rest is working well (at least for me! :) )
if ($uid > $this->getLastUID()) {
$emails[$box]['uid'] = $uid;
$emails[$box]['date'] = (isset($header->udate)) ? date('Y-m-d H:i:s', $header->udate) : null;
$emails[$box]['subject'] = (isset($overview[0]->subject)) ? $overview[0]->subject : null;
$emails[$box]['from'] = (isset($header->from[0])) ? $this->extractEmail($header->from[0]) : null;
$emails[$box]['from_name'] = (isset($header->from[0]->personal)) ? $header->from[0]->personal : null;
$emails[$box]['to'] = (isset($header->to[0])) ? $this->extractEmail($header->to[0]) : null;
$emails[$box]['to_name'] = (isset($header->to[0]->personal)) ? $header->to[0]->personal : null;
$emails[$box]['reply_to'] = (isset($header->reply_to[0])) ? $this->extractEmail($header->reply_to[0]) : null;
$emails[$box]['reply_name'] = (isset($header->reply_to[0]->personal)) ? $header->reply_to[0]->personal : null;
/* output the email body */
$emails[$box]['message'] = $this->getBody($uid, $mailbox);
} else {
break;
}
imap_close($mailbox);
return $emails;
}
/**
* Extract email from Imap Instance
*
* #param object $email
*
* #return bool|string
*/
private function extractEmail($email)
{
if (isset($email->mailbox) && isset($email->host))
return $email->mailbox.'#'.$email->host;
return false;
}
/**
* Get body message
*
* #param integer $uid
* #param Imap Instance $imap
*
* #return bool
*/
private function getBody($uid, $imap)
{
$body = $this->getPart($imap, $uid, "TEXT/HTML");
// if HTML body is empty, try getting text body
if ($body == "") {
$body = $this->getPart($imap, $uid, "TEXT/PLAIN");
}
return $body;
}
/**
* Treat body message of email
*
* #param Imap Instance $imap
* #param integer $uid
* #param string $mimetype
* #param bool $structure
* #param bool $partNumber
*
* #return bool|string
*/
private function getPart($imap, $uid, $mimetype, $structure = false, $partNumber = false)
{
if (!$structure) {
$structure = imap_fetchstructure($imap, $uid, FT_UID);
}
if ($structure) {
if ($mimetype == $this->getMimeType($structure)) {
if (!$partNumber) {
$partNumber = 1;
}
$text = imap_fetchbody($imap, $uid, $partNumber, FT_UID);
switch ($structure->encoding) {
case 3: return imap_base64($text);
case 4: return imap_qprint($text);
default: return $text;
}
}
// multipart
if ($structure->type == 1) {
foreach ($structure->parts as $index => $subStruct) {
$prefix = "";
if ($partNumber) {
$prefix = $partNumber . ".";
}
$data = $this->getPart($imap, $uid, $mimetype, $subStruct, $prefix . ($index + 1));
if ($data) {
return $data;
}
}
}
}
return false;
}
/**
* Get Mimetype of part
*
* #param $structure
*
* #return string
*/
private function getMimeType($structure)
{
$primaryMimetype = array("TEXT", "MULTIPART", "MESSAGE", "APPLICATION", "AUDIO", "IMAGE", "VIDEO", "OTHER");
if ($structure->subtype) {
return $primaryMimetype[(int)$structure->type] . "/" . $structure->subtype;
}
return "TEXT/PLAIN";
}
On the .env you must insert:
IMAP={imap.gmail.com:993/ssl/novalidate-cert}INBOX
(or you can use {imap.gmail.com:993/imap/ssl}INBOX)
IMAP_EMAIL=<Your GMAIL>
IMAP_PASSWORD=<PASSWORD>
Although you can use some libraries, like https://github.com/barbushin/php-imap. I really believe that it is easy to go straight with raw php (http://php.net/manual/en/book.imap.php).
You can get more information on: https://davidwalsh.name/gmail-php-imap (Where most of the functions are from, btw)
You can use GMAIL API
https://developers.google.com/gmail/api/quickstart/php
You can search or filter files using the messages.list and threads.list methods https://developers.google.com/gmail/api/guides/filtering
GET https://www.googleapis.com/gmail/v1/users/me/messages?q="in:sent after:2014/01/01 before:2014/01/30"
I want to set minimum width and height for uploading images in codeigniter.
The code is shown below.
$config['max_width'] = '480';
$config['max_height'] = '270';
$this->upload->do_upload()
I have set maximum cut off for this but how to set minimum ??
Copy your Upload.php from system/libraries and paste on application/libraries. Then,
introduce 2 new variables.
public $max_width = 0;
public $max_height = 0;
public $min_width = 0; // new
public $min_height = 0; // new
Add these to the initialize function so that you can pass values through a $config variable.
Locate is_allowed_dimensions function and modify it.
if ($this->min_width > 0 AND $D['0'] < $this->min_width)
{
return FALSE;
}
if ($this->min_height > 0 AND $D['1'] < $this->min_height)
{
return FALSE;
}
Check the upload language file and alter the upload_invalid_dimensions key accordingly to fit your case.
Have not tested this, but should work :)
Go to system/validation. And then open file FileRules.php
Paste this code :
public function min_dims(?string $blank, string $params): bool
{
// Grab the file name off the top of the $params
// after we split it.
$params = explode(',', $params);
$name = array_shift($params);
if (! ($files = $this->request->getFileMultiple($name))) {
$files = [$this->request->getFile($name)];
}
foreach ($files as $file) {
if ($file === null) {
return false;
}
if ($file->getError() === UPLOAD_ERR_NO_FILE) {
return true;
}
// Get Parameter sizes
$allowedWidth = $params[0] ?? 0;
$allowedHeight = $params[1] ?? 0;
// Get uploaded image size
$info = getimagesize($file->getTempName());
$fileWidth = $info[0];
$fileHeight = $info[1];
if ($fileWidth < $allowedWidth || $fileHeight < $allowedHeight) {
return false;
}
}
return true;
}
And, in your controller use min_dims[field_name,1100,1100]. The first
parameter is the field name. The second is
the width, and the third is the height
Good Luck!
Here is the working fix, additionally you need to specify min_height and min_width while you init
<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
class CI_Upload {
public $max_size = 0;
public $max_width = 0;
public $max_height = 0;
public $min_width = 0;
public $min_height = 0;
public $max_filename = 0;
public $allowed_types = "";
public $file_temp = "";
public $file_name = "";
public $orig_name = "";
public $file_type = "";
public $file_size = "";
public $file_ext = "";
public $upload_path = "";
public $overwrite = FALSE;
public $encrypt_name = FALSE;
public $is_image = FALSE;
public $image_width = '';
public $image_height = '';
public $image_type = '';
public $image_size_str = '';
public $error_msg = array();
public $mimes = array();
public $remove_spaces = TRUE;
public $xss_clean = FALSE;
public $temp_prefix = "temp_file_";
public $client_name = '';
protected $_file_name_override = '';
/**
* Constructor
*
* #access public
*/
public function __construct($props = array())
{
if (count($props) > 0)
{
$this->initialize($props);
}
log_message('debug', "Upload Class Initialized");
}
// --------------------------------------------------------------------
/**
* Initialize preferences
*
* #param array
* #return void
*/
public function initialize($config = array())
{
$defaults = array(
'max_size' => 0,
'max_width' => 0,
'max_height' => 0,
'min_width' => 0,
'min_height' => 0,
'max_filename' => 0,
'allowed_types' => "",
'file_temp' => "",
'file_name' => "",
'orig_name' => "",
'file_type' => "",
'file_size' => "",
'file_ext' => "",
'upload_path' => "",
'overwrite' => FALSE,
'encrypt_name' => FALSE,
'is_image' => FALSE,
'image_width' => '',
'image_height' => '',
'image_type' => '',
'image_size_str' => '',
'error_msg' => array(),
'mimes' => array(),
'remove_spaces' => TRUE,
'xss_clean' => FALSE,
'temp_prefix' => "temp_file_",
'client_name' => ''
);
foreach ($defaults as $key => $val)
{
if (isset($config[$key]))
{
$method = 'set_'.$key;
if (method_exists($this, $method))
{
$this->$method($config[$key]);
}
else
{
$this->$key = $config[$key];
}
}
else
{
$this->$key = $val;
}
}
// if a file_name was provided in the config, use it instead of the user input
// supplied file name for all uploads until initialized again
$this->_file_name_override = $this->file_name;
}
// --------------------------------------------------------------------
/**
* Perform the file upload
*
* #return bool
*/
public function do_upload($field = 'userfile')
{
// Is $_FILES[$field] set? If not, no reason to continue.
if ( ! isset($_FILES[$field]))
{
$this->set_error('upload_no_file_selected');
return FALSE;
}
// Is the upload path valid?
if ( ! $this->validate_upload_path())
{
// errors will already be set by validate_upload_path() so just return FALSE
return FALSE;
}
// Was the file able to be uploaded? If not, determine the reason why.
if ( ! is_uploaded_file($_FILES[$field]['tmp_name']))
{
$error = ( ! isset($_FILES[$field]['error'])) ? 4 : $_FILES[$field]['error'];
switch($error)
{
case 1: // UPLOAD_ERR_INI_SIZE
$this->set_error('upload_file_exceeds_limit');
break;
case 2: // UPLOAD_ERR_FORM_SIZE
$this->set_error('upload_file_exceeds_form_limit');
break;
case 3: // UPLOAD_ERR_PARTIAL
$this->set_error('upload_file_partial');
break;
case 4: // UPLOAD_ERR_NO_FILE
$this->set_error('upload_no_file_selected');
break;
case 6: // UPLOAD_ERR_NO_TMP_DIR
$this->set_error('upload_no_temp_directory');
break;
case 7: // UPLOAD_ERR_CANT_WRITE
$this->set_error('upload_unable_to_write_file');
break;
case 8: // UPLOAD_ERR_EXTENSION
$this->set_error('upload_stopped_by_extension');
break;
default : $this->set_error('upload_no_file_selected');
break;
}
return FALSE;
}
// Set the uploaded data as class variables
$this->file_temp = $_FILES[$field]['tmp_name'];
$this->file_size = $_FILES[$field]['size'];
$this->_file_mime_type($_FILES[$field]);
$this->file_type = preg_replace("/^(.+?);.*$/", "\\1", $this->file_type);
$this->file_type = strtolower(trim(stripslashes($this->file_type), '"'));
$this->file_name = $this->_prep_filename($_FILES[$field]['name']);
$this->file_ext = $this->get_extension($this->file_name);
$this->client_name = $this->file_name;
// Is the file type allowed to be uploaded?
if ( ! $this->is_allowed_filetype())
{
$this->set_error('upload_invalid_filetype');
return FALSE;
}
// if we're overriding, let's now make sure the new name and type is allowed
if ($this->_file_name_override != '')
{
$this->file_name = $this->_prep_filename($this->_file_name_override);
// If no extension was provided in the file_name config item, use the uploaded one
if (strpos($this->_file_name_override, '.') === FALSE)
{
$this->file_name .= $this->file_ext;
}
// An extension was provided, lets have it!
else
{
$this->file_ext = $this->get_extension($this->_file_name_override);
}
if ( ! $this->is_allowed_filetype(TRUE))
{
$this->set_error('upload_invalid_filetype');
return FALSE;
}
}
// Convert the file size to kilobytes
if ($this->file_size > 0)
{
$this->file_size = round($this->file_size/1024, 2);
}
// Is the file size within the allowed maximum?
if ( ! $this->is_allowed_filesize())
{
$this->set_error('upload_invalid_filesize');
return FALSE;
}
// Are the image dimensions within the allowed size?
// Note: This can fail if the server has an open_basdir restriction.
if ( ! $this->is_allowed_dimensions())
{
$this->set_error('upload_invalid_dimensions');
return FALSE;
}
// Sanitize the file name for security
$this->file_name = $this->clean_file_name($this->file_name);
// Truncate the file name if it's too long
if ($this->max_filename > 0)
{
$this->file_name = $this->limit_filename_length($this->file_name, $this->max_filename);
}
// Remove white spaces in the name
if ($this->remove_spaces == TRUE)
{
$this->file_name = preg_replace("/\s+/", "_", $this->file_name);
}
/*
* Validate the file name
* This function appends an number onto the end of
* the file if one with the same name already exists.
* If it returns false there was a problem.
*/
$this->orig_name = $this->file_name;
if ($this->overwrite == FALSE)
{
$this->file_name = $this->set_filename($this->upload_path, $this->file_name);
if ($this->file_name === FALSE)
{
return FALSE;
}
}
/*
* Run the file through the XSS hacking filter
* This helps prevent malicious code from being
* embedded within a file. Scripts can easily
* be disguised as images or other file types.
*/
if ($this->xss_clean)
{
if ($this->do_xss_clean() === FALSE)
{
$this->set_error('upload_unable_to_write_file');
return FALSE;
}
}
/*
* Move the file to the final destination
* To deal with different server configurations
* we'll attempt to use copy() first. If that fails
* we'll use move_uploaded_file(). One of the two should
* reliably work in most environments
*/
if ( ! #copy($this->file_temp, $this->upload_path.$this->file_name))
{
if ( ! #move_uploaded_file($this->file_temp, $this->upload_path.$this->file_name))
{
$this->set_error('upload_destination_error');
return FALSE;
}
}
/*
* Set the finalized image dimensions
* This sets the image width/height (assuming the
* file was an image). We use this information
* in the "data" function.
*/
$this->set_image_properties($this->upload_path.$this->file_name);
return TRUE;
}
// --------------------------------------------------------------------
/**
* Finalized Data Array
*
* Returns an associative array containing all of the information
* related to the upload, allowing the developer easy access in one array.
*
* #return array
*/
public function data()
{
return array (
'file_name' => $this->file_name,
'file_type' => $this->file_type,
'file_path' => $this->upload_path,
'full_path' => $this->upload_path.$this->file_name,
'raw_name' => str_replace($this->file_ext, '', $this->file_name),
'orig_name' => $this->orig_name,
'client_name' => $this->client_name,
'file_ext' => $this->file_ext,
'file_size' => $this->file_size,
'is_image' => $this->is_image(),
'image_width' => $this->image_width,
'image_height' => $this->image_height,
'image_type' => $this->image_type,
'image_size_str' => $this->image_size_str,
);
}
// --------------------------------------------------------------------
/**
* Set Upload Path
*
* #param string
* #return void
*/
public function set_upload_path($path)
{
// Make sure it has a trailing slash
$this->upload_path = rtrim($path, '/').'/';
}
// --------------------------------------------------------------------
/**
* Set the file name
*
* This function takes a filename/path as input and looks for the
* existence of a file with the same name. If found, it will append a
* number to the end of the filename to avoid overwriting a pre-existing file.
*
* #param string
* #param string
* #return string
*/
public function set_filename($path, $filename)
{
if ($this->encrypt_name == TRUE)
{
mt_srand();
$filename = md5(uniqid(mt_rand())).$this->file_ext;
}
if ( ! file_exists($path.$filename))
{
return $filename;
}
$filename = str_replace($this->file_ext, '', $filename);
$new_filename = '';
for ($i = 1; $i < 100; $i++)
{
if ( ! file_exists($path.$filename.$i.$this->file_ext))
{
$new_filename = $filename.$i.$this->file_ext;
break;
}
}
if ($new_filename == '')
{
$this->set_error('upload_bad_filename');
return FALSE;
}
else
{
return $new_filename;
}
}
// --------------------------------------------------------------------
/**
* Set Maximum File Size
*
* #param integer
* #return void
*/
public function set_max_filesize($n)
{
$this->max_size = ((int) $n < 0) ? 0: (int) $n;
}
// --------------------------------------------------------------------
/**
* Set Maximum File Name Length
*
* #param integer
* #return void
*/
public function set_max_filename($n)
{
$this->max_filename = ((int) $n < 0) ? 0: (int) $n;
}
// --------------------------------------------------------------------
/**
* Set Maximum Image Width
*
* #param integer
* #return void
*/
public function set_max_width($n)
{
$this->max_width = ((int) $n < 0) ? 0: (int) $n;
}
public function set_min_width($n)
{
$this->min_width = ((int) $n < 0) ? 0: (int) $n;
}
// --------------------------------------------------------------------
/**
* Set Maximum Image Height
*
* #param integer
* #return void
*/
public function set_max_height($n)
{
$this->max_height = ((int) $n < 0) ? 0: (int) $n;
}
public function set_min_height($n)
{
$this->min_height = ((int) $n < 0) ? 0: (int) $n;
}
// --------------------------------------------------------------------
/**
* Set Allowed File Types
*
* #param string
* #return void
*/
public function set_allowed_types($types)
{
if ( ! is_array($types) && $types == '*')
{
$this->allowed_types = '*';
return;
}
$this->allowed_types = explode('|', $types);
}
// --------------------------------------------------------------------
/**
* Set Image Properties
*
* Uses GD to determine the width/height/type of image
*
* #param string
* #return void
*/
public function set_image_properties($path = '')
{
if ( ! $this->is_image())
{
return;
}
if (function_exists('getimagesize'))
{
if (FALSE !== ($D = #getimagesize($path)))
{
$types = array(1 => 'gif', 2 => 'jpeg', 3 => 'png');
$this->image_width = $D['0'];
$this->image_height = $D['1'];
$this->image_type = ( ! isset($types[$D['2']])) ? 'unknown' : $types[$D['2']];
$this->image_size_str = $D['3']; // string containing height and width
}
}
}
// --------------------------------------------------------------------
/**
* Set XSS Clean
*
* Enables the XSS flag so that the file that was uploaded
* will be run through the XSS filter.
*
* #param bool
* #return void
*/
public function set_xss_clean($flag = FALSE)
{
$this->xss_clean = ($flag == TRUE) ? TRUE : FALSE;
}
// --------------------------------------------------------------------
/**
* Validate the image
*
* #return bool
*/
public function is_image()
{
// IE will sometimes return odd mime-types during upload, so here we just standardize all
// jpegs or pngs to the same file type.
$png_mimes = array('image/x-png');
$jpeg_mimes = array('image/jpg', 'image/jpe', 'image/jpeg', 'image/pjpeg');
if (in_array($this->file_type, $png_mimes))
{
$this->file_type = 'image/png';
}
if (in_array($this->file_type, $jpeg_mimes))
{
$this->file_type = 'image/jpeg';
}
$img_mimes = array(
'image/gif',
'image/jpeg',
'image/png',
);
return (in_array($this->file_type, $img_mimes, TRUE)) ? TRUE : FALSE;
}
// --------------------------------------------------------------------
/**
* Verify that the filetype is allowed
*
* #return bool
*/
public function is_allowed_filetype($ignore_mime = FALSE)
{
if ($this->allowed_types == '*')
{
return TRUE;
}
if (count($this->allowed_types) == 0 OR ! is_array($this->allowed_types))
{
$this->set_error('upload_no_file_types');
return FALSE;
}
$ext = strtolower(ltrim($this->file_ext, '.'));
if ( ! in_array($ext, $this->allowed_types))
{
return FALSE;
}
// Images get some additional checks
$image_types = array('gif', 'jpg', 'jpeg', 'png', 'jpe');
if (in_array($ext, $image_types))
{
if (getimagesize($this->file_temp) === FALSE)
{
return FALSE;
}
}
if ($ignore_mime === TRUE)
{
return TRUE;
}
$mime = $this->mimes_types($ext);
if (is_array($mime))
{
if (in_array($this->file_type, $mime, TRUE))
{
return TRUE;
}
}
elseif ($mime == $this->file_type)
{
return TRUE;
}
return FALSE;
}
// --------------------------------------------------------------------
/**
* Verify that the file is within the allowed size
*
* #return bool
*/
public function is_allowed_filesize()
{
if ($this->max_size != 0 AND $this->file_size > $this->max_size)
{
return FALSE;
}
else
{
return TRUE;
}
}
// --------------------------------------------------------------------
/**
* Verify that the image is within the allowed width/height
*
* #return bool
*/
public function is_allowed_dimensions()
{
if ( ! $this->is_image())
{
return TRUE;
}
if (function_exists('getimagesize'))
{
$D = #getimagesize($this->file_temp);
if ($this->max_width > 0 AND $D['0'] > $this->max_width)
{
return FALSE;
}
if ($this->max_height > 0 AND $D['1'] > $this->max_height)
{
return FALSE;
}
if ($D['0'] < $this->min_width)
{
return FALSE;
}
if ($D['1'] < $this->min_height)
{
return FALSE;
}
return TRUE;
}
return TRUE;
}
// --------------------------------------------------------------------
/**
* Validate Upload Path
*
* Verifies that it is a valid upload path with proper permissions.
*
*
* #return bool
*/
public function validate_upload_path()
{
if ($this->upload_path == '')
{
$this->set_error('upload_no_filepath');
return FALSE;
}
if (function_exists('realpath') AND #realpath($this->upload_path) !== FALSE)
{
$this->upload_path = str_replace("\\", "/", realpath($this->upload_path));
}
if ( ! #is_dir($this->upload_path))
{
$this->set_error('upload_no_filepath');
return FALSE;
}
if ( ! is_really_writable($this->upload_path))
{
$this->set_error('upload_not_writable');
return FALSE;
}
$this->upload_path = preg_replace("/(.+?)\/*$/", "\\1/", $this->upload_path);
return TRUE;
}
// --------------------------------------------------------------------
/**
* Extract the file extension
*
* #param string
* #return string
*/
public function get_extension($filename)
{
$x = explode('.', $filename);
return '.'.end($x);
}
// --------------------------------------------------------------------
/**
* Clean the file name for security
*
* #param string
* #return string
*/
public function clean_file_name($filename)
{
$bad = array(
"<!--",
"-->",
"'",
"<",
">",
'"',
'&',
'$',
'=',
';',
'?',
'/',
"%20",
"%22",
"%3c", // <
"%253c", // <
"%3e", // >
"%0e", // >
"%28", // (
"%29", // )
"%2528", // (
"%26", // &
"%24", // $
"%3f", // ?
"%3b", // ;
"%3d" // =
);
$filename = str_replace($bad, '', $filename);
return stripslashes($filename);
}
// --------------------------------------------------------------------
/**
* Limit the File Name Length
*
* #param string
* #return string
*/
public function limit_filename_length($filename, $length)
{
if (strlen($filename) < $length)
{
return $filename;
}
$ext = '';
if (strpos($filename, '.') !== FALSE)
{
$parts = explode('.', $filename);
$ext = '.'.array_pop($parts);
$filename = implode('.', $parts);
}
return substr($filename, 0, ($length - strlen($ext))).$ext;
}
// --------------------------------------------------------------------
/**
* Runs the file through the XSS clean function
*
* This prevents people from embedding malicious code in their files.
* I'm not sure that it won't negatively affect certain files in unexpected ways,
* but so far I haven't found that it causes trouble.
*
* #return void
*/
public function do_xss_clean()
{
$file = $this->file_temp;
if (filesize($file) == 0)
{
return FALSE;
}
if (function_exists('memory_get_usage') && memory_get_usage() && ini_get('memory_limit') != '')
{
$current = ini_get('memory_limit') * 1024 * 1024;
// There was a bug/behavioural change in PHP 5.2, where numbers over one million get output
// into scientific notation. number_format() ensures this number is an integer
// http://bugs.php.net/bug.php?id=43053
$new_memory = number_format(ceil(filesize($file) + $current), 0, '.', '');
ini_set('memory_limit', $new_memory); // When an integer is used, the value is measured in bytes. - PHP.net
}
// If the file being uploaded is an image, then we should have no problem with XSS attacks (in theory), but
// IE can be fooled into mime-type detecting a malformed image as an html file, thus executing an XSS attack on anyone
// using IE who looks at the image. It does this by inspecting the first 255 bytes of an image. To get around this
// CI will itself look at the first 255 bytes of an image to determine its relative safety. This can save a lot of
// processor power and time if it is actually a clean image, as it will be in nearly all instances _except_ an
// attempted XSS attack.
if (function_exists('getimagesize') && #getimagesize($file) !== FALSE)
{
if (($file = #fopen($file, 'rb')) === FALSE) // "b" to force binary
{
return FALSE; // Couldn't open the file, return FALSE
}
$opening_bytes = fread($file, 256);
fclose($file);
// These are known to throw IE into mime-type detection chaos
// <a, <body, <head, <html, <img, <plaintext, <pre, <script, <table, <title
// title is basically just in SVG, but we filter it anyhow
if ( ! preg_match('/<(a|body|head|html|img|plaintext|pre|script|table|title)[\s>]/i', $opening_bytes))
{
return TRUE; // its an image, no "triggers" detected in the first 256 bytes, we're good
}
else
{
return FALSE;
}
}
if (($data = #file_get_contents($file)) === FALSE)
{
return FALSE;
}
$CI =& get_instance();
return $CI->security->xss_clean($data, TRUE);
}
// --------------------------------------------------------------------
/**
* Set an error message
*
* #param string
* #return void
*/
public function set_error($msg)
{
$CI =& get_instance();
$CI->lang->load('upload');
if (is_array($msg))
{
foreach ($msg as $val)
{
$msg = ($CI->lang->line($val) == FALSE) ? $val : $CI->lang->line($val);
$this->error_msg[] = $msg;
log_message('error', $msg);
}
}
else
{
$msg = ($CI->lang->line($msg) == FALSE) ? $msg : $CI->lang->line($msg);
$this->error_msg[] = $msg;
log_message('error', $msg);
}
}
// --------------------------------------------------------------------
/**
* Display the error message
*
* #param string
* #param string
* #return string
*/
public function display_errors($open = '<p>', $close = '</p>')
{
$str = '';
foreach ($this->error_msg as $val)
{
$str .= $open.$val.$close;
}
return $str;
}
// --------------------------------------------------------------------
/**
* List of Mime Types
*
* This is a list of mime types. We use it to validate
* the "allowed types" set by the developer
*
* #param string
* #return string
*/
public function mimes_types($mime)
{
global $mimes;
if (count($this->mimes) == 0)
{
if (defined('ENVIRONMENT') AND is_file(APPPATH.'config/'.ENVIRONMENT.'/mimes.php'))
{
include(APPPATH.'config/'.ENVIRONMENT.'/mimes.php');
}
elseif (is_file(APPPATH.'config/mimes.php'))
{
include(APPPATH.'config//mimes.php');
}
else
{
return FALSE;
}
$this->mimes = $mimes;
unset($mimes);
}
return ( ! isset($this->mimes[$mime])) ? FALSE : $this->mimes[$mime];
}
// --------------------------------------------------------------------
/**
* Prep Filename
*
* Prevents possible script execution from Apache's handling of files multiple extensions
* http://httpd.apache.org/docs/1.3/mod/mod_mime.html#multipleext
*
* #param string
* #return string
*/
protected function _prep_filename($filename)
{
if (strpos($filename, '.') === FALSE OR $this->allowed_types == '*')
{
return $filename;
}
$parts = explode('.', $filename);
$ext = array_pop($parts);
$filename = array_shift($parts);
foreach ($parts as $part)
{
if ( ! in_array(strtolower($part), $this->allowed_types) OR $this->mimes_types(strtolower($part)) === FALSE)
{
$filename .= '.'.$part.'_';
}
else
{
$filename .= '.'.$part;
}
}
$filename .= '.'.$ext;
return $filename;
}
// --------------------------------------------------------------------
/**
* File MIME type
*
* Detects the (actual) MIME type of the uploaded file, if possible.
* The input array is expected to be $_FILES[$field]
*
* #param array
* #return void
*/
protected function _file_mime_type($file)
{
// We'll need this to validate the MIME info string (e.g. text/plain; charset=us-ascii)
$regexp = '/^([a-z\-]+\/[a-z0-9\-\.\+]+)(;\s.+)?$/';
/* Fileinfo extension - most reliable method
*
* Unfortunately, prior to PHP 5.3 - it's only available as a PECL extension and the
* more convenient FILEINFO_MIME_TYPE flag doesn't exist.
*/
if (function_exists('finfo_file'))
{
$finfo = finfo_open(FILEINFO_MIME);
if (is_resource($finfo)) // It is possible that a FALSE value is returned, if there is no magic MIME database file found on the system
{
$mime = #finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
/* According to the comments section of the PHP manual page,
* it is possible that this function returns an empty string
* for some files (e.g. if they don't exist in the magic MIME database)
*/
if (is_string($mime) && preg_match($regexp, $mime, $matches))
{
$this->file_type = $matches[1];
return;
}
}
}
/* This is an ugly hack, but UNIX-type systems provide a "native" way to detect the file type,
* which is still more secure than depending on the value of $_FILES[$field]['type'], and as it
* was reported in issue #750 (https://github.com/EllisLab/CodeIgniter/issues/750) - it's better
* than mime_content_type() as well, hence the attempts to try calling the command line with
* three different functions.
*
* Notes:
* - the DIRECTORY_SEPARATOR comparison ensures that we're not on a Windows system
* - many system admins would disable the exec(), shell_exec(), popen() and similar functions
* due to security concerns, hence the function_exists() checks
*/
if (DIRECTORY_SEPARATOR !== '\\')
{
$cmd = 'file --brief --mime ' . escapeshellarg($file['tmp_name']) . ' 2>&1';
if (function_exists('exec'))
{
/* This might look confusing, as $mime is being populated with all of the output when set in the second parameter.
* However, we only neeed the last line, which is the actual return value of exec(), and as such - it overwrites
* anything that could already be set for $mime previously. This effectively makes the second parameter a dummy
* value, which is only put to allow us to get the return status code.
*/
$mime = #exec($cmd, $mime, $return_status);
if ($return_status === 0 && is_string($mime) && preg_match($regexp, $mime, $matches))
{
$this->file_type = $matches[1];
return;
}
}
if ( (bool) #ini_get('safe_mode') === FALSE && function_exists('shell_exec'))
{
$mime = #shell_exec($cmd);
if (strlen($mime) > 0)
{
$mime = explode("\n", trim($mime));
if (preg_match($regexp, $mime[(count($mime) - 1)], $matches))
{
$this->file_type = $matches[1];
return;
}
}
}
if (function_exists('popen'))
{
$proc = #popen($cmd, 'r');
if (is_resource($proc))
{
$mime = #fread($proc, 512);
#pclose($proc);
if ($mime !== FALSE)
{
$mime = explode("\n", trim($mime));
if (preg_match($regexp, $mime[(count($mime) - 1)], $matches))
{
$this->file_type = $matches[1];
return;
}
}
}
}
}
// Fall back to the deprecated mime_content_type(), if available (still better than $_FILES[$field]['type'])
if (function_exists('mime_content_type'))
{
$this->file_type = #mime_content_type($file['tmp_name']);
if (strlen($this->file_type) > 0) // It's possible that mime_content_type() returns FALSE or an empty string
{
return;
}
}
$this->file_type = $file['type'];
}
// --------------------------------------------------------------------
}