0

Why are $_Files and/or HTTPFoundation\FileBag invalid in custom module Controller?

I have noticed a very strange issue with accessing an individual file from the $_FILES object or the Symfony derived \Symfony\Component\HTTPFoundation\FileBag from within a custom module controller.

The exact location of the issue seems to be in Symfony\Component\HttpKernel\HttpKernel -> handleRaw

Specifically, the file exists prior dispatching the KernelEvent::REQUEST but it does not after.

// request
$event = new GetResponseEvent($this, $request, $type);

// THE FILE EXISTS HERE
// file_exists( $request->files->get(0)->getRealPath()) == TRUE

$this->dispatcher->dispatch(KernelEvents::REQUEST, $event);

// THE FILE DOES NOT EXISTS HERE
// file_exists( $request->files->get(0)->getRealPath()) == FALSE

Here is the entire method for reference also including some code to move the file, which works as expected as long as it is before the event dispatch.

  /**
 * Handles a request to convert it to a response.
 *
 * Exceptions are not caught.
 *
 * @param Request $request A Request instance
 * @param int     $type    The type of the request (one of HttpKernelInterface::MASTER_REQUEST or HttpKernelInterface::SUB_REQUEST)
 *
 * @return Response A Response instance
 *
 * @throws \LogicException       If one of the listener does not behave as expected
 * @throws NotFoundHttpException When controller cannot be found
 */
private function handleRaw(Request $request, $type = self::MASTER_REQUEST)
{
    $this->requestStack->push($request);


    $directory = \Drupal::service('file_system')->realpath(file_default_scheme() . "://");

    /* @var \Symfony\Component\HttpFoundation\File\UploadedFile $uploadedFile*/
    $uploadedFile = $request->files->get(0);

    if (is_uploaded_file($uploadedFile->getRealPath())){
      $success = $uploadedFile->move($directory);
    }



  // request
    $event = new GetResponseEvent($this, $request, $type);
    $this->dispatcher->dispatch(KernelEvents::REQUEST, $event);

    if ($event->hasResponse()) {
        return $this->filterResponse($event->getResponse(), $request, $type);
    }

    // load controller
    if (false === $controller = $this->resolver->getController($request)) {
        throw new NotFoundHttpException(sprintf('Unable to find the controller for path "%s". The route is wrongly configured.', $request->getPathInfo()));
    }

    $event = new FilterControllerEvent($this, $controller, $request, $type);
    $this->dispatcher->dispatch(KernelEvents::CONTROLLER, $event);
    $controller = $event->getController();

    // controller arguments
    $arguments = $this->argumentResolver->getArguments($request, $controller);

    $event = new FilterControllerArgumentsEvent($this, $controller, $arguments, $request, $type);
    $this->dispatcher->dispatch(KernelEvents::CONTROLLER_ARGUMENTS, $event);
    $controller = $event->getController();
    $arguments = $event->getArguments();

    // call controller
    $response = call_user_func_array($controller, $arguments);

    // view
    if (!$response instanceof Response) {
        $event = new GetResponseForControllerResultEvent($this, $request, $type, $response);
        $this->dispatcher->dispatch(KernelEvents::VIEW, $event);

        if ($event->hasResponse()) {
            $response = $event->getResponse();
        }

        if (!$response instanceof Response) {
            $msg = sprintf('The controller must return a response (%s given).', $this->varToString($response));

            // the user may have forgotten to return something
            if (null === $response) {
                $msg .= ' Did you forget to add a return statement somewhere in your controller?';
            }
            throw new \LogicException($msg);
        }
    }

    return $this->filterResponse($response, $request, $type);
}

I am trying to write a custom GraphQL module mutation plugin that handles file uploads and unless I can get access to the file object from the POST request, I don't think this will be possible.

Any thoughts are greatly appreciated.

I believe this may be a core issue or an upstream issue with Symfony or at the very least a side effect of the assumptions of how new requests are created.

https://www.drupal.org/project/drupal/issues/2934486#comment-12406531

1 Answer 1

0

Update: this has been addressed upstream here https://github.com/symfony/psr-http-message-bridge/issues/39#issuecomment-558306949

There is an issue when a Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory request is created from anywhere. If there are any POST $_FILES (File object) on that request, they are renamed and moved to the tmp directory with a new name. This new name is not propagated on the original request.

During this process of creating the new psr7 request, any temporary files are moved and renamed in the temporary folder.

The tmp_name values starts as something like: /private/var/tmp/phpEHH19a and then on line 102 of the HTTPFoundationFactory, the file is moved and renamed to something like:

symfony5a4e551619af73.65717941BYwCkd

This renaming flow happens like this

HttpFoundationFactory->createRequest() -> getFiles --> createUploadFiles and this line 102 of HTTPFoundationFactory

$psrUploadedFile->moveTo($temporaryPath); causes the file to be renamed.

This type of request can happen in a number of places, but definitely in a decoupled app, especially during authentication. For example with the simple_oauth module when updating the tokens. This new request is never passed along and the original request still retains the original tmp_name file paths.

Once you finally get access to the request object in a module controller via \Symfony\Component\HttpFoundation\Request

and check all the files via: $request->files->all();

The Symfony\Component\HttpFoundation\File\UploadedFile Object contains the original file path.

This is a major problem that needs to be overcome in order to handle multipart/form-data with decoupled javascript file uploads. I noticed the issue when working on a custom graphql module to handle file uploads.

Here is the path you can use to trace through the issue: The SimpleOauthAuthenticationProvider calls validateAuthenticatedRequest($request) which creates a new psr7 request (see above). During this process, the files are moved and the new request does not include the updated request files object.

I think this is a bug upstream with Symfony or it's a bug with how files are handled with sub requests or maybe it's a bug with how simple_oauth handles getting the new tokens. I don't think it's a simple_oauth bug though because it's just making a request and there is a side effect of the temporary files being moved.

See the screenshot below. The $request variable is created and the tokens are reset. As a result of the new request, the temporary files are moved and renamed. The screenshot shows the new spl filepaths but this request dies in this method and the files do not get updated on the original request object. If you look at the watched variables on the right you can see that the file at the original tmp_name path no longer exists.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.