Buy
Buy

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.

Leave a comment!

  • 2019-02-27 weaverryan

    Hey Krzysztof Krakowiak!

    Ah... very interesting! I can't guarantee that this will be there - at the moment, I've finished coding the tutorial, but I might add this at the end - i'm not sure. Indeed, being able to do all of this on the client-side before uploading is an interesting challenge.

    For the progress bar, we're not talking about that directly, but relying on Dropzone to do that. However, in general, this can be accomplished via JavaScript - there is a way to get notified of upload progress, which you can then use to figure out the percentage (by comparing it with the file's filesize - also available in JS). So, it's not a topic we covered directly - because the heavy-lifting of handling the AJAX upload was handled by Dropzone.

    Cheers!

  • 2019-02-25 Krzysztof Krakowiak

    Thanks Ryan for your answer, but could you cover one particular topic for multiple file upload with preview and image rotation. What I am trying to archive is to see the preview of all selected for upload images, and then have ability to rotate them if needed, before these are send to server. It means that I am not sending original selected files, but modified ones.

    So user will select image files, these images will be displayed as a list with a preview, and every image will get set of buttons to rotate or remove form list. I believe that this is a pretty standard requirement which requires few extra features from HTML5/JavaScript (FileReader, Blob, FormData, etc..). If this is achievable with jQuery, please show how :).

    I did a lot a lot of googling for that, it is hard to find correct answers and understand what exactly is required.

    I work on my own solution but this will work with Base64 (data:image/jpeg;charset=utf-8;base64), some people are using canvas instead, I am not sure why.

    ah, one more feature will be required, progress bar for uploaded images.

  • 2019-02-25 weaverryan

    Hey Krzysztof Krakowiak!

    We are going to cover this... partly :). We're going to cover using Dropzone to upload multiple files via AJAX really nicely (including deleting, updating some info about the files, and even re-ordering). But, because I don't want to make React or Vue a requirement for understanding this, I've tried to keep it simple with jQuery (it's also done outside of Encore, but re-using the code in Encore wouldn't require much change - and we're happy to help).

    I hope that satisfies at least a big :).

    Cheers!

  • 2019-02-22 Victor Bocharsky

    Hey cybernet2u ,

    See this comment: https://symfonycasts.com/sc... - we're going to cover multi-file uploads. And sure, upload size limit and extensions will be covered as well.

    Cheers!

  • 2019-02-22 Krzysztof Krakowiak

    Could you extend this tutorial and add bits form Webpack Encore with advanced JavaScript (React or Vue) to upload nicely multiple files.

  • 2019-02-21 cybernet2u

    what about multi-file upload ? upload size limit ... etc