1
votes

I am creating an API with API platform. One of the features is to be able to upload and download files from a React client developped independently from my API

1 - First try

I followed the docs to setup VichUploaderBundle which led me to the exact same configuration as the docs (https://api-platform.com/docs/core/file-upload/)

From this, I can get my images by sending a GET request to the contentURL attribute set by my subscriber, which has the following format : "localhost/media/{fileName}" . However, I get a "CORS Missing allow origin" from my app when doing this.

2 - Second try

I fixed this by :

  • removing the subscriber and the contentUrl attribute
  • writing an itemOperation on the get method to serve my files directly through the "media_objects/{id}" route :
<?php
// api/src/Controller/GetMediaObjectAction.php

namespace App\Controller;
use App\Entity\MediaObject;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use App\Repository\MediaObjectRepository;

final class GetMediaObjectAction
{
    private $mediaObjectRepository;

    public function __construct(MediaObjectRepository $mediaObjectRepository)
    {
        $this->mediaObjectRepository = $mediaObjectRepository;
    }

    public function __invoke(Request $request): BinaryFileResponse
    {
        $id = $request->attributes->get('id');
        $filePath = $this->mediaObjectRepository->findOneById($id)->getFilePath();
        $file = "media/" . $filePath;

        return new BinaryFileResponse($file);
    }
}

EDIT : Here is my implementation of the MediaObject entity as requested

<?php
// api/src/Entity/MediaObject.php
namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;
use App\Controller\CreateMediaObjectAction;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use Vich\UploaderBundle\Mapping\Annotation as Vich;

/**
 * @ORM\Entity
 * @ApiResource(
 *     iri="http://schema.org/MediaObject",
 *     normalizationContext={
 *         "groups"={"media_object_read"}
 *     },
 *     collectionOperations={
 *         "post"={
 *             "controller"=CreateMediaObjectAction::class,
 *             "deserialize"=false,
 *             "validation_groups"={"Default", "media_object_create"},
 *             "openapi_context"={
 *                 "requestBody"={
 *                     "content"={
 *                         "multipart/form-data"={
 *                             "schema"={
 *                                 "type"="object",
 *                                 "properties"={
 *                                     "file"={
 *                                         "type"="string",
 *                                         "format"="binary"
 *                                     }
 *                                 }
 *                             }
 *                         }
 *                     }
 *                 }
 *             }
 *         },
 *         "get"
 *     },
 *     itemOperations={
 *         "get"
 *     }
 * )
 * @Vich\Uploadable
 */
class MediaObject
{
    /**
     * @var int|null
     *
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue
     * @ORM\Id
     */
    protected $id;

    /**
     * @var string|null
     *
     * @ApiProperty(iri="http://schema.org/contentUrl")
     * @Groups({"media_object_read"})
     */
    public $contentUrl;

    /**
     * @var File|null
     *
     * @Assert\NotNull(groups={"media_object_create"})
     * @Vich\UploadableField(mapping="media_object",fileNameProperty="filePath")
     */
    public $file;

    /**
     * @var string|null
     *
     * @ORM\Column(nullable=true)
     */
    public $filePath;

    public function getId(): ?int
    {
        return $this->id;
    }
}

END OF EDIT

Now I don't have this CORS problem anymore since API-platform is directly serving the file when responding to my "media_objects/{id}" route.

However, this brought some questions :

  • Why did the CORS error pop in the first place ? I would guess it is because when performing a get request directly on the "public" folder, API-platform is not enforcing its CORS policy and not providing the required headers to the client
  • Is it a correct practice to serve the files this way ? The fact that the documentation introduces a subscriber to create a contentUrl makes me wonder...
  • Now that the server handles retrieving the file in the Action, does it make sense to have the files in the public folder ? Wouldn't it allow anyone to retrieve my files, and make enforcing security rules on them more difficult ?

Thank you in advance !

1

1 Answers

1
votes

Why did the CORS error pop in the first place?

Because API Platform adds the Access-Control-Allow-Origin header to the HTTP response (using Nelmio Cors Bundle) with the CORS_ALLOW_ORIGIN value defined in your .env file. This value typically includes only localhost and example.com by default. The requests send by your React client likely do not originate from either of these hosts, resulting in your browser stepping in and raising an error. More info here.

The Nelmio Cors Bundle configuration documentation explains how to deal with this error. Simplest approach is to set CORS_ALLOW_ORIGIN=* in your .env, and have your nelmio_cors.yaml configuration file include:

nelmio_cors:
    defaults:
        origin_regex: true
        allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']

The generic BinaryFileResponse instance returned by your custom controller doesn't include this header (bypassing all the CORS stuff), resulting in your browser being happy.

Is it a correct practice to serve the files this way?

I recommend sticking with the guidelines and best practices provided by any vendor documentation. This one included.

Does it make sense to have the files in the public folder ? Wouldn't it allow anyone to retrieve my files, and make enforcing security rules on them more difficult?

Nothing wrong with the backend exposing public media assets instead of database blobls. Web servers are very capable of restricting access to these resources if necessary, and so is PHP.