Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Unique (but not Insane) Filenames

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

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!

17
Login or Register to join the conversation
Yuki K. Avatar
Yuki K. Avatar Yuki K. | posted 2 years ago

Hello

when trying to install via ”composer require gedmo/doctrine-extensions” I get messages : Your requirements could not be resolved to an installable set of packages.

I'm using the code from this tutorial as is.
I don't know how to solve this.

”Problem 1
- Root composer.json requires gedmo/doctrine-extensions ^3.0 -> satisfiable by gedmo/doctrine-extensions[v3.0.0].
- gedmo/doctrine-extensions v3.0.0 requires doctrine/common ^2.13 || ^3.0 -> found doctrine/common[2.13.0, 2.13.1, 2.13.2, 2.13.3, 3.0.0, ..., 3.1.0] but the package is fixed to 2.12.0 (lock file version) by a partial update and that version does not match. Make sure you list it as an argument for the update command.
Problem 2
- stof/doctrine-extensions-bundle is locked to version v1.3.0 and an update of this package was not requested.
- stof/doctrine-extensions-bundle v1.3.0 requires gedmo/doctrine-extensions ^2.3.4 -> found gedmo/doctrine-extensions[v2.3.4, ..., v2.4.42] but it conflicts with your root composer.json require (^3.0).

Use the option --with-all-dependencies (-W) to allow upgrades, downgrades and removals for packages currently locked to specific versions.”

Reply
Yuki K. Avatar

sorry, I forgot to type "use Gedmo\Sluggable\Util\Urlizer;"
It's resolved.

Reply

Glad you figured it out - thanks for the update!

Reply
Jerzy Avatar

Hi!
I have a problem, I would really love to use Urlizer, but it seems outdated now. When I do " composer require gedmo/doctrine-extensions", it gives me a lot of errors like:

" - gedmo/doctrine-extensions v2.4.0 requires doctrine/common ~2.4 -> satisfiable by doctrine/common[2.12.0, 2.13.0, 2.13.1, 2.13.2, 2.13.3, v2.10.0, v2.11.0, v2.4.0, v2.4.1, v2.4.2, v2.4.3, v2.5.0, v2.5.1, v2.5.2, v2.5.3, v2.6.0, v2.6.1, v2.6.2, v2.7.0, v2.7.1, v2.7.2, v2.7.3, v2.8.0, v2.8.1, v2.9.0].
- Can only install one of: doctrine/common[2.12.0, 3.0.2].
- ...
- Can only install one of: doctrine/common[v2.9.0, 3.0.2].
- Installation request for doctrine/common (locked at 3.0.2) -> satisfiable by doctrine/common[3.0.2].
Do you know any simple solution to this problem?

Reply

Hey @aeiouy

Is it related to course code? If no and you are using latest symfony I'd recommend to use slugger from symfony/string component. You can read more about it here https://symfony.com/doc/current/components/string.html#slugger

Cheers!

Reply
Jerzy Avatar

Yes, it's related, cause Urlizer is beeing used in this video so I didn't get the first part of your answer, but slugger fixed my problem so I'm glad you answered me!
Cheers!

Reply

I mean you got this output on course code? Which is used for this tutorial? Or it's your personal project which you building using this tutorial?

Hope this is now more clear :)

Cheers!

Reply
Jerzy Avatar

Ah okay, then you are right.
It was on my personal project :)

Reply
Abdessamad E. Avatar
Abdessamad E. Avatar Abdessamad E. | posted 3 years ago

Hello,
class Urlizer not defined for me!

Reply
Abdessamad E. Avatar

done!
composer require gedmo/doctrine-extensions

Reply

Hey Abdessamad,

Yep, you need to install the package with Composer first. Glad you figured it out yourself! And thanks for sharing your solution with others.

Cheers!

Reply
Krzysztof K. Avatar
Krzysztof K. Avatar Krzysztof K. | posted 4 years ago

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

Reply

Hey Krzysztof K.!

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!

Reply
Krzysztof K. Avatar

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.

Reply

Hey Krzysztof K.!

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!

Reply
Remus M. Avatar
Remus M. Avatar Remus M. | posted 4 years ago

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

Reply

Hey Remus M. ,

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!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial is built on Symfony 4 but works great in Symfony 5!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "aws/aws-sdk-php": "^3.87", // 3.87.10
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.1
        "knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
        "knplabs/knp-time-bundle": "^1.8", // 1.9.0
        "league/flysystem-aws-s3-v3": "^1.0", // 1.0.22
        "league/flysystem-cached-adapter": "^1.0", // 1.0.9
        "liip/imagine-bundle": "^2.1", // 2.1.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.1.0
        "oneup/flysystem-bundle": "^3.0", // 3.0.3
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.2.4
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.2.3
        "symfony/console": "^4.0", // v4.2.3
        "symfony/flex": "^1.9", // v1.17.6
        "symfony/form": "^4.0", // v4.2.3
        "symfony/framework-bundle": "^4.0", // v4.2.3
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.2.3
        "symfony/serializer-pack": "^1.0", // v1.0.2
        "symfony/twig-bundle": "^4.0", // v4.2.3
        "symfony/validator": "^4.0", // v4.2.3
        "symfony/web-server-bundle": "^4.0", // v4.2.3
        "symfony/yaml": "^4.0", // v4.2.3
        "twig/extensions": "^1.5" // v1.5.4
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.1.0
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.7
        "fzaninotto/faker": "^1.7", // v1.8.0
        "symfony/debug-bundle": "^3.3|^4.0", // v4.2.3
        "symfony/dotenv": "^4.0", // v4.2.3
        "symfony/maker-bundle": "^1.0", // v1.11.3
        "symfony/monolog-bundle": "^3.0", // v3.3.1
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.2.3
        "symfony/profiler-pack": "^1.0", // v1.0.4
        "symfony/var-dumper": "^3.3|^4.0" // v4.2.3
    }
}
userVoice