Custom storage path per model - laravel

I'm implementing an email handling system where I want to save the original email and all its attachments to a path. For example - mail-data/123456/5
Where 123456 is the parent ID and 5 is the child ID.
In filesystems.php I've created a custom disk called mail-data
'mail-data' => [
'driver' => 'local',
'root' => storage_path('app/public/mail-data'),
'visibility' => 'private',
],
This works great as far as setting a prefix for the storage path, visibility etc. However, what I want to be able to do is on a per model basis, call a storage property and it return the mail-data driver set to the exact path. This way, all of my logic can simply be:
$model->storage->put($file->getFilename(), $file->stream());
rather than:
$path = Storage::disk('mail-data')->put($model->parent_id . '/' . $model->id . '/' . $file->getFilename(), $file->getStream())
I think the best way to do this by creating an accessor on the model, and I've been able to update the adapter, I just don't know how to update that on the Filesystem instance and return it?
public function getStorageAttribute()
{
$storage = Storage::disk('mail-data');
$adapter = $storage->getAdapter();
$adapter->setPathPrefix($adapter->getPathPrefix() . $this->parent_id . '/' . $this->id);
// what to do here to return our modified storage instance?
}

Right, I was a bit stupid here... it turns out that when you run setPathPrefix on the adapter, it's all by reference so the code above actually had the desired effect. For anyone Googling in the future, here is the final code -
On the model -
/**
* Get our storage disk for this model
*
* #return \Illuminate\Contracts\Filesystem\Filesystem
*/
public function getStorageAttribute()
{
$storage = Storage::disk('mail-data');
$adapter = $storage->getAdapter();
$adapter->setPathPrefix($adapter->getPathPrefix() . $this->ticket_id . '/' . $this->id);
return $storage;
}
I can then access my models storage at the absolute storage path simply using $model->storage. So my now much cleaner code for saving my mail data looks like this (no more calculating paths and having to worry about calculating paths anywhere else in my logic) -
$storage = $model->storage;
$storage->put('email.eml', $mail->message()->getStream());
/** #var MimePart $attachment */
foreach ($mail->attachments() as $attachment) {
$storage->put($attachment->getFilename(), $attachment->getStream());
}
Very happy with that solution and I hope it comes in handy for someone else in the future :)

Related

Eloquent | Setting column value on insert

So currently, I have a table which is for business locations in order to seperate the locations from the actual businesses table.
In this table, I want to store the longitude and latitude and obviously I can't get the user to input that without requiring them to do manual work which I really want to avoid.
So I wrote a class in order to get the longitude and latitude ready for entry to the database.
I've read online about doing setLongitudeAttribute() function within the model but I'm basing it off of the whole address which they are entering so I need to capture the whole of the request and then input it in myself.
I understand I can do this in the controller and do a custom insert but I didn't know if it was possible to keep it all contained within the model.
So essentially to break it down.
User inputs the full address including all address lines and postal/zip code.
Eloquent model captures this data.
Converts the address to long and lat
Model then handles the request in order to set the longitude and latitude based on the address.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Http\Calls\LongitudeLatitude;
class BusinessLocation extends Model
{
/**
* #var array
*/
protected $fillable = [
'business_id',
'address_line_1',
'address_line_2',
'address_line_3',
'address_line_4',
'postcode',
'longitude',
'latitude'
];
/**
* #var string
*/
protected $address;
protected function setAddressLine1Attribute($value)
{
$this->address .= $value . '+';
$this->attributes['address_line_1'] = $value;
}
protected function setAddressLine2Attribute($value)
{
$this->address .= $value . '+';
$this->attributes['address_line_2'] = $value;
}
protected function setAddressLine3Attribute($value)
{
$this->address .= $value . '+';
$this->attributes['address_line_3'] = $value;
}
protected function setAddress4Attribute($value)
{
$this->address .= $value . '+';
$this->attributes['address_line_4'] = $value;
}
protected function setPostcodeAttribute($value)
{
$this->address .= $value;
$this->attributes['postcode'] = $value;
$this->setCoordinates();
}
protected function setCoordinates()
{
$long_lat = new LongitudeLatitude();
$coords = $long_lat->get($this->address);
$this->attributes['longitude'] = $coords['longitude'];
$this->attributes['latitude'] = $coords['latitude'];
}
First of all, and as you correctly mention, it doesn't look a good practice to write this logic in the model, but in the controller.
If I were you, I would leave the model logic to the model, and the rest to the controller and the other objects required to "transform" the input. This could even be done from the client with some javascript library if you will, sending just the lat and long to the server (your choice).
If you still want to create a method / setter in the model, I would recommend you to get some inspiration in any consolidated library, and the more official, the best, as for example: Laravel Cashier (https://laravel.com/docs/5.6/billing), which provides extra behaviours to the model, in a separate logic unit.
If you want to follow Laravel Cashier approach, it uses a trait to apply the behaviour:
https://github.com/laravel/cashier/blob/7.0/src/Billable.php
It uses the official stripe package (it could be, in your case, the class you wrote, or any other such as Google Maps SDK) as well as other helper and data objects, which would make your model lighter in memory and make another objects responsible of the additional logic (easier to test and maintain), while it would also embed the behaviours in your model.
About your code...
You can not guarantee that the setters are executed in the order you want (or you shouldn't trust it will always do), so, in case you want to continue with the code you already have, I would suggest to move $this->setCoordinates(); to somewhere else, for example an event observer such as creating, updating and saving:
https://laravel.com/docs/5.6/eloquent#events
And also evaluate the address every time you call it, as follows:
public function setCoordinates()
{
$address = $this->attributes['address_line_1'] . '+' .
$this->attributes['address_line_2'] . '+' .
$this->attributes['address_line_3'] . '+' .
$this->attributes['address_line_4'] . '+' .
$this->attributes['postcode'];
$long_lat = new LongitudeLatitude();
$coords = $long_lat->get($address);
$this->attributes['longitude'] = $coords['longitude'];
$this->attributes['latitude'] = $coords['latitude'];
}
Notice that the method is public, so you can call it from the event observer.
There are other business logic factors to consider:
- Do you want to give the possibility to fill manually the coordinates? if not, you should remove them from the $fillable array.
- Do you really need to evaluate the coordinates every time you update any of the address fields? Then you may want to have a status property to verify if the coordinates are pristine (in address fields setters set pristine as false, in coordinates getters check if the value is null or if it's not pristine, and if so, set the coordinates again).

Are laravel's routes safeguarding enough against file traversal attacks?

Route::get('/transaction/{name}', 'TransactionController#download');
public function download($name){
$path = storage_path('app/something/') . $name . '.xml';
return response()->download($path);
}
The user shall using this action only be able to download .xml files in app/something.
Is it possible to to download data outside of the specified app/something folder.
Laravel doesn't protect against traversal attacks - the router will return any value with your code example, meaning that someone could get access to your filesystem!
You an use PHP's basename() to sanitise $name by removing any path references from the string:
Route::get('/transaction/{name}', 'TransactionController#download');
public function download($name){
$path = storage_path('app/something/') . basename($name, '.xml') . '.xml';
return response()->download($path);
}
As far as i know Laravel will compile your path to:
#^/transaction/(?P<name>[^/]++)$#s
So simple / will not not work..
You could use more sophisticated backslash - but it depends on server..
At the end - Remember not to trust all user input.. No matter does it goes through routing or received directly..
Updated answer
As you can see below, it's definitely possible to do malicious stuff within Laravel routes. Given your function setup, the chance of someone doing something you don't want is small, because he/she can only alter the $name variable.
You can still write some extra code like this (found on viblo.asia):
$basepath = '/foo/bar/baz/'; // Path to xml file
$realBase = realpath($basepath);
$userpath = $basepath . $_GET['path'];
$realUserPath = realpath($userpath);
if ($realUserPath === false || strpos($realUserPath, $realBase) !== 0) {
//Directory Traversal!
} else {
//Good path!
}
To prevent users from accessing files they aren't allowed to.
Old, but relevant answer
Just tried this in Homestead:
Route::get(
'/',
function () {
dump(exec('ls ' . storage_path() . '/../../../'));
}
);
And that prints the corresponding folder just fine:
So I'd say that it's definitely possible to do stuff outside of the specified folder. Try this for yourself for example:
Route::get(
'/',
function () {
for ($i = 0; $i < 10; $i++) {
$path = str_repeat('/..', $i);
dump(exec('ls ' . storage_path() . $path));
}
}
);
And see your folders appear on screen when you hit the / route.

Laravel 5.3 dynamic routing to multiple controllers

I'm using Laravel 5.3. I have a bunch of urls that I'd like to handle with a single route, to multiple controllers.
e.g.
GET /admin/foo => FooController#index
GET /admin/foo/edit/1 => FooController#edit($id)
GET /admin/bar => BarController#index
GET /admin/bar/edit/1 => BarController#item($id)
GET /admin/baz => BazController#index
GET /admin/baz/edit/1 => BazController#item($id)
etc.
I want to be able to detect if the controller exists, and if not throw a 404 or route to a default controller (which may throw a 404).
Below is what I've got so far, but I'm not sure what I'm doing. Shouldn't I be instantiating the controller using the service container? I don't think I should be hardcoding namespaces like this. And my handling of the id parameter is sketchy. Perhaps I should have two routes for these two patterns or something?
Route::get('/admin/{entityType}/{action?}/{id?}', function ($entityType, $action = 'index', $id = null) {
$controllerClass = 'App\Http\Controllers\\' . ucfirst($entityType) . 'Controller';
$controller = new $controllerClass;
$route = app(\Illuminate\Routing\Route::class);
$container = app(\Illuminate\Container\Container::class);
return (new Illuminate\Routing\ControllerDispatcher($container))->dispatch($route, $controller, $action);
abort(404);
});
I'd recommend you to define a route for every controller explicitly. This is the best way to build a maintainable app.
Also, if using one route and one method is an option (with right architecure it is) use one route:
Route::get('/admin/{entityType}/{action?}/{id?}', 'Controller#method');
And one entry point:
public function method($entity, $action = null, $id = null)
{
// Handle request here.
https://laravel.com/docs/5.3/routing#parameters-optional-parameters

I can't find the implementation for Storage Facade in laravel

I'm new with laravel and I'm working in fileststem on laravel
(I want to do usual fileststem process like -make dir - copy - put -delete -ect)
I'm using laravel "Storage" Facade
but when i type
i referenced the class above like this in my code
use Illuminate\Support\Facades\Storage;
for example below :
if (file_exists(public_path($oldImage))) {
Storage::delete($oldImage);
}
nothing happens ,and when i refer to the class code i found this :
namespace Illuminate\Support\Facades;
/**
* #see \Illuminate\Filesystem\FilesystemManager
*/
class Storage extends Facade
{
/**
* Get the registered name of the component.
*
* #return string
*/
protected static function getFacadeAccessor()
{
return 'filesystem';
}
}
so where is the implementation and if you have alternative way to deal with
filesystem process rather than "Storage" facade ??
Storage is a facade and accesses the class Filesystem located here: vendor/laravel/framework/src/Illuminate/Filesystem/Filesystem.php
As you can see in the official filesystem documentation the code snippets use Storage.
UPDATE:
You should add use Storage; to be able to use the Storage facade.
I recommend reading the Laravel 8.X docs to get an initial heads up: https://laravel.com/docs/8.x/filesystem
NOTE: Before you get too carried away, make sure you understand the difference between local and public.
For starters, you should make your first goal to upload a file and acquire the UploadedFile type.
You can access a single file via something like $request->file('name'), or an array of images via something like:
// $request->input('images')
foreach ($images as $image) {
\Log::debug($image->getClientOriginalName());
}
If your file upload can be single and/or multiple, I recommend going with the array approach because a single file wrapped in an array allows you to use the same syntax for single and multi uploads (ie: that foreach loop works fine with one image, no extra code).
Here's an example:
use Illuminate\Support\Facades\Storage;
$slug = 'davids-sandwich-photos';
foreach ($images as $image) {
Storage::putFileAs(
'images' .'/'. $slug,
$image,
$image->getClientOriginalName()
);
}
Storage::putFileAs() can take 3 parameters: directory, content, filename. You can see above in the code that I interpolated a mix of static and derived directory name. You could do something like 'images' .'/'. $slug .'/'. Auth::user()->id to save the file in /images/davids-sandwich-photos/11.
Then, check in your repo directory: /storage/app/ and look for the images directory.
You can manually delete the folders while testing to get your bearings straight.
That should be enough to get most people started.
To avoid using the Storage facade, you can use:
foreach ($images as $image) {
$image->storeAs(
'examples' .'/'. $slug,
$image->getClientOriginalName(),
'public'
);
}
--
Check out config/filesystems.php under the disks section if you want to start manipulating the drivers, but I'm not a DB admin expert here.
I also saved this along my journey: https://medium.com/#shafiya.ariff23/how-to-store-uploaded-images-in-public-folder-in-laravel-5-8-and-display-them-on-shared-hosting-e31c7f37a737. You might need that if you get stuck with something like symlinking.
<img
v-for="image in example.images"
:key="image.filename"
:src="`/storage/examples/${example.slug}/${image.filename}`"
>
NOTE: The important part with Vue JS is to use <img src="/storage/examples/slug/filename.jpg"> if your file is located in your repository as /storage/app/public/examples/slug/filename.jpg Pay close attention to every character.
The public_path function returns the fully qualified path to the public directory ie public directory inside the laravel application. When using Storage, the path is set to the storage/app directory.
if (file_exists(public_path($oldImage))) {
//public_path($oldImage) will check for file in public directory
Storage::delete($oldImage); //Will delete file in storage/app directory
}
The modified code should be
if(Storage::has($oldImage)){
Storage::delete($oldImage);
}

How to protect image from public view in Laravel 5?

I have installed Laravel 5.0 and have made Authentication. Everything is working just fine.
My web site is only open for Authenticated members. The content inside is protected to Authenticated members only, but the images inside the site is not protected for public view.
Any one writes the image URL directly can see the image, even if the person is not logged in to the system.
http://www.somedomainname.net/images/users/userImage.jpg
My Question: is it possible to protect images (the above URL example) from public view, in other Word if a URL of the image send to any person, the individual must be member and login to be able to see the image.
Is that possible and how?
It is possible to protect images from public view in Laravel 5.x folder.
Create images folder under storage folder (I have chosen storage folder because it has write permission already that I can use when I upload images to it) in Laravel like storage/app/images.
Move the images you want to protect from public folder to the new created images folder. You could also chose other location to create images folder but not inside the public folder, but with in Laravel folder structure but still a logical location example not inside controller folder. Next you need to create a route and image controller.
Create Route
Route::get('images/users/{user_id}/{slug}', [
'as' => 'images.show',
'uses' => 'ImagesController#show',
'middleware' => 'auth',
]);
The route will forward all image request access to Authentication page if person is not logged in.
Create ImagesController
class ImagesController extends Controller {
public function show($user_id, $slug)
{
$storagePath = storage_path('app/images/users/' . $user_id . '/' . $slug);
return Image::make($storagePath)->response();
}
}
EDIT (NOTE)
For those who use Laravel 5.2 and newer. Laravel introduces new and better way to serve files that has less overhead (This way does not regenerate the file as mentioned in the answer):
File Responses
The file method can be used to display a file, such as an image or
PDF, directly in the user's browser instead of initiating a download.
This method accepts the path to the file as its first argument and an
array of headers as its second argument:
return response()->file($pathToFile);
return response()->file($pathToFile, $headers);
You can modify your storage path and file/folder structure as you wish to fit your requirement, this is just to demonstrate how I did it and how it works.
You can also added condition to show the images only for specific members in the controller.
It is also possible to hash the file name with file name, time stamp and other variables in addition.
Addition: some asked if this method can be used as alternative to public folder upload, YES it is possible but it is not recommended practice as explained in this answer. So the same method can be also used to upload images in storage path even if you do not intend to protect them, just follow the same process but remove 'middleware' => 'auth',. That way you won't give 777 permission in your public folder and still have a safe uploading environment. The same mentioned answer also explain how to use this method with out authentication in case some one would use it or giving alternative solution as well.
In a previous project I protected the uploads by doing the following:
Created Storage Disk:
config/filesystems.php
'myDisk' => [
'driver' => 'local',
'root' => storage_path('app/uploads'),
'url' => env('APP_URL') . '/storage',
'visibility' => 'private',
],
This will upload the files to \storage\app\uploads\ which is not available to public viewing.
To save files on your controller:
Storage::disk('myDisk')->put('/ANY FOLDER NAME/' . $file, $data);
In order for users to view the files and to protect the uploads from unauthorized access. First check if the file exist on the disk:
public function returnFile($file)
{
//This method will look for the file and get it from drive
$path = storage_path('app/uploads/ANY FOLDER NAME/' . $file);
try {
$file = File::get($path);
$type = File::mimeType($path);
$response = Response::make($file, 200);
$response->header("Content-Type", $type);
return $response;
} catch (FileNotFoundException $exception) {
abort(404);
}
}
Serve the file if the user have the right access:
public function licenceFileShow($file)
{
/**
*Make sure the #param $file has a dot
* Then check if the user has Admin Role. If true serve else
*/
if (strpos($file, '.') !== false) {
if (Auth::user()->hasAnyRole(['Admin'])) {
/** Serve the file for the Admin*/
return $this->returnFile($file);
} else {
/**Logic to check if the request is from file owner**/
return $this->returnFile($file);
}
} else {
//Invalid file name given
return redirect()->route('home');
}
}
Finally on Web.php routes:
Route::get('uploads/user-files/{filename}', 'MiscController#licenceFileShow');
I haven't actually tried this but I found Nginx auth_request module that allows you to check the authentication from Laravel, but still send the file using Nginx.
It sends an internal request to given url and checks the http code for success (2xx) or failure (4xx) and on success, lets the user download the file.
Edit: Another option is something I've tried and it seemed to work fine. You can use X-Accel-Redirect -header to serve the file from Nginx. The request goes through PHP, but instead of sending the whole file through, it just sends the file location to Nginx which then serves it to the client.
if I am understanding you it's like !
Route::post('/download/{id}', function(Request $request , $id){
{
if(\Auth::user()->id == $id) {
return \Storage::download($request->f);
}
else {
\Session::flash('error' , 'Access deny');
return back();
}
}
})->name('download')->middleware('auth:owner,admin,web');
Every file inside the public folder is accessible in the browser. Anyone easily gets that file if they find out the file name and storage path.
So better option is to store the file outside the public folder eg: /storage/app/private
Now do following steps:
create a route (eg: private/{file_name})
Route::get('/private/{file_name}', [App\Http\Controllers\FileController::class, 'view'])->middleware(['auth'])->name('view.file');
create a function in a controller that returns a file path. to create a controller run the command php artisan make:controller FileController
and paste the view function in FileController
public function view($file)
{
$filePath = "notes/{$file}";
if(Storage::exists($filePath)){
return Storage::response($filePath);
}
abort(404);
}
then, paste use Illuminate\Support\Facades\Storage; in FileController for Storage
And don't forget to assign middleware (in route or controller) as your requirement(eg: auth)
And now, only those who have access to that middleware can access that file through a route name called view.file

Resources