Unique (but not Insane) Filenames
I told the UploadedFile
object to move the file into public/uploads
. And it did... but I kinda get the feeling it wasn't trying very hard. I mean, that is a horrible filename. Well, to be fair, this is the temporary filename that PHP decided to use.
Using the Original Filename
Fortunately, the move()
method has a second argument: the name to give to the file. The easiest name to use is: $uploadedFile->getClientOriginalName()
. This is the name that the file had on my computer: it's one of the pieces of data that is sent up on the request, along with the file contents.
// ... lines 1 - 15 | |
class ArticleAdminController extends BaseController | |
{ | |
// ... lines 18 - 73 | |
public function temporaryUploadAction(Request $request) | |
{ | |
// ... lines 76 - 78 | |
dd($uploadedFile->move( | |
$destination, | |
$uploadedFile->getClientOriginalName() | |
)); | |
} | |
// ... lines 84 - 121 | |
} |
Move over and resubmit the form again. There it is: astronaut.jpg
!
Security Concerns
But there are a few problems with this. Number one is security. Boo security! I know, I know, if the world were more butterflies and ice cream cones, we wouldn't need to worry about this stuff. But when it comes to file uploads, security concerns are real.
Right now, our upload form has no validation at all. So even though we are intending for this to be an image, the user could upload anything. And to make things worse, the file will then be publicly accessible. Someone could basically use our site as a private file storage, even storing viruses and trying to trick people into downloading it from our trusted domain. We'll talk about validation a bit later: it is critical that you do not allow your users to upload any file type.
Side note: no matter how you build your app or what safeguards you put it place, you should always make sure that your web server will only parse your main public/index.php
file through PHP. If your server is configured to execute any file ending in .php
through PHP, that is a huge security risk. Ok, back to butterflies and ice cream.
Even after we add validation to guarantee that the uploaded file is actually an image, the user could still successfully upload an image with a .exe
or .php
file extension! Even if we validate the file type, allowing fake extensions is weird... and could be risky.
So problem number one is security and we'll tackle part of it in a minute and the other part when we talk about validation.
Problem number two is that the filename is not guaranteed to be unique! If someone else uploads a file called astronaut.jpg
, boom! My schweet photo is gone!
Making Filenames Unique
There are a few ways to handle the unique problem - but the easiest one is just to add some sort of unique id to the filename. Set $newFilename
to uniqid()
, a '-'
then $uploadedFile->getClientOriginalName()
. Below, use $newFilename
.
// ... lines 1 - 73 | |
public function temporaryUploadAction(Request $request) | |
{ | |
// ... lines 76 - 78 | |
$newFilename = uniqid().'-'.$uploadedFile->getClientOriginalName(); | |
dd($uploadedFile->move( | |
$destination, | |
$newFilename | |
)); | |
} | |
// ... lines 86 - 125 |
Let's try that! Better. It's kind of an ugly hash on the beginning of the filename, but it does solve the unique problem. You can also use a shorter hash or, when we actually save this data to our Article
object, you could use the Article id instead of the hash. Or, if you really want to keep the original filename exactly as it was, well... we'll talk about that later when we upload "references" to our Article.
Correcting the File Extension
The other thing I want to solve is the possibility that someone uploads an image with a totally insane file extension - like .potato
. We can fix this really nicely. Create a new variable called $originalFilename
set to pathinfo()
with $uploadedFile->getClientOriginalName()
and the constant PATHINFO_FILENAME
.
This will give us the original filename - astronaut.jpg
- but without the file extension: so, just astronaut
. Then, for the filename, use $originalFilename
, a dash, the uniqid()
, a period, and now the real extension of the file: $uploadedFile->guessExtension()
. Oh, see how there are two methods: ->guessClientExtension()
and ->guessExtension()
? The difference is important: the guessExtension()
method looks at the file contents, determines the mime type, and returns the file extension for that. But the guessClientExtension()
uses the mime type the user sent... which can't be trusted.
// ... lines 1 - 73 | |
public function temporaryUploadAction(Request $request) | |
{ | |
// ... lines 76 - 79 | |
$originalFilename = pathinfo($uploadedFile->getClientOriginalName(), PATHINFO_FILENAME); | |
$newFilename = $originalFilename.'-'.uniqid().'.'.$uploadedFile->guessExtension(); | |
// ... lines 82 - 86 | |
} | |
// ... lines 88 - 127 |
So, we're not validating that this is an image file yet, but no matter what they upload, we should now get the correct file extension.
Give it a try! Nice! We've got a .jpeg
ending.
Optional: Normalizing Filenames
There's one last thing you might want to do... and it's really optional. Go back to the form. One of my files has uppercase letters and spaces inside. Let's try uploading that. It works! There is no problem with storing spaces or... most weird characters on a filesystem. But if you want to guarantee cleaner filenames, there's an easy way to do that. I'll use a class called Urlizer
: this comes from the gedmo/doctrine-extensions
library. It has a nice method called urlize()
and we can wrap our $originalFilename
in that to make it a bit cleaner.
// ... lines 1 - 8 | |
use Gedmo\Sluggable\Util\Urlizer; | |
// ... lines 10 - 74 | |
public function temporaryUploadAction(Request $request) | |
{ | |
// ... lines 77 - 81 | |
$newFilename = Urlizer::urlize($originalFilename).'-'.uniqid().'.'.$uploadedFile->guessExtension(); | |
// ... lines 83 - 87 | |
} | |
// ... lines 89 - 128 |
Try that out. Nice! So now we have a unique, normalized filename that at least looks a bit like the original filename. Later, we'll see how we can keep the exact original filename in all cases... if you care. But unless your users are downloading these files, the exact filenames aren't usually that important.
Next: it's time to put this upload field properly into our Symfony form and save the filename to the Article
entity.
For anyone following along and having troubles with the using the
Urlizer
class. You can instead use a Slugger class provided from thesymfony/string
component. You can either injectSymfony\Component\String\Slugger\SluggerInterface
into your controller method, or just instantiate a slugger manually vianew Symfony\Component\String\Slugger\AsciiSlugger()
. Then just call theslug()
method on your slugger object and pass in the original filename.