Rikijs
Rikijs

Reputation: 808

Show message about server error to user after file upload in `Symfony v6.4` with `OneUpUploaderBundle` and `UPPY`

Introduction

I am using:

As of time of writing there is no official OneUpUploaderBundle integration with UPPY. Luckily, developers of OneUpUploaderBundle are forward thinking and added support for a custom Uploader!

So I choose to implement it.


Implementation

Firstly, I chose to set up upload with Blueimp jQuery File Uploader as it is supported by bundle.

After success I moved to setting up upload with UPPY and Custom uploader.

For my CustomUploader I chose BlueimpController - from a bunch of other uploaders that bundle supports.

At the moment upload works with custom uploader (for UPPY).


Problem

For security reasons I do not allow upload of files where extension does not correspond with its mime type. I check this (on server, after file is received) with Symfony in UploadValidationListener. In this case I need to show error to the user, but do not know how to pass it to UPPY Dashboard.

Status is: file is uploaded successfully, but such a file is not kept - as it failed validation. Without a message, user would be left wondering: where does phantom files are, as they saw them uploading successfully!


Code

here is how UPPY is set up

'use strict';

import Translator from 'bazinga-translator';
import Uppy from '@uppy/core';
import Dashboard from '@uppy/dashboard';
import GoldenRetriever from '@uppy/golden-retriever';
import XHR from '@uppy/xhr-upload';

import '@uppy/core/dist/style.css';
import '@uppy/dashboard/dist/style.css';
import en_US from "@uppy/locales/lib/en_US";

const js_data = document.querySelector('#js-data');
let data_allowed_file_types_str = js_data.dataset.allowedFileTypes;
const data_allowed_file_types = data_allowed_file_types_str.split('|');
const data_allowed_file_size_min = js_data.dataset.allowedFileSizeMin;
const data_allowed_file_size_max = js_data.dataset.allowedFileSizeMax;
const data_upload_endpoint = js_data.dataset.uploadEndpoint;

const uppy = new Uppy({
    debug: false,
    locale: en_US,
    onBeforeFileAdded: (currentFile, files) =>
    {
        const do_not_allow_arr = ['do_not_allow_me_1.txt', 'do_not_allow_me_2.txt', 'do_not_allow_me_3.txt'];
        let allowed = true;
        do_not_allow_arr.forEach((element) =>
        {
            if (currentFile.name === element)
            {
                allowed = false;
                uppy.log(Translator.trans('upload.error.skipFilesFromBlockList'));
                uppy.info((Translator.trans('upload.error.skipFilesFromBlockList')), 'error', 5000);
                return allowed;
            }
        });
        return allowed;
    },
});

uppy.use(GoldenRetriever);

uppy.use(Dashboard, {
        inline: true,
        target: '#uppy-dashboard',
        id: 'uppy',
        width: '100%',
        proudlyDisplayPoweredByUppy: false,
        showProgressDetails: true,
    });

uppy.use(XHR, {
        endpoint: data_upload_endpoint
    });

uppy.setOptions({
    restrictions:
        {
            minNumberOfFiles: 1,
            maxNumberOfFiles: 1,
            minFileSize: data_allowed_file_size_min,
            minTotalFileSize: data_allowed_file_size_min,
            maxTotalFileSize: data_allowed_file_size_max,
            allowedFileTypes: data_allowed_file_types
        },
        //autoProceed: true
    });

uppy.on('file-added', (file) => {
    console.log('Added file', file);
});

uppy.on('upload-success', (file, responseObject) => {
    // (depending on the uploader plugin used, it might contain
    // less info, the example is for @uppy/xhr-upload)
    // responseObject = {
    //     status, // HTTP status code (0, 200, 300)
    //     body, // response body
    //     uploadURL // the file url, if it was returned
    // }
    console.log('after upload success');
});

uppy.on('upload-error', (file, responseObject) => {
    // responseObject = {
    //     status, // HTTP status code (0, 200, 300)
    //     body // response body
    // }
    console.log('after upload error');
});

here is validator code

<?php

namespace App\EventListener;

use App\Entity\FileType;
use App\UltraHelpers\UltraHelpers;
use Doctrine\Persistence\ManagerRegistry;
use Oneup\UploaderBundle\Event\ValidationEvent;
use Oneup\UploaderBundle\Uploader\Exception\ValidationException;
use Symfony\Component\Mime\MimeTypes;

class UploadValidationListener
{
    /**
     * @var ManagerRegistry
     */
    private ManagerRegistry $doctrine;

    /**
     * @var UltraHelpers
     */
    private UltraHelpers $ultraHelpers;

    public function __construct(
        ManagerRegistry $doctrine,
        UltraHelpers $ultraHelpers
    )
    {
        $this->doctrine = $doctrine;
        $this->ultraHelpers = $ultraHelpers;
    }

    public function onValidate(ValidationEvent $event): void
    {
        $repo_file_Type = $this->doctrine->getRepository(FileType::class);
        $file_extensions_allowed = $repo_file_Type->getActivatedFileTypeOnlyListArray();

        $mimeTypes = new MimeTypes();

        $errors = [];

        $file_list_banned = [];
        $file_list_banned[] = 'do_not_allow_me_1.txt';
        $file_list_banned[] = 'do_not_allow_me_2.txt';
        $file_list_banned[] = 'do_not_allow_me_3.txt';

        $config = $event->getConfig();
        $file = $event->getFile();

        $filtered_file_info = $this->ultraHelpers->filterFileInfoFromFileName($file->getClientOriginalName());
        $transliterated_file_name = $filtered_file_info['name'];
        $full_file_name = $transliterated_file_name .'.'. $filtered_file_info['extension'];

        $file_extension = $filtered_file_info['extension'];
        $file_mime_type_from_file = $mimeTypes->guessMimeType($file);
        $file_mime_types_from_extension = $mimeTypes->getMimeTypes($file_extension);

 
        // process forbidden_files
        if (in_array($full_file_name, $file_list_banned, true))
        {
            $errors[] = 'error.file_banned';
        }

        // If mime type of current file does not correspond to extensions mime type
        // Damaged file or specially prepared for research / hacking
        if (!in_array($file_mime_type_from_file, $file_mime_types_from_extension, true))
        {
            $errors[] = 'error.mime_type_mismatch';
        }

        if (!empty($errors))
        {
            throw new ValidationException(implode(', ', $errors));
        }
    }
}


Update 1

I added UploadExceptionListener yet it did not change anything. Files that are not corresponding to declared type (by extension) are still silently removed (on server) and UI shows it was a sucessful upload.

services.yaml

App\EventListener\UploadExceptionListener:
    tags:
        - { name: kernel.event_listener, event: kernel.exception }

ExceptionListener

<?php

namespace App\EventListener;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;

class UploadExceptionListener
{
    public function __invoke(ExceptionEvent $event): void
    {
        // You get the exception object from the received event
        $exception = $event->getThrowable();
        $message = sprintf(
            'My Error says: %s with code: %s',
            $exception->getMessage(),
            $exception->getCode()
        );

        // Customize your response object to display the exception details
        $response = new Response();
        $response->setContent($message);

        // HttpExceptionInterface is a special type of exception that
        // holds status code and header details
        if ($exception instanceof HttpExceptionInterface)
        {
            $response->setStatusCode($exception->getStatusCode());
            $response->headers->replace($exception->getHeaders());
        }
        else
        {
            $response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
        }

        // sends the modified response object to the event
        $event->setResponse($response);
    }
}


Update 2

Tried suggestions in the comments:

  1. set validator to only return 400
  2. set validator to only return 500

Trying 1 and 2 conditions with malformed file (mimetype does not correspond extension):

Trying 1 and 2 conditions with normal file:


Update 3

Wrote a second EventListener and added EventSubscriber in place of first EventListener:

Did similar as exlained here:

Still can not get error form server showing in Uppy UI!

Upvotes: 0

Views: 571

Answers (1)

ping
ping

Reputation: 61

can use a ExceptionListener, checkout an example here https://symfony.com/doc/current/event_dispatcher.html#creating-an-event-listener

services.yaml

services:
    App\Listener\ExceptionListener:
        tags:
            - { name: kernel.event_listener, event: kernel.exception }

Upvotes: 0

Related Questions