CssUrls.php 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Deploy\Package\Processor\PostProcessor;
  7. use Magento\Deploy\Console\DeployStaticOptions;
  8. use Magento\Deploy\Package\Package;
  9. use Magento\Deploy\Package\PackageFile;
  10. use Magento\Deploy\Package\Processor\ProcessorInterface;
  11. use Magento\Framework\App\Filesystem\DirectoryList;
  12. use Magento\Framework\Exception\NotFoundException;
  13. use Magento\Framework\Filesystem;
  14. use Magento\Framework\View\Url\CssResolver;
  15. use Magento\Framework\View\Asset\Minification;
  16. /**
  17. * Post-processor scans through all CSS files and correct misleading URLs
  18. *
  19. * Such URLs may pre-exist in CSS files, but can appear when file was copied from one of the ancestors,
  20. * so all relative URLs need to be adjusted
  21. */
  22. class CssUrls implements ProcessorInterface
  23. {
  24. /**
  25. * Static content directory writable interface
  26. *
  27. * @var Filesystem\Directory\WriteInterface
  28. */
  29. private $staticDir;
  30. /**
  31. * Helper class for static files minification related processes
  32. *
  33. * @var Minification
  34. */
  35. private $minification;
  36. /**
  37. * Deployment procedure options
  38. *
  39. * @var array
  40. */
  41. private $options = [];
  42. /**
  43. * CssUrls constructor
  44. *
  45. * @param Filesystem $filesystem
  46. * @param Minification $minification
  47. */
  48. public function __construct(Filesystem $filesystem, Minification $minification)
  49. {
  50. $this->staticDir = $filesystem->getDirectoryWrite(DirectoryList::STATIC_VIEW);
  51. $this->minification = $minification;
  52. }
  53. /**
  54. * @inheritdoc
  55. */
  56. public function process(Package $package, array $options)
  57. {
  58. $this->options = $options;
  59. if ($this->options[DeployStaticOptions::NO_CSS] === true) {
  60. return false;
  61. }
  62. $urlMap = [];
  63. /** @var PackageFile $file */
  64. foreach (array_keys($package->getMap()) as $fileId) {
  65. $filePath = str_replace(\Magento\Framework\View\Asset\Repository::FILE_ID_SEPARATOR, '/', $fileId);
  66. if (strtolower(pathinfo($fileId, PATHINFO_EXTENSION)) == 'css') {
  67. $urlMap = $this->parseCss(
  68. $urlMap,
  69. $filePath,
  70. $package->getPath(),
  71. $this->staticDir->readFile(
  72. $this->minification->addMinifiedSign($package->getPath() . '/' . $filePath)
  73. ),
  74. $package
  75. );
  76. }
  77. }
  78. $this->updateCssUrls($urlMap);
  79. return true;
  80. }
  81. /**
  82. * Collect all URLs
  83. *
  84. * @param array $urlMap
  85. * @param string $cssFilePath
  86. * @param string $packagePath
  87. * @param string $cssContent
  88. * @param Package $package
  89. * @return array
  90. * @throws NotFoundException
  91. */
  92. private function parseCss(array $urlMap, $cssFilePath, $packagePath, $cssContent, Package $package)
  93. {
  94. $cssFilePath = $this->minification->addMinifiedSign($cssFilePath);
  95. $cssFileBasePath = pathinfo($cssFilePath, PATHINFO_DIRNAME);
  96. $urls = $this->getCssUrls($cssContent);
  97. foreach ($urls as $url) {
  98. if ($this->isExternalUrl($url)) {
  99. $urlMap[$url][] = [
  100. 'filePath' => $this->minification->addMinifiedSign($packagePath . '/' . $cssFilePath),
  101. 'replace' => $this->getValidExternalUrl($url, $package)
  102. ];
  103. continue;
  104. }
  105. $filePath = $this->getNormalizedFilePath($packagePath . '/' . $cssFileBasePath . '/' . $url);
  106. if ($this->staticDir->isReadable($this->minification->addMinifiedSign($filePath))) {
  107. continue;
  108. }
  109. $lookupFileId = $this->getNormalizedFilePath($cssFileBasePath . '/' . $url);
  110. /** @var PackageFile $matchedFile */
  111. $matchedFile = $this->getFileFromParent($lookupFileId, $package);
  112. if ($matchedFile) {
  113. $urlMap[$url][] = [
  114. 'filePath' => $this->minification->addMinifiedSign($packagePath . '/' . $cssFilePath),
  115. 'replace' => '../../../../' // base path is always of four chunks size
  116. . str_repeat('../', count(explode('/', $cssFileBasePath)))
  117. . $this->minification->addMinifiedSign($matchedFile->getDeployedFilePath())
  118. ];
  119. }
  120. }
  121. return $urlMap;
  122. }
  123. /**
  124. * Replace relative URLs in CSS files
  125. *
  126. * @param array $urlMap
  127. * @return void
  128. */
  129. private function updateCssUrls(array $urlMap)
  130. {
  131. foreach ($urlMap as $ref => $targetFiles) {
  132. foreach ($targetFiles as $matchedFileData) {
  133. $filePath = $matchedFileData['filePath'];
  134. $oldCss = $this->staticDir->readFile($filePath);
  135. $newCss = str_replace($ref, $matchedFileData['replace'], $oldCss);
  136. if ($oldCss !== $newCss) {
  137. $this->staticDir->writeFile($filePath, $newCss);
  138. }
  139. }
  140. }
  141. }
  142. /**
  143. * Parse css and return all urls
  144. *
  145. * @param string $cssContent
  146. * @return array
  147. */
  148. private function getCssUrls($cssContent)
  149. {
  150. $urls = [];
  151. preg_match_all(CssResolver::REGEX_CSS_RELATIVE_URLS, $cssContent, $matches);
  152. if (!empty($matches[0]) && !empty($matches[1])) {
  153. $urls = array_combine($matches[0], $matches[1]);
  154. }
  155. return $urls;
  156. }
  157. /**
  158. * Remove “..” segments from URL
  159. *
  160. * @param string $url
  161. * @return string
  162. */
  163. private function getNormalizedFilePath($url)
  164. {
  165. $urlParts = explode('/', $url);
  166. $result = [];
  167. if (preg_match('/{{.*}}/', $url)) {
  168. foreach (array_reverse($urlParts) as $index => $part) {
  169. if (!preg_match('/^{{.*}}$/', $part)) {
  170. $result[] = $part;
  171. } else {
  172. break;
  173. }
  174. }
  175. return implode('/', array_reverse($result));
  176. }
  177. $prevIndex = 0;
  178. foreach ($urlParts as $index => $part) {
  179. if ($part == '..') {
  180. unset($urlParts[$index]);
  181. unset($urlParts[$prevIndex]);
  182. --$prevIndex;
  183. } else {
  184. $prevIndex = $index;
  185. }
  186. }
  187. return implode('/', $urlParts);
  188. }
  189. /**
  190. * Fulfil placeholders in external URL with appropriate area, theme and locale values
  191. *
  192. * @param string $url
  193. * @param Package $package
  194. * @return string
  195. */
  196. private function getValidExternalUrl($url, Package $package)
  197. {
  198. $url = $this->minification->removeMinifiedSign($url);
  199. $filePath = $this->getNormalizedFilePath($url);
  200. if (!$this->isFileExistsInPackage($filePath, $package)) {
  201. /** @var PackageFile $matchedFile */
  202. $matchedFile = $this->getFileFromParent($filePath, $package);
  203. $package = $matchedFile->getPackage();
  204. }
  205. return preg_replace(
  206. '/(?<=}})(.*)(?=\/{{)/',
  207. $package->getArea() . '/' . $package->getTheme(),
  208. $this->minification->addMinifiedSign($url)
  209. );
  210. }
  211. /**
  212. * Find file in ancestors by the same relative path
  213. *
  214. * @param string $fileName
  215. * @param Package $currentPackage
  216. * @return PackageFile|null
  217. */
  218. private function getFileFromParent($fileName, Package $currentPackage)
  219. {
  220. /** @var Package $package */
  221. foreach (array_reverse($currentPackage->getParentPackages()) as $package) {
  222. foreach ($package->getFiles() as $file) {
  223. if ($file->getDeployedFileName() === $fileName) {
  224. return $file;
  225. }
  226. }
  227. }
  228. return null;
  229. }
  230. /**
  231. * Check if URL has placeholders, used for referencing to resources with full URL
  232. *
  233. * @param string $url
  234. * @return bool
  235. */
  236. private function isExternalUrl($url)
  237. {
  238. return preg_match('/{{.*}}/', $url);
  239. }
  240. /**
  241. * Check if file of the same deployed path exists in package
  242. *
  243. * @param string $filePath
  244. * @param Package $package
  245. * @return bool
  246. */
  247. private function isFileExistsInPackage($filePath, Package $package)
  248. {
  249. /** @var PackageFile $file */
  250. foreach ($package->getFiles() as $file) {
  251. if ($file->getDeployedFileName() === $filePath) {
  252. return true;
  253. }
  254. }
  255. return false;
  256. }
  257. }