ConfigurationTest.php 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Ui\Component;
  7. use Magento\Framework\App\Filesystem\DirectoryList;
  8. use Magento\Framework\Component\ComponentFile;
  9. use Magento\Framework\Component\ComponentRegistrar;
  10. use Magento\Framework\Component\DirSearch;
  11. use Magento\Framework\Exception\FileSystemException;
  12. use Magento\Framework\Filesystem;
  13. use Magento\Framework\Filesystem\Directory\ReadInterface;
  14. use Magento\TestFramework\Helper\Bootstrap;
  15. use Magento\Ui\Config\Reader\DefinitionMap;
  16. /**
  17. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  18. */
  19. class ConfigurationTest extends \PHPUnit\Framework\TestCase
  20. {
  21. /**
  22. * @var DirSearch
  23. */
  24. private $dirSearch;
  25. /**
  26. * @var ReadInterface
  27. */
  28. private $appDir;
  29. /**
  30. * @var ReadInterface
  31. */
  32. private $rootDir;
  33. /**
  34. * @var \DOMDocument
  35. */
  36. private $dom;
  37. /**
  38. * @var array
  39. */
  40. private $map;
  41. /**
  42. * @var string
  43. */
  44. private $currentComponent;
  45. /**
  46. * @var ComponentFile
  47. */
  48. private $currentFile;
  49. /**
  50. * @var array
  51. */
  52. private $whiteList = [
  53. 'argument[@name="data"]/item[@name="config"]/item[@name="multiple"]' => [
  54. '//*[@formElement="select"]',
  55. '//*[substring(@component, string-length(@component) - string-length("ui-group") +1) = "ui-group"]'
  56. ]
  57. ];
  58. public function setUp()
  59. {
  60. $objectManager = Bootstrap::getObjectManager();
  61. $mapReader = $objectManager->create(DefinitionMap::class);
  62. $this->map = $mapReader->read();
  63. $this->dirSearch = $objectManager->create(DirSearch::class);
  64. /** @var Filesystem $filesystem */
  65. $filesystem = $objectManager->create(Filesystem::class);
  66. $this->appDir = $filesystem->getDirectoryRead(DirectoryList::APP);
  67. $this->rootDir = $filesystem->getDirectoryRead(DirectoryList::ROOT);
  68. }
  69. /**
  70. * @return void
  71. */
  72. public function testConfiguration()
  73. {
  74. $uiConfigurationFiles = $this->dirSearch->collectFilesWithContext(
  75. ComponentRegistrar::MODULE,
  76. 'view/*/ui_component/*.xml'
  77. );
  78. $this->generateXpaths();
  79. $result = [];
  80. /** @var ComponentFile $file */
  81. foreach ($uiConfigurationFiles as $file) {
  82. $this->currentFile = $file;
  83. $fullPath = $file->getFullPath();
  84. // by default search files in `app` directory but Magento can be installed via composer
  85. // or some modules can be in `vendor` directory (like bundled extensions)
  86. try {
  87. $content = $this->appDir->readFile($this->appDir->getRelativePath($fullPath));
  88. } catch (FileSystemException $e) {
  89. $content = $this->rootDir->readFile($this->rootDir->getRelativePath($fullPath));
  90. }
  91. $this->assertConfigurationSemantic($this->getDom($content), $result);
  92. }
  93. if (!empty($result)) {
  94. $this->fail(implode("\n\n", $result));
  95. }
  96. }
  97. /**
  98. * @return void
  99. */
  100. private function generateXpaths()
  101. {
  102. foreach ($this->map as $name => &$map) {
  103. $this->currentComponent = $name;
  104. $xpaths = [];
  105. $counter = 0;
  106. while (!empty($map)) {
  107. $this->hasXpaths($map, $xpaths, $counter);
  108. $counter++;
  109. }
  110. $this->map[$name]['xpaths'] = $xpaths;
  111. }
  112. }
  113. /**
  114. * @param \DOMNode $node
  115. * @param array $result
  116. * @return void
  117. */
  118. private function assertConfigurationSemantic(\DOMNode $node, &$result = [])
  119. {
  120. foreach ($node->childNodes as $child) {
  121. if ($child->nodeType === XML_ELEMENT_NODE) {
  122. if (isset($this->map[$child->localName])) {
  123. $xpaths = [];
  124. $this->currentComponent = $child->localName;
  125. if (isset($this->map[$this->currentComponent]['xpaths'])) {
  126. $xpaths = $this->map[$this->currentComponent]['xpaths'];
  127. }
  128. $domXpath = new \DOMXPath($this->getDom());
  129. foreach ($xpaths as $xpathData) {
  130. if ($domXpath->query($xpathData['xpath'], $child)->length !== 0
  131. && !$this->isAvailable($xpathData['xpath'], $child)
  132. ) {
  133. $result[] = 'Xpath: "' . $xpathData['xpath'] . '" is a forbidden.' . "\n" .
  134. 'This node should migrate to "' . trim($xpathData['target']) . "\"\n" .
  135. 'File: ' . $this->currentFile->getFullPath() . "\n";
  136. }
  137. }
  138. }
  139. $this->assertConfigurationSemantic($child, $result);
  140. }
  141. }
  142. }
  143. /**
  144. * @param string $targetXpath
  145. * @param \DOMElement $node
  146. * @return bool
  147. */
  148. private function isAvailable($targetXpath, \DOMElement $node)
  149. {
  150. $domXpath = new \DOMXPath($this->getDom());
  151. if (isset($this->whiteList[$targetXpath])) {
  152. $availableForXpath = $this->whiteList[$targetXpath];
  153. foreach ($availableForXpath as $xpath) {
  154. $result = $domXpath->query($xpath, $node);
  155. if ($result->length != 0) {
  156. return true;
  157. }
  158. }
  159. }
  160. return false;
  161. }
  162. /**
  163. * @param array $data
  164. * @param array $result
  165. * @param int $counter
  166. * @return bool
  167. *
  168. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  169. */
  170. private function hasXpaths(array &$data, array &$result, $counter)
  171. {
  172. foreach ($data as $name => &$child) {
  173. if (!is_array($child)) {
  174. continue;
  175. }
  176. if (isset($child['value'])) {
  177. $result[$counter]['xpath'] .= '[@name="' . $child['name'] . '"]';
  178. $result[$counter]['target'] = $child['value'];
  179. unset($data[$name]);
  180. $this->deleteEmptyNodes($this->map[$this->currentComponent]);
  181. return true;
  182. }
  183. if (isset($child['name']) && is_string($child['name'])) {
  184. $result[$counter]['xpath'] .= '[@name="' . $child['name'] . '"]';
  185. $break = false;
  186. if (isset($child['item'])) {
  187. $result[$counter]['xpath'] .= '/item';
  188. $break = $this->hasXpaths($child['item'], $result, $counter);
  189. } elseif (isset($child['argument'])) {
  190. $result[$counter]['xpath'] .= '/argument';
  191. $break = $this->hasXpaths($child['argument'], $result, $counter);
  192. }
  193. if ($break) {
  194. return true;
  195. }
  196. }
  197. if (!isset($result[$counter]['xpath'])) {
  198. $result[$counter]['xpath'] = '';
  199. }
  200. $result[$counter]['xpath'] .= $name;
  201. $this->hasXpaths($child, $result, $counter);
  202. }
  203. return true;
  204. }
  205. /**
  206. * @param array $map
  207. * @param bool $isRemoveParentNode
  208. * @return bool
  209. *
  210. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  211. */
  212. private function deleteEmptyNodes(array &$map, $isRemoveParentNode = false)
  213. {
  214. if (empty($map)) {
  215. return true;
  216. }
  217. foreach ($map as $name => &$child) {
  218. if (is_array($child)) {
  219. $isRemoveParentNode = $this->deleteEmptyNodes($map[$name]);
  220. }
  221. if (empty($map[$name]) || $isRemoveParentNode) {
  222. if ((isset($map['item']) && empty($map['item']))
  223. || (isset($map['argument']) && empty($map['argument']))
  224. ) {
  225. unset($map[$name]);
  226. return true;
  227. }
  228. unset($map[$name]);
  229. }
  230. }
  231. return false;
  232. }
  233. /**
  234. * @param string|null $content
  235. * @return \DOMDocument
  236. */
  237. private function getDom($content = null)
  238. {
  239. if ($content) {
  240. $this->dom = new \DOMDocument();
  241. $this->dom->loadXML($content);
  242. }
  243. return $this->dom;
  244. }
  245. }