StaticFilesTest.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Test\Integrity;
  7. use Magento\Framework\App\Filesystem\DirectoryList;
  8. /**
  9. * An integrity test that searches for references to static files and asserts that they are resolved via fallback
  10. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  11. */
  12. class StaticFilesTest extends \PHPUnit\Framework\TestCase
  13. {
  14. /**
  15. * @var \Magento\Framework\View\Design\FileResolution\Fallback\StaticFile
  16. */
  17. private $fallback;
  18. /**
  19. * @var \Magento\Framework\View\Design\FileResolution\Fallback\Resolver\Simple
  20. */
  21. private $explicitFallback;
  22. /**
  23. * @var \Magento\Framework\View\Design\Theme\FlyweightFactory
  24. */
  25. private $themeRepo;
  26. /**
  27. * @var \Magento\Framework\View\DesignInterface
  28. */
  29. private $design;
  30. /**
  31. * @var \Magento\Framework\View\Design\ThemeInterface
  32. */
  33. private $baseTheme;
  34. /**
  35. * @var \Magento\Framework\View\Design\FileResolution\Fallback\Resolver\Alternative
  36. */
  37. private $alternativeResolver;
  38. /**
  39. * Factory for simple rule
  40. *
  41. * @var \Magento\Framework\View\Design\Fallback\Rule\SimpleFactory
  42. */
  43. private $simpleFactory;
  44. /**
  45. * @var \Magento\Framework\Filesystem
  46. */
  47. private $filesystem;
  48. protected function setUp()
  49. {
  50. $om = \Magento\TestFramework\Helper\Bootstrap::getObjectmanager();
  51. $this->fallback = $om->get(\Magento\Framework\View\Design\FileResolution\Fallback\StaticFile::class);
  52. $this->explicitFallback = $om->get(
  53. \Magento\Framework\View\Design\FileResolution\Fallback\Resolver\Simple::class
  54. );
  55. $this->themeRepo = $om->get(\Magento\Framework\View\Design\Theme\FlyweightFactory::class);
  56. $this->design = $om->get(\Magento\Framework\View\DesignInterface::class);
  57. $this->baseTheme = $om->get(\Magento\Framework\View\Design\ThemeInterface::class);
  58. $this->alternativeResolver = $om->get(
  59. \Magento\Framework\View\Design\FileResolution\Fallback\Resolver\Alternative::class
  60. );
  61. $this->simpleFactory = $om->get(\Magento\Framework\View\Design\Fallback\Rule\SimpleFactory::class);
  62. $this->filesystem = $om->get(\Magento\Framework\Filesystem::class);
  63. }
  64. /**
  65. * Scan references to files from other static files and assert they are correct
  66. *
  67. * The CSS or LESS files may refer to other resources using `import` or url() notation
  68. * We want to check integrity of all these references
  69. * Note that the references may have syntax specific to the Magento preprocessing subsystem
  70. *
  71. * @param string $area
  72. * @param string $themePath
  73. * @param string $locale
  74. * @param string $module
  75. * @param string $filePath
  76. * @param string $absolutePath
  77. * @dataProvider referencesFromStaticFilesDataProvider
  78. */
  79. public function testReferencesFromStaticFiles($area, $themePath, $locale, $module, $filePath, $absolutePath)
  80. {
  81. $contents = file_get_contents($absolutePath);
  82. preg_match_all(
  83. \Magento\Framework\View\Url\CssResolver::REGEX_CSS_RELATIVE_URLS,
  84. $contents,
  85. $matches
  86. );
  87. foreach ($matches[1] as $relatedResource) {
  88. if (false !== strpos($relatedResource, '@')) { // unable to parse paths with LESS variables/mixins
  89. continue;
  90. }
  91. list($relatedModule, $relatedPath) =
  92. \Magento\Framework\View\Asset\Repository::extractModule($relatedResource);
  93. if ($relatedModule) {
  94. $fallbackModule = $relatedModule;
  95. } else {
  96. if ('less' == pathinfo($filePath, PATHINFO_EXTENSION)) {
  97. /**
  98. * The LESS library treats the related resources with relative links not in the same way as CSS:
  99. * when another LESS file is included, it is embedded directly into the resulting document, but the
  100. * relative paths of related resources are not adjusted accordingly to the new root file.
  101. * Probably it is a bug of the LESS library.
  102. */
  103. $this->markTestSkipped("Due to LESS library specifics, the '{$relatedResource}' cannot be tested.");
  104. }
  105. $fallbackModule = $module;
  106. $relatedPath = \Magento\Framework\View\FileSystem::getRelatedPath($filePath, $relatedResource);
  107. }
  108. // the $relatedPath will be suitable for feeding to the fallback system
  109. $staticFile = $this->getStaticFile($area, $themePath, $locale, $relatedPath, $fallbackModule);
  110. if (empty($staticFile) && substr($relatedPath, 0, 2) === '..') {
  111. //check if static file exists on lib level
  112. $path = substr($relatedPath, 2);
  113. $libDir = rtrim($this->filesystem->getDirectoryRead(DirectoryList::LIB_WEB)->getAbsolutePath(), '/');
  114. $rule = $this->simpleFactory->create(['pattern' => $libDir]);
  115. $params = ['area' => $area, 'theme' => $themePath, 'locale' => $locale];
  116. $staticFile = $this->alternativeResolver->resolveFile($rule, $path, $params);
  117. }
  118. $this->assertNotEmpty(
  119. $staticFile,
  120. "The related resource cannot be resolved through fallback: '{$relatedResource}'"
  121. );
  122. }
  123. }
  124. /**
  125. * Get a default theme path for specified area
  126. *
  127. * @param string $area
  128. * @return string
  129. * @throws \LogicException
  130. */
  131. private function getDefaultThemePath($area)
  132. {
  133. switch ($area) {
  134. case 'frontend':
  135. return $this->design->getConfigurationDesignTheme($area);
  136. case 'adminhtml':
  137. return 'Magento/backend';
  138. case 'doc':
  139. return 'Magento/blank';
  140. default:
  141. throw new \LogicException('Unable to determine theme path');
  142. }
  143. }
  144. /**
  145. * Get static file through fallback system using specified params
  146. *
  147. * @param string $area
  148. * @param string|\Magento\Framework\View\Design\ThemeInterface $theme - either theme path (string) or theme object
  149. * @param string $locale
  150. * @param string $filePath
  151. * @param string $module
  152. * @param bool $isExplicit
  153. * @return bool|string
  154. */
  155. private function getStaticFile($area, $theme, $locale, $filePath, $module = null, $isExplicit = false)
  156. {
  157. if ($area == 'base') {
  158. $theme = $this->baseTheme;
  159. }
  160. if (!is_object($theme)) {
  161. $themePath = $theme ?: $this->getDefaultThemePath($area);
  162. $theme = $this->themeRepo->create($themePath, $area);
  163. }
  164. if ($isExplicit) {
  165. $type = \Magento\Framework\View\Design\Fallback\RulePool::TYPE_STATIC_FILE;
  166. return $this->explicitFallback->resolve($type, $filePath, $area, $theme, $locale, $module);
  167. }
  168. return $this->fallback->getFile($area, $theme, $locale, $filePath, $module);
  169. }
  170. /**
  171. * @return array
  172. */
  173. public function referencesFromStaticFilesDataProvider()
  174. {
  175. return \Magento\Framework\App\Utility\Files::init()->getStaticPreProcessingFiles('*.{less,css}');
  176. }
  177. /**
  178. * There must be either .css or .less file, because if there are both, then .less will not be found by fallback
  179. *
  180. * @param string $area
  181. * @param string $themePath
  182. * @param string $locale
  183. * @param string $module
  184. * @param string $filePath
  185. * @dataProvider lessNotConfusedWithCssDataProvider
  186. */
  187. public function testLessNotConfusedWithCss($area, $themePath, $locale, $module, $filePath)
  188. {
  189. if (false !== strpos($filePath, 'widgets.css')) {
  190. $filePath .= '';
  191. }
  192. $fileName = pathinfo($filePath, PATHINFO_FILENAME);
  193. $dirName = dirname($filePath);
  194. if ('.' == $dirName) {
  195. $dirName = '';
  196. } else {
  197. $dirName .= '/';
  198. }
  199. $cssPath = $dirName . $fileName . '.css';
  200. $lessPath = $dirName . $fileName . '.less';
  201. $cssFile = $this->getStaticFile($area, $themePath, $locale, $cssPath, $module, true);
  202. $lessFile = $this->getStaticFile($area, $themePath, $locale, $lessPath, $module, true);
  203. $this->assertFalse(
  204. $cssFile && $lessFile,
  205. "A resource file of only one type must exist. Both found: '$cssFile' and '$lessFile'"
  206. );
  207. }
  208. /**
  209. * @return array
  210. */
  211. public function lessNotConfusedWithCssDataProvider()
  212. {
  213. return \Magento\Framework\App\Utility\Files::init()->getStaticPreProcessingFiles('*.{less,css}');
  214. }
  215. /**
  216. * Test if references $this->getViewFileUrl() in .phtml-files are correct
  217. *
  218. * @param string $phtmlFile
  219. * @param string $area
  220. * @param string $themePath
  221. * @param string $fileId
  222. * @dataProvider referencesFromPhtmlFilesDataProvider
  223. */
  224. public function testReferencesFromPhtmlFiles($phtmlFile, $area, $themePath, $fileId)
  225. {
  226. list($module, $filePath) = \Magento\Framework\View\Asset\Repository::extractModule($fileId);
  227. $this->assertNotEmpty(
  228. $this->getStaticFile($area, $themePath, 'en_US', $filePath, $module),
  229. "Unable to locate '{$fileId}' reference from {$phtmlFile}"
  230. );
  231. }
  232. /**
  233. * @return array
  234. */
  235. public function referencesFromPhtmlFilesDataProvider()
  236. {
  237. $result = [];
  238. foreach (\Magento\Framework\App\Utility\Files::init()->getPhtmlFiles(true, false) as $info) {
  239. list($area, $themePath, , , $file) = $info;
  240. foreach ($this->collectGetViewFileUrl($file) as $fileId) {
  241. $result[] = [$file, $area, $themePath, $fileId];
  242. }
  243. }
  244. return $result;
  245. }
  246. /**
  247. * Find invocations of $block->getViewFileUrl() and extract the first argument value
  248. *
  249. * @param string $file
  250. * @return array
  251. */
  252. private function collectGetViewFileUrl($file)
  253. {
  254. $result = [];
  255. if (preg_match_all('/\$block->getViewFileUrl\(\'([^\']+?)\'\)/', file_get_contents($file), $matches)) {
  256. foreach ($matches[1] as $fileId) {
  257. $result[] = $fileId;
  258. }
  259. }
  260. return $result;
  261. }
  262. /**
  263. * @param string $layoutFile
  264. * @param string $area
  265. * @param string $themePath
  266. * @param string $fileId
  267. * @dataProvider referencesFromLayoutFilesDataProvider
  268. */
  269. public function testReferencesFromLayoutFiles($layoutFile, $area, $themePath, $fileId)
  270. {
  271. list($module, $filePath) = \Magento\Framework\View\Asset\Repository::extractModule($fileId);
  272. $this->assertNotEmpty(
  273. $this->getStaticFile($area, $themePath, 'en_US', $filePath, $module),
  274. "Unable to locate '{$fileId}' reference from layout XML in {$layoutFile}"
  275. );
  276. }
  277. /**
  278. * @return array
  279. */
  280. public function referencesFromLayoutFilesDataProvider()
  281. {
  282. $result = [];
  283. $files = \Magento\Framework\App\Utility\Files::init()->getLayoutFiles(['with_metainfo' => true], false);
  284. foreach ($files as $metaInfo) {
  285. list($area, $themePath, , , $file) = $metaInfo;
  286. foreach ($this->collectFileIdsFromLayout($file) as $fileId) {
  287. $result[] = [$file, $area, $themePath, $fileId];
  288. }
  289. }
  290. return $result;
  291. }
  292. /**
  293. * Collect view file declarations in layout XML-files
  294. *
  295. * @param string $file
  296. * @return array
  297. */
  298. private function collectFileIdsFromLayout($file)
  299. {
  300. $xml = simplexml_load_file($file);
  301. $elements = $xml->xpath('//head/css|link|script');
  302. $result = [];
  303. if ($elements) {
  304. foreach ($elements as $node) {
  305. $result[] = (string)$node;
  306. }
  307. }
  308. return $result;
  309. }
  310. }