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:

  1. <?xml version="1.0"?>
  2. <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
  4. <type name="Magento\Catalog\Model\Webapi\Product\Option\Type\File\Processor">
  5. <plugin name="AllowFileUploadPlugin"
  6. type="Vendor\ModuleName\Plugin\Catalog\Webaip\ProductOptionTypeFileProcessor\AroundProcessFileContent\AllowFileUploadPlugin"/>
  7. </type>
  8. </config>
<?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:

  1. <?php
  2. declare(strict_types=1);
  3. namespace Vendor\ModuleName\Plugin\Catalog\Webaip\ProductOptionTypeFileProcessor\AroundProcessFileContent;
  4. use Magento\Catalog\Model\Webapi\Product\Option\Type\File\Processor;
  5. use Magento\Framework\Api\Data\ImageContentInterface;
  6. use Magento\Framework\Api\Uploader;
  7. use Magento\Framework\App\Filesystem\DirectoryList;
  8. use Magento\Framework\Exception\FileSystemException;
  9. use Magento\Framework\Exception\InputException;
  10. use Magento\Framework\Filesystem;
  11. use Magento\Framework\Filesystem\Directory\WriteInterface;
  12. use Magento\Framework\Phrase;
  13. /**
  14. * Allow not only images to be uploaded
  15. *
  16. * Class AllowFileUploadPlugin
  17. */
  18. class AllowFileUploadPlugin
  19. {
  20. public const FILE_EXTENSIONS_TO_PROCESS = [
  21. 'pdf',
  22. 'docx'
  23. ];
  24. /**
  25. * @var string
  26. */
  27. protected $destinationFolder = 'custom_options/quote';
  28. /**
  29. * 4Mb file size limit
  30. */
  31. public const FILE_SIZE_LIMIT_BYTES = 4 * 1048576;
  32. /**
  33. * @var Filesystem
  34. */
  35. private $filesystem;
  36. /**
  37. * @var Uploader
  38. */
  39. private $uploader;
  40. /**
  41. * @var WriteInterface
  42. */
  43. private $mediaDirectory;
  44. /**
  45. * AllowFileUploadPlugin constructor.
  46. * @param Filesystem $filesystem
  47. * @param Uploader $uploader
  48. * @throws FileSystemException
  49. */
  50. public function __construct(
  51. Filesystem $filesystem,
  52. Uploader $uploader
  53. ) {
  54. $this->filesystem = $filesystem;
  55. $this->uploader = $uploader;
  56. $this->mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA);
  57. }
  58. /**
  59. * Core magento allows only images to be uploaded
  60. * Here we allow PDF and DOC files
  61. *
  62. * @param Processor $subject
  63. * @param callable $proceed
  64. * @param ImageContentInterface $fileContent
  65. * @return array
  66. * @throws InputException
  67. * @throws FileSystemException
  68. * @SuppressWarnings(PHPMD.UnusedFormalParameter)
  69. */
  70. public function aroundProcessFileContent(
  71. Processor $subject,
  72. callable $proceed,
  73. ImageContentInterface $fileContent
  74. ): array {
  75. $fileMatches = array_search(
  76. strtolower($fileContent->getType()),
  77. array_map('strtolower', self::FILE_EXTENSIONS_TO_PROCESS),
  78. true
  79. ) !== false;
  80. if (!$fileMatches) {
  81. return $proceed($fileContent);
  82. }
  83. $filePath = $this->saveFile($fileContent);
  84. $fileAbsolutePath = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA)->getAbsolutePath($filePath);
  85. $fileHash = md5($this->filesystem->getDirectoryRead(DirectoryList::MEDIA)->readFile($filePath));
  86. $imageSize = getimagesize($fileAbsolutePath);
  87. $result = [
  88. 'type' => $fileContent->getType(),
  89. 'title' => $fileContent->getName(),
  90. 'fullpath' => $fileAbsolutePath,
  91. 'quote_path' => $filePath,
  92. 'order_path' => $filePath,
  93. 'size' => filesize($fileAbsolutePath),
  94. 'width' => $imageSize ? $imageSize[0] : 0,
  95. 'height' => $imageSize ? $imageSize[1] : 0,
  96. 'secret_key' => substr($fileHash, 0, 20),
  97. ];
  98. return $result;
  99. }
  100. /**
  101. * Save file
  102. *
  103. * @param ImageContentInterface $fileContent
  104. * @return string
  105. * @throws InputException
  106. * @throws FileSystemException
  107. */
  108. private function saveFile(ImageContentInterface $fileContent): string
  109. {
  110. $this->validateSize($fileContent);
  111. $content = @base64_decode($fileContent->getBase64EncodedData(), true);
  112. $tmpDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::SYS_TMP);
  113. $fileName = $this->getFileName($fileContent);
  114. $tmpFileName = substr(md5((string)mt_rand()), 0, 7) . '.' . $fileName;
  115. $tmpDirectory->writeFile($tmpFileName, $content);
  116. $fileAttributes = [
  117. 'tmp_name' => $tmpDirectory->getAbsolutePath() . $tmpFileName,
  118. 'name' => $fileContent->getName()
  119. ];
  120. $this->uploader->processFileAttributes($fileAttributes);
  121. $this->uploader->setFilesDispersion(true);
  122. $this->uploader->setFilenamesCaseSensitivity(false);
  123. $this->uploader->setAllowRenameFiles(true);
  124. $this->uploader->save($this->mediaDirectory->getAbsolutePath($this->destinationFolder), $fileName);
  125. $filePath = $this->uploader->getUploadedFileName();
  126. return $this->destinationFolder . $filePath;
  127. }
  128. /**
  129. * Validate file size
  130. *
  131. * @param ImageContentInterface $fileContent
  132. * @throws InputException
  133. */
  134. private function validateSize(ImageContentInterface $fileContent): void
  135. {
  136. //convert base 64 size to real one
  137. $fileSizeInBytes = strlen($fileContent->getBase64EncodedData()) / 4 * 3;
  138. if ($fileSizeInBytes > self::FILE_SIZE_LIMIT_BYTES) {
  139. throw new InputException(new Phrase('File size exceeds the limit'));
  140. }
  141. }
  142. /**
  143. * Get file name
  144. *
  145. * @param $fileContent
  146. * @return mixed|string
  147. */
  148. private function getFileName($fileContent)
  149. {
  150. $fileName = $fileContent->getName();
  151. if (!pathinfo($fileName, PATHINFO_EXTENSION)) {
  152. $fileName .= '.' . $fileContent->getType();
  153. }
  154. return $fileName;
  155. }
  156. }
<?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.