Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/code/Magento/Backend/Block/Media/Uploader.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ protected function _construct()
$this->getConfig()->setFilters(
[
'images' => [
'label' => __('Images (.gif, .jpg, .png)'),
'files' => ['*.gif', '*.jpg', '*.png'],
'label' => __('Images (.gif, .jpg, .png, .webp, .avif)'),
'files' => ['*.gif', '*.jpg', '*.png', '*.webp', '*.avif'],
],
'media' => [
'label' => __('Media (.avi, .flv, .swf)'),
Expand Down
2 changes: 1 addition & 1 deletion app/code/Magento/Backend/i18n/en_US.csv
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Views,Views
Revenue,Revenue
Tax,Tax
Shipping,Shipping
"Images (.gif, .jpg, .png)","Images (.gif, .jpg, .png)"
"Images (.gif, .jpg, .png, .webp, .avif)","Images (.gif, .jpg, .png, .webp, .avif)"
"Media (.avi, .flv, .swf)","Media (.avi, .flv, .swf)"
"All Files","All Files"
"Reset to Default","Reset to Default"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ define([
targetElement = this.element.find('.fileinput-button.form-buttons')[0],
uploadUrl = $(uploaderElement).attr('data-url'),
fileId = null,
allowedExt = ['jpeg', 'jpg', 'png', 'gif'],
allowedExt = ['jpeg', 'jpg', 'png', 'gif', 'webp', 'avif'],
allowedResize = false,
options = {
proudlyDisplayPoweredByUppy: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,8 @@ protected function _prepareLayout()
)->setFilters(
[
'images' => [
'label' => __('Images (.gif, .jpg, .png)'),
'files' => ['*.gif', '*.jpg', '*.jpeg', '*.png'],
'label' => __('Images (.gif, .jpg, .png, .webp, .avif)'),
'files' => ['*.gif', '*.jpg', '*.jpeg', '*.png', '*.webp', '*.avif'],
],
]
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ class Upload extends \Magento\Backend\App\Action implements HttpPostActionInterf
'jpg' => 'image/jpg',
'jpeg' => 'image/jpeg',
'gif' => 'image/gif',
'png' => 'image/png'
'png' => 'image/png',
'webp' => 'image/webp',
'avif' => 'image/avif'
];

/**
Expand Down Expand Up @@ -113,6 +115,8 @@ public function execute()
}
} catch (LocalizedException $e) {
$result = ['error' => $e->getMessage(), 'errorcode' => $e->getCode()];
} catch (\InvalidArgumentException $e) {
$result = ['error' => __($e->getMessage())->render(), 'errorcode' => 0];
} catch (\Throwable $e) {
$result = ['error' => 'Something went wrong while saving the file(s).', 'errorcode' => 0];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ class MimeTypeExtensionMap
'image/jpeg' => 'jpg',
'image/gif' => 'gif',
'image/png' => 'png',
'image/webp' => 'webp',
'image/avif' => 'avif',
];

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ public function addImage(

// phpcs:ignore Magento2.Functions.DiscouragedFunction
$pathinfo = pathinfo($file);
$imgExtensions = ['jpg', 'jpeg', 'gif', 'png'];
$imgExtensions = ['jpg', 'jpeg', 'gif', 'png', 'webp', 'avif'];
if (!isset($pathinfo['extension']) || !in_array(strtolower($pathinfo['extension']), $imgExtensions)) {
throw new LocalizedException(
__('The image type for the file is invalid. Enter the correct image type and try again.')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public function afterSave($object)
try {
/** @var $uploader \Magento\MediaStorage\Model\File\Uploader */
$uploader = $this->_fileUploaderFactory->create(['fileId' => $this->getAttribute()->getName()]);
$uploader->setAllowedExtensions(['jpg', 'jpeg', 'gif', 'png']);
$uploader->setAllowedExtensions(['jpg', 'jpeg', 'gif', 'png', 'webp', 'avif']);
$uploader->setAllowRenameFiles(true);
$uploader->setFilesDispersion(true);
} catch (\Exception $e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
/**
* Copyright 2026 Mage-OS
* All Rights Reserved.
*/
-->

<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd">
<!-- Advanced > Developer > Image Processing Settings > Image Adapter -->
<entity name="AdminImageAdapterDefault">
<!-- Default configuration -->
<data key="path">dev/image/default_adapter</data>
<data key="scope_id">0</data>
<data key="label">GD2</data>
<data key="value">GD2</data>
</entity>
<entity name="AdminImageAdapterImageMagick">
<data key="path">dev/image/default_adapter</data>
<data key="scope_id">0</data>
<data key="label">ImageMagick</data>
<data key="value">IMAGEMAGICK</data>
</entity>
</entities>
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
/**
* Copyright 2026 Mage-OS
* All Rights Reserved.
*/
-->
<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd">
<test name="AdminProductImageAvifUploadTest">
<annotations>
<features value="Catalog"/>
<stories value="Add/remove images and videos for all product types and category"/>
<title value="Admin should be able to add an AVIF image to a Simple Product"/>
<description value="AVIF upload goes through the same client side restrictions and server side
allow-lists as any other format. Runs against the ImageMagick adapter because GD only decodes AVIF when it
was built against libavif, which is still uncommon; the ImageMagick build needs the AVIF delegate."/>
<severity value="MAJOR"/>
<group value="Catalog"/>
</annotations>

<before>
<createData entity="_defaultCategory" stepKey="category"/>
<createData entity="_defaultProduct" stepKey="product">
<requiredEntity createDataKey="category"/>
</createData>
<magentoCLI
command="config:set {{AdminImageAdapterImageMagick.path}} {{AdminImageAdapterImageMagick.value}}"
stepKey="useImageMagick"/>
<magentoCLI command="cache:flush" stepKey="flushCacheBefore"/>
<actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/>
</before>

<after>
<magentoCLI command="config:set {{AdminImageAdapterDefault.path}} {{AdminImageAdapterDefault.value}}"
stepKey="restoreAdapter"/>
<magentoCLI command="cache:flush" stepKey="flushCacheAfter"/>
<deleteData createDataKey="category" stepKey="deleteCategory"/>
<deleteData createDataKey="product" stepKey="deleteProduct"/>
<actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/>
</after>

<actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductIndex"/>
<actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGrid"/>
<actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProductGridBySku">
<argument name="product" value="$$product$$"/>
</actionGroup>
<actionGroup ref="OpenProductForEditByClickingRowXColumnYInProductGridActionGroup"
stepKey="openProductForEdit"/>

<waitForElementVisible selector="{{AdminProductImagesSection.productImagesToggle}}"
stepKey="waitForImagesToggle"/>
<click selector="{{AdminProductImagesSection.productImagesToggle}}" stepKey="expandImages"/>
<waitForElementVisible selector="{{AdminProductImagesSection.imageUploadButton}}"
stepKey="waitForImagesSection"/>

<!-- *.avif is allowed -->
<attachFile selector="{{AdminProductImagesSection.imageFileUpload}}" userInput="avif.avif"
stepKey="attachAvif"/>
<waitForPageLoad stepKey="waitForUploadAvif"/>
<dontSeeElement selector="{{AdminProductMessagesSection.errorMessage}}" stepKey="dontSeeErrorAvif"/>
<waitForElementVisible selector="{{AdminProductImagesSection.imageFile('avif')}}" stepKey="seeAvifThumbnail"/>

<actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/>

<!-- The section collapses on reload, so it has to be reopened before the gallery renders again -->
<conditionalClick selector="{{AdminProductImagesSection.productImagesToggle}}"
dependentSelector="{{AdminProductImagesSection.imageFile('avif')}}" visible="false"
stepKey="expandImagesAfterSave"/>
<waitForElementVisible selector="{{AdminProductImagesSection.imageFile('avif')}}" stepKey="seeAvifAfterSave"/>
</test>
</tests>
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@
<waitForPageLoad stepKey="waitForUploadJpg"/>
<dontSeeElement selector="{{AdminProductMessagesSection.errorMessage}}" stepKey="dontSeeErrorJpg"/>

<!-- *.webp is allowed -->
<attachFile selector="{{AdminProductImagesSection.imageFileUpload}}" userInput="webp.webp"
stepKey="attachWebp"/>
<waitForPageLoad stepKey="waitForUploadWebp"/>
<dontSeeElement selector="{{AdminProductMessagesSection.errorMessage}}" stepKey="dontSeeErrorWebp"/>

<!-- *.png is allowed -->
<attachFile selector="{{AdminProductImagesSection.imageFileUpload}}" userInput="png.png" stepKey="attachPng"/>
<waitForPageLoad stepKey="waitForUploadPng"/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
<?php
/**
* Copyright 2026 Adobe
* All Rights Reserved.
*/
declare(strict_types=1);

namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\Gallery;

use Magento\Backend\App\Action\Context;
use Magento\Catalog\Controller\Adminhtml\Product\Gallery\Upload;
use Magento\Catalog\Model\Product\Media\Config;
use Magento\Framework\App\Filesystem\DirectoryList;
use Magento\Framework\Controller\Result\Raw;
use Magento\Framework\Controller\Result\RawFactory;
use Magento\Framework\Event\ManagerInterface;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Filesystem;
use Magento\Framework\Filesystem\Directory\ReadInterface;
use Magento\Framework\Image\Adapter\AdapterInterface;
use Magento\Framework\Image\AdapterFactory;
use Magento\Framework\ObjectManagerInterface;
use Magento\Framework\Phrase;
use Magento\MediaStorage\Model\File\Uploader;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

/**
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class UploadTest extends TestCase
{
private const BASE_TMP_MEDIA_PATH = 'catalog/product/tmp';

private const ABSOLUTE_TMP_MEDIA_PATH = '/var/www/html/pub/media/catalog/product/tmp';

/** @var Upload */
private $controller;

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

/** @var Config|MockObject */
private $productMediaConfig;

/** @var Raw|MockObject */
private $response;

/** @var string|null */
private $responseContents;

/**
* @inheritdoc
*/
protected function setUp(): void
{
$this->uploader = $this->getMockBuilder(Uploader::class)
->disableOriginalConstructor()
->getMock();

$objectManager = $this->createMock(ObjectManagerInterface::class);
$objectManager->method('create')
->with(Uploader::class, ['fileId' => 'image'])
->willReturn($this->uploader);

$context = $this->getMockBuilder(Context::class)
->disableOriginalConstructor()
->getMock();
$context->method('getObjectManager')->willReturn($objectManager);
$context->method('getEventManager')->willReturn($this->createMock(ManagerInterface::class));

$this->response = $this->getMockBuilder(Raw::class)
->disableOriginalConstructor()
->getMock();
$this->response->method('setHeader')->willReturnSelf();
$this->response->method('setContents')
->willReturnCallback(function ($contents) {
$this->responseContents = $contents;
return $this->response;
});

$resultRawFactory = $this->getMockBuilder(RawFactory::class)
->disableOriginalConstructor()
->onlyMethods(['create'])
->getMock();
$resultRawFactory->method('create')->willReturn($this->response);

$adapterFactory = $this->getMockBuilder(AdapterFactory::class)
->disableOriginalConstructor()
->getMock();
$adapterFactory->method('create')->willReturn($this->createMock(AdapterInterface::class));

$directory = $this->createMock(ReadInterface::class);
$directory->method('getAbsolutePath')
->with(self::BASE_TMP_MEDIA_PATH)
->willReturn(self::ABSOLUTE_TMP_MEDIA_PATH);

$filesystem = $this->getMockBuilder(Filesystem::class)
->disableOriginalConstructor()
->getMock();
$filesystem->method('getDirectoryRead')
->with(DirectoryList::MEDIA)
->willReturn($directory);

$this->productMediaConfig = $this->getMockBuilder(Config::class)
->disableOriginalConstructor()
->getMock();
$this->productMediaConfig->method('getBaseTmpMediaPath')->willReturn(self::BASE_TMP_MEDIA_PATH);

$this->controller = new Upload(
$context,
$resultRawFactory,
$adapterFactory,
$filesystem,
$this->productMediaConfig
);
}

public function testExecuteAcceptsWebpAndAvifExtensions(): void
{
$this->uploader->expects($this->once())
->method('setAllowedExtensions')
->with(['jpg', 'jpeg', 'gif', 'png', 'webp', 'avif']);
$this->uploader->method('save')
->with(self::ABSOLUTE_TMP_MEDIA_PATH)
->willReturn([
'file' => '/m/a/magento_image.webp',
'tmp_name' => '/tmp/magento_image.webp',
'path' => self::ABSOLUTE_TMP_MEDIA_PATH,
]);
$this->productMediaConfig->method('getTmpMediaUrl')
->with('/m/a/magento_image.webp')
->willReturn('http://localhost/media/tmp/catalog/product/m/a/magento_image.webp');

$result = $this->executeAndDecode();

$this->assertSame('/m/a/magento_image.webp.tmp', $result['file']);
$this->assertSame(
'http://localhost/media/tmp/catalog/product/m/a/magento_image.webp',
$result['url']
);
$this->assertArrayNotHasKey('error', $result);
$this->assertArrayNotHasKey('tmp_name', $result);
$this->assertArrayNotHasKey('path', $result);
}

public function testExecuteSurfacesImageAdapterMessage(): void
{
$this->uploader->method('save')
->willThrowException(new \InvalidArgumentException('Wrong file size.'));

$result = $this->executeAndDecode();

$this->assertSame('Wrong file size.', $result['error']);
$this->assertSame(0, $result['errorcode']);
}

public function testExecuteKeepsLocalizedExceptionMessageAndCode(): void
{
$this->uploader->method('save')
->willThrowException(new LocalizedException(new Phrase('File validation failed.'), null, 42));

$result = $this->executeAndDecode();

$this->assertSame('File validation failed.', $result['error']);
$this->assertSame(42, $result['errorcode']);
}

public function testExecuteHidesUnexpectedFailures(): void
{
$this->uploader->method('save')
->willThrowException(new \RuntimeException('Connection to the storage backend was lost'));

$result = $this->executeAndDecode();

$this->assertSame('Something went wrong while saving the file(s).', $result['error']);
$this->assertSame(0, $result['errorcode']);
}

/**
* @return array
*/
private function executeAndDecode(): array
{
$this->assertSame($this->response, $this->controller->execute());

return json_decode((string)$this->responseContents, true);
}
}
Loading
Loading