Magento 2 customizable options – allow any file upload via Rest API

Magento 2 custom options allows you to add file upload option to product. So on frontend customer is able to attach a file to product when placing an order.

Recently I have discovered that it is not possible to upload file other than image via Rest API. In the code below I suggest a sample Plugin code that allows you to overcome this restriction.

First declare plugin in app/code/Vendor/ModuleName/etc/webapi_rest/di.xml:

<?xml version="1.0"?>

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Catalog\Model\Webapi\Product\Option\Type\File\Processor">
        <plugin name="AllowFileUploadPlugin"
                type="Vendor\ModuleName\Plugin\Catalog\Webaip\ProductOptionTypeFileProcessor\AroundProcessFileContent\AllowFileUploadPlugin"/>
    </type>
</config>

Plugin code can look like this:

<?php

declare(strict_types=1);

namespace Vendor\ModuleName\Plugin\Catalog\Webaip\ProductOptionTypeFileProcessor\AroundProcessFileContent;

use Magento\Catalog\Model\Webapi\Product\Option\Type\File\Processor;
use Magento\Framework\Api\Data\ImageContentInterface;
use Magento\Framework\Api\Uploader;
use Magento\Framework\App\Filesystem\DirectoryList;
use Magento\Framework\Exception\FileSystemException;
use Magento\Framework\Exception\InputException;
use Magento\Framework\Filesystem;
use Magento\Framework\Filesystem\Directory\WriteInterface;
use Magento\Framework\Phrase;

/**
 * Allow not only images to be uploaded
 *
 * Class AllowFileUploadPlugin
 */
class AllowFileUploadPlugin
{

    public const FILE_EXTENSIONS_TO_PROCESS = [
        'pdf',
        'docx'
    ];

    /**
     * @var string
     */
    protected $destinationFolder = 'custom_options/quote';

    /**
     * 4Mb file size limit
     */
    public const FILE_SIZE_LIMIT_BYTES = 4 * 1048576;

    /**
     * @var Filesystem
     */
    private $filesystem;

    /**
     * @var Uploader
     */
    private $uploader;

    /**
     * @var WriteInterface
     */
    private $mediaDirectory;

    /**
     * AllowFileUploadPlugin constructor.
     * @param Filesystem $filesystem
     * @param Uploader $uploader
     * @throws FileSystemException
     */
    public function __construct(
        Filesystem $filesystem,
        Uploader $uploader
    ) {
        $this->filesystem = $filesystem;
        $this->uploader = $uploader;
        $this->mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA);
    }

    /**
     * Core magento allows only images to be uploaded
     * Here we allow PDF and DOC files
     *
     * @param Processor $subject
     * @param callable $proceed
     * @param ImageContentInterface $fileContent
     * @return array
     * @throws InputException
     * @throws FileSystemException
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
     */
    public function aroundProcessFileContent(
        Processor $subject,
        callable $proceed,
        ImageContentInterface $fileContent
    ): array {
        $fileMatches = array_search(
            strtolower($fileContent->getType()),
            array_map('strtolower', self::FILE_EXTENSIONS_TO_PROCESS),
            true
        ) !== false;

        if (!$fileMatches) {
            return $proceed($fileContent);
        }

        $filePath = $this->saveFile($fileContent);

        $fileAbsolutePath = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA)->getAbsolutePath($filePath);
        $fileHash = md5($this->filesystem->getDirectoryRead(DirectoryList::MEDIA)->readFile($filePath));
        $imageSize = getimagesize($fileAbsolutePath);
        $result = [
            'type'       => $fileContent->getType(),
            'title'      => $fileContent->getName(),
            'fullpath'   => $fileAbsolutePath,
            'quote_path' => $filePath,
            'order_path' => $filePath,
            'size'       => filesize($fileAbsolutePath),
            'width'      => $imageSize ? $imageSize[0] : 0,
            'height'     => $imageSize ? $imageSize[1] : 0,
            'secret_key' => substr($fileHash, 0, 20),
        ];

        return $result;
    }

    /**
     * Save file
     *
     * @param ImageContentInterface $fileContent
     * @return string
     * @throws InputException
     * @throws FileSystemException
     */
    private function saveFile(ImageContentInterface $fileContent): string
    {
        $this->validateSize($fileContent);

        $content = @base64_decode($fileContent->getBase64EncodedData(), true);
        $tmpDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::SYS_TMP);
        $fileName = $this->getFileName($fileContent);
        $tmpFileName = substr(md5((string)mt_rand()), 0, 7) . '.' . $fileName;
        $tmpDirectory->writeFile($tmpFileName, $content);

        $fileAttributes = [
            'tmp_name' => $tmpDirectory->getAbsolutePath() . $tmpFileName,
            'name'     => $fileContent->getName()
        ];

        $this->uploader->processFileAttributes($fileAttributes);
        $this->uploader->setFilesDispersion(true);
        $this->uploader->setFilenamesCaseSensitivity(false);
        $this->uploader->setAllowRenameFiles(true);
        $this->uploader->save($this->mediaDirectory->getAbsolutePath($this->destinationFolder), $fileName);

        $filePath = $this->uploader->getUploadedFileName();

        return $this->destinationFolder . $filePath;
    }

    /**
     * Validate file size
     *
     * @param ImageContentInterface $fileContent
     * @throws InputException
     */
    private function validateSize(ImageContentInterface $fileContent): void
    {
        //convert base 64 size to real one
        $fileSizeInBytes = strlen($fileContent->getBase64EncodedData()) / 4 * 3;

        if ($fileSizeInBytes > self::FILE_SIZE_LIMIT_BYTES) {
            throw new InputException(new Phrase('File size exceeds the limit'));
        }
    }

    /**
     * Get file name
     *
     * @param $fileContent
     * @return mixed|string
     */
    private function getFileName($fileContent)
    {
        $fileName = $fileContent->getName();
        if (!pathinfo($fileName, PATHINFO_EXTENSION)) {
            $fileName .= '.' . $fileContent->getType();
        }

        return $fileName;
    }
}

Hope it is helpful.