diff --git a/app/code/Magento/Email/Model/Template/Css/Processor.php b/app/code/Magento/Email/Model/Template/Css/Processor.php new file mode 100644 index 0000000000000000000000000000000000000000..ae7d083750863d2d7dbd5869341bbee772544421 --- /dev/null +++ b/app/code/Magento/Email/Model/Template/Css/Processor.php @@ -0,0 +1,67 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Email\Model\Template\Css; + +use Magento\Framework\View\Asset\NotationResolver\Variable; +use Magento\Framework\View\Asset\Repository; + +class Processor +{ + /** + * @var Repository + */ + private $assetRepository; + + /** + * @param Repository $assetRepository + */ + public function __construct(Repository $assetRepository) + { + $this->assetRepository = $assetRepository; + } + + /** + * Process css placeholders + * + * @param string $css + * @return string + */ + public function process($css) + { + $matches = []; + if (preg_match_all(Variable::VAR_REGEX, $css, $matches, PREG_SET_ORDER)) { + $replacements = []; + foreach ($matches as $match) { + if (!isset($replacements[$match[0]])) { + $replacements[$match[0]] = $this->getPlaceholderValue($match[1]); + } + } + $css = str_replace(array_keys($replacements), $replacements, $css); + } + return $css; + } + + /** + * Retrieve placeholder value + * + * @param string $placeholder + * @return string + */ + private function getPlaceholderValue($placeholder) + { + /** @var \Magento\Framework\View\Asset\File\FallbackContext $context */ + $context = $this->assetRepository->getStaticViewFileContext(); + + switch ($placeholder) { + case 'base_url_path': + return $context->getBaseUrl(); + case 'locale': + return $context->getLocale(); + default: + return ''; + } + } +} diff --git a/app/code/Magento/Email/Model/Template/Filter.php b/app/code/Magento/Email/Model/Template/Filter.php index 2bb9fc742f258090d1cda46b538053e00fb896e8..d9409d62f159cfd7c2ab1e6c32aae6d07a33b8fc 100644 --- a/app/code/Magento/Email/Model/Template/Filter.php +++ b/app/code/Magento/Email/Model/Template/Filter.php @@ -5,6 +5,9 @@ */ namespace Magento\Email\Model\Template; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\ReadInterface; use Magento\Framework\View\Asset\ContentProcessorException; use Magento\Framework\View\Asset\ContentProcessorInterface; @@ -153,6 +156,16 @@ class Filter extends \Magento\Framework\Filter\Template */ protected $configVariables; + /** + * @var \Magento\Email\Model\Template\Css\Processor + */ + private $cssProcessor; + + /** + * @var ReadInterface + */ + private $pubDirectory; + /** * @param \Magento\Framework\Stdlib\StringUtils $string * @param \Psr\Log\LoggerInterface $logger @@ -203,6 +216,31 @@ class Filter extends \Magento\Framework\Filter\Template parent::__construct($string, $variables); } + /** + * @deprecated + * @return Css\Processor + */ + private function getCssProcessor() + { + if (!$this->cssProcessor) { + $this->cssProcessor = ObjectManager::getInstance()->get(Css\Processor::class); + } + return $this->cssProcessor; + } + + /** + * @deprecated + * @param string $dirType + * @return ReadInterface + */ + private function getPubDirectory($dirType) + { + if (!$this->pubDirectory) { + $this->pubDirectory = ObjectManager::getInstance()->get(Filesystem::class)->getDirectoryRead($dirType); + } + return $this->pubDirectory; + } + /** * Set use absolute links flag * @@ -788,7 +826,9 @@ class Filter extends \Magento\Framework\Filter\Template return '/* ' . __('"file" parameter must be specified') . ' */'; } - $css = $this->getCssFilesContent([$params['file']]); + $css = $this->getCssProcessor()->process( + $this->getCssFilesContent([$params['file']]) + ); if (strpos($css, ContentProcessorInterface::ERROR_MESSAGE_PREFIX) !== false) { // Return compilation error wrapped in CSS comment @@ -889,7 +929,12 @@ class Filter extends \Magento\Framework\Filter\Template try { foreach ($files as $file) { $asset = $this->_assetRepo->createAsset($file, $designParams); - $css .= $asset->getContent(); + $pubDirectory = $this->getPubDirectory($asset->getContext()->getBaseDirType()); + if ($pubDirectory->isExist($asset->getPath())) { + $css .= $pubDirectory->readFile($asset->getPath()); + } else { + $css .= $asset->getContent(); + } } } catch (ContentProcessorException $exception) { $css = $exception->getMessage(); @@ -914,6 +959,8 @@ class Filter extends \Magento\Framework\Filter\Template $cssToInline = $this->getCssFilesContent( $this->getInlineCssFiles() ); + $cssToInline = $this->getCssProcessor()->process($cssToInline); + // Only run Emogrify if HTML and CSS contain content if ($html && $cssToInline) { try { diff --git a/app/code/Magento/Email/Test/Unit/Model/Template/Css/ProcessorTest.php b/app/code/Magento/Email/Test/Unit/Model/Template/Css/ProcessorTest.php new file mode 100644 index 0000000000000000000000000000000000000000..fac9ba0f1b5aa902a22f08141e5f0c824a1a1382 --- /dev/null +++ b/app/code/Magento/Email/Test/Unit/Model/Template/Css/ProcessorTest.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Email\Test\Unit\Model\Template\Css; + +use Magento\Email\Model\Template\Css\Processor; +use Magento\Framework\View\Asset\File\FallbackContext; +use Magento\Framework\View\Asset\Repository; + +class ProcessorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var Processor + */ + protected $processor; + + /** + * @var Repository|\PHPUnit_Framework_MockObject_MockObject + */ + protected $assetRepository; + + /** + * @var FallbackContext|\PHPUnit_Framework_MockObject_MockObject + */ + protected $fallbackContext; + + public function setUp() + { + $this->assetRepository = $this->getMockBuilder(Repository::class) + ->disableOriginalConstructor() + ->getMock(); + $this->fallbackContext = $this->getMockBuilder(FallbackContext::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->processor = new Processor($this->assetRepository); + } + + public function testProcess() + { + $url = 'http://magento.local/pub/static/'; + $locale = 'en_US'; + $css = '@import url("{{base_url_path}}frontend/_view/{{locale}}/css/email.css");'; + $expectedCss = '@import url("' . $url . 'frontend/_view/' . $locale . '/css/email.css");'; + + $this->assetRepository->expects($this->exactly(2)) + ->method('getStaticViewFileContext') + ->willReturn($this->fallbackContext); + $this->fallbackContext->expects($this->once()) + ->method('getBaseUrl') + ->willReturn($url); + $this->fallbackContext->expects($this->once()) + ->method('getLocale') + ->willReturn($locale); + $this->assertEquals($expectedCss, $this->processor->process($css)); + } +} diff --git a/app/code/Magento/Email/Test/Unit/Model/Template/FilterTest.php b/app/code/Magento/Email/Test/Unit/Model/Template/FilterTest.php index 84e87e9e8154156b7323e1cbfd5d6d11ebdaab39..bcfd95d897d298faf58543b08ad3385e71f66476 100644 --- a/app/code/Magento/Email/Test/Unit/Model/Template/FilterTest.php +++ b/app/code/Magento/Email/Test/Unit/Model/Template/FilterTest.php @@ -5,6 +5,13 @@ */ namespace Magento\Email\Test\Unit\Model\Template; +use Magento\Email\Model\Template\Css\Processor; +use Magento\Email\Model\Template\Filter; +use Magento\Framework\App\Area; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem\Directory\ReadInterface; +use Magento\Framework\View\Asset\File\FallbackContext; + /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -94,7 +101,6 @@ class FilterTest extends \PHPUnit_Framework_TestCase $this->escaper = $this->getMockBuilder(\Magento\Framework\Escaper::class) ->disableOriginalConstructor() - ->enableProxyingToOriginalMethods() ->getMock(); $this->assetRepo = $this->getMockBuilder(\Magento\Framework\View\Asset\Repository::class) @@ -138,7 +144,7 @@ class FilterTest extends \PHPUnit_Framework_TestCase /** * @param array|null $mockedMethods Methods to mock - * @return \Magento\Email\Model\Template\Filter|\PHPUnit_Framework_MockObject_MockObject + * @return Filter|\PHPUnit_Framework_MockObject_MockObject */ protected function getModel($mockedMethods = null) { @@ -252,13 +258,23 @@ class FilterTest extends \PHPUnit_Framework_TestCase public function testApplyInlineCss($html, $css, $expectedResults) { $filter = $this->getModel(['getCssFilesContent']); + $cssProcessor = $this->getMockBuilder(Processor::class) + ->disableOriginalConstructor() + ->getMock(); + $reflectionClass = new \ReflectionClass(Filter::class); + $reflectionProperty = $reflectionClass->getProperty('cssProcessor'); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($filter, $cssProcessor); + $cssProcessor->expects($this->any()) + ->method('process') + ->willReturnArgument(0); $filter->expects($this->exactly(count($expectedResults))) ->method('getCssFilesContent') ->will($this->returnValue($css)); $designParams = [ - 'area' => \Magento\Framework\App\Area::AREA_FRONTEND, + 'area' => Area::AREA_FRONTEND, 'theme' => 'themeId', 'locale' => 'localeId', ]; @@ -269,6 +285,60 @@ class FilterTest extends \PHPUnit_Framework_TestCase } } + public function testGetCssFilesContent() + { + $file = 'css/email.css'; + $path = Area::AREA_FRONTEND . '/themeId/localeId'; + $css = 'p{color:black}'; + $designParams = [ + 'area' => Area::AREA_FRONTEND, + 'theme' => 'themeId', + 'locale' => 'localeId', + ]; + $filter = $this->getModel(); + + $asset = $this->getMockBuilder(\Magento\Framework\View\Asset\File::class) + ->disableOriginalConstructor() + ->getMock(); + + $fallbackContext = $this->getMockBuilder(FallbackContext::class) + ->disableOriginalConstructor() + ->getMock(); + $fallbackContext->expects($this->once()) + ->method('getBaseDirType') + ->willReturn(DirectoryList::STATIC_VIEW); + $asset->expects($this->atLeastOnce()) + ->method('getContext') + ->willReturn($fallbackContext); + + $asset->expects($this->atLeastOnce()) + ->method('getPath') + ->willReturn($path . DIRECTORY_SEPARATOR . $file); + $this->assetRepo->expects($this->once()) + ->method('createAsset') + ->with($file, $designParams) + ->willReturn($asset); + + $pubDirectory = $this->getMockBuilder(ReadInterface::class) + ->getMockForAbstractClass(); + $reflectionClass = new \ReflectionClass(Filter::class); + $reflectionProperty = $reflectionClass->getProperty('pubDirectory'); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($filter, $pubDirectory); + $pubDirectory->expects($this->once()) + ->method('isExist') + ->with($path . DIRECTORY_SEPARATOR . $file) + ->willReturn(true); + $pubDirectory->expects($this->once()) + ->method('readFile') + ->with($path . DIRECTORY_SEPARATOR . $file) + ->willReturn($css); + + $filter->setDesignParams($designParams); + + $this->assertEquals($css, $filter->getCssFilesContent([$file])); + } + /** * @return array */ @@ -301,7 +371,19 @@ class FilterTest extends \PHPUnit_Framework_TestCase */ public function testApplyInlineCssThrowsExceptionWhenDesignParamsNotSet() { - $this->getModel()->applyInlineCss('test'); + $filter = $this->getModel(); + $cssProcessor = $this->getMockBuilder(Processor::class) + ->disableOriginalConstructor() + ->getMock(); + $reflectionClass = new \ReflectionClass(Filter::class); + $reflectionProperty = $reflectionClass->getProperty('cssProcessor'); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($filter, $cssProcessor); + $cssProcessor->expects($this->any()) + ->method('process') + ->willReturnArgument(0); + + $filter->applyInlineCss('test'); } /** @@ -348,7 +430,10 @@ class FilterTest extends \PHPUnit_Framework_TestCase $construction = ["{{config path={$path}}}", 'config', " path={$path}"]; $scopeConfigValue = 'value'; - $storeMock = $this->getMock(\Magento\Store\Api\Data\StoreInterface::class, [], [], '', false); + $storeMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->storeManager->expects($this->once())->method('getStore')->willReturn($storeMock); $storeMock->expects($this->once())->method('getId')->willReturn(1); @@ -369,7 +454,9 @@ class FilterTest extends \PHPUnit_Framework_TestCase $construction = ["{{config path={$path}}}", 'config', " path={$path}"]; $scopeConfigValue = ''; - $storeMock = $this->getMock(\Magento\Store\Api\Data\StoreInterface::class, [], [], '', false); + $storeMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class) + ->disableOriginalConstructor() + ->getMock(); $this->storeManager->expects($this->once())->method('getStore')->willReturn($storeMock); $storeMock->expects($this->once())->method('getId')->willReturn(1); diff --git a/lib/internal/Magento/Framework/View/Asset/NotationResolver/Variable.php b/lib/internal/Magento/Framework/View/Asset/NotationResolver/Variable.php index 59108ea0f5c427acfe367344fbfe7b6f9dcdcfaf..4e6bba487a1b7388f49483e43049892660836b14 100644 --- a/lib/internal/Magento/Framework/View/Asset/NotationResolver/Variable.php +++ b/lib/internal/Magento/Framework/View/Asset/NotationResolver/Variable.php @@ -58,18 +58,21 @@ class Variable } /** - * Retrieves the value of a given placeholder + * Process placeholder * * @param string $placeholder * @return string */ public function getPlaceholderValue($placeholder) { + /** @var \Magento\Framework\View\Asset\File\FallbackContext $context */ $context = $this->assetRepo->getStaticViewFileContext(); switch ($placeholder) { case self::VAR_BASE_URL_PATH: - return $context->getBaseUrl() . $context->getPath(); + return '{{' . self::VAR_BASE_URL_PATH . '}}' . $context->getAreaCode() . + ($context->getThemePath() ? '/' . $context->getThemePath() . '/' : '') . + '{{locale}}'; default: return ''; } diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Asset/NotationResolver/VariableTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Asset/NotationResolver/VariableTest.php index 77b81faf359aa316efb6415d3f2f506732d70169..b4bda4a93dfd51140e6cde6f6bf8e7fab5119b5a 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Asset/NotationResolver/VariableTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Asset/NotationResolver/VariableTest.php @@ -6,43 +6,51 @@ namespace Magento\Framework\View\Test\Unit\Asset\NotationResolver; -use Magento\Framework\App\Filesystem\DirectoryList; -use Magento\Framework\View\Asset\NotationResolver; +use Magento\Framework\UrlInterface; +use Magento\Framework\View\Asset\File\FallbackContext; +use Magento\Framework\View\Asset\NotationResolver\Variable; +use Magento\Framework\View\Asset\Repository; class VariableTest extends \PHPUnit_Framework_TestCase { /** - * @var \Magento\Framework\View\Asset\File\Context|\PHPUnit_Framework_MockObject_MockObject + * @var FallbackContext|\PHPUnit_Framework_MockObject_MockObject */ private $context; /** - * @var \Magento\Framework\View\Asset\Repository|\PHPUnit_Framework_MockObject_MockObject + * @var Repository|\PHPUnit_Framework_MockObject_MockObject */ private $assetRepo; /** - * @var \Magento\Framework\View\Asset\NotationResolver\Variable + * @var Variable */ private $object; protected function setUp() { - $baseUrl = 'http://example.com/pub/static/'; - $path = 'frontend/Magento/blank/en_US'; + $area = 'frontend'; + $themePath = 'Magento/blank'; - $this->context = $this->getMock( - \Magento\Framework\View\Asset\File\Context::class, - null, - [$baseUrl, DirectoryList::STATIC_VIEW, $path] - ); + $this->context = $this->getMockBuilder(FallbackContext::class) + ->disableOriginalConstructor() + ->getMock(); + $this->context->expects($this->once()) + ->method('getAreaCode') + ->willReturn($area); + $this->context->expects($this->exactly(2)) + ->method('getThemePath') + ->willReturn($themePath); - $this->assetRepo = $this->getMock(\Magento\Framework\View\Asset\Repository::class, [], [], '', false); + $this->assetRepo = $this->getMockBuilder(Repository::class) + ->disableOriginalConstructor() + ->getMock(); $this->assetRepo->expects($this->any()) ->method('getStaticViewFileContext') ->will($this->returnValue($this->context)); - $this->object = new \Magento\Framework\View\Asset\NotationResolver\Variable($this->assetRepo); + $this->object = new Variable($this->assetRepo); } /** @@ -61,7 +69,7 @@ class VariableTest extends \PHPUnit_Framework_TestCase public function convertVariableNotationDataProvider() { return [ - ['{{base_url_path}}/file.ext', 'http://example.com/pub/static/frontend/Magento/blank/en_US/file.ext'], + ['{{base_url_path}}/file.ext', '{{base_url_path}}frontend/Magento/blank/{{locale}}/file.ext'], ]; } }