271 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			PHP
		
	
	
		
			Executable File
		
	
	
			
		
		
	
	
			271 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			PHP
		
	
	
		
			Executable File
		
	
	
<?php
 | 
						|
/**
 | 
						|
 * Copyright © Magento, Inc. All rights reserved.
 | 
						|
 * See COPYING.txt for license details.
 | 
						|
 */
 | 
						|
namespace Magento\Ui\Component;
 | 
						|
 | 
						|
use Magento\Framework\App\Filesystem\DirectoryList;
 | 
						|
use Magento\Framework\Component\ComponentFile;
 | 
						|
use Magento\Framework\Component\ComponentRegistrar;
 | 
						|
use Magento\Framework\Component\DirSearch;
 | 
						|
use Magento\Framework\Exception\FileSystemException;
 | 
						|
use Magento\Framework\Exception\ValidatorException;
 | 
						|
use Magento\Framework\Filesystem;
 | 
						|
use Magento\Framework\Filesystem\Directory\ReadInterface;
 | 
						|
use Magento\TestFramework\Helper\Bootstrap;
 | 
						|
use Magento\Ui\Config\Reader\DefinitionMap;
 | 
						|
 | 
						|
/**
 | 
						|
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
 | 
						|
 */
 | 
						|
class ConfigurationTest extends \PHPUnit\Framework\TestCase
 | 
						|
{
 | 
						|
    /**
 | 
						|
     * @var DirSearch
 | 
						|
     */
 | 
						|
    private $dirSearch;
 | 
						|
 | 
						|
    /**
 | 
						|
     * @var ReadInterface
 | 
						|
     */
 | 
						|
    private $appDir;
 | 
						|
 | 
						|
    /**
 | 
						|
     * @var ReadInterface
 | 
						|
     */
 | 
						|
    private $rootDir;
 | 
						|
 | 
						|
    /**
 | 
						|
     * @var \DOMDocument
 | 
						|
     */
 | 
						|
    private $dom;
 | 
						|
 | 
						|
    /**
 | 
						|
     * @var array
 | 
						|
     */
 | 
						|
    private $map;
 | 
						|
 | 
						|
    /**
 | 
						|
     * @var string
 | 
						|
     */
 | 
						|
    private $currentComponent;
 | 
						|
 | 
						|
    /**
 | 
						|
     * @var ComponentFile
 | 
						|
     */
 | 
						|
    private $currentFile;
 | 
						|
 | 
						|
    /**
 | 
						|
     * @var array
 | 
						|
     */
 | 
						|
    private $whiteList = [
 | 
						|
        'argument[@name="data"]/item[@name="config"]/item[@name="multiple"]' => [
 | 
						|
            '//*[@formElement="select"]',
 | 
						|
            '//*[substring(@component, string-length(@component) - string-length("ui-group") +1) = "ui-group"]'
 | 
						|
        ]
 | 
						|
    ];
 | 
						|
 | 
						|
    protected function setUp(): void
 | 
						|
    {
 | 
						|
        $objectManager = Bootstrap::getObjectManager();
 | 
						|
        $mapReader = $objectManager->create(DefinitionMap::class);
 | 
						|
        $this->map = $mapReader->read();
 | 
						|
 | 
						|
        $this->dirSearch = $objectManager->create(DirSearch::class);
 | 
						|
 | 
						|
        /** @var Filesystem $filesystem */
 | 
						|
        $filesystem = $objectManager->create(Filesystem::class);
 | 
						|
        $this->appDir = $filesystem->getDirectoryRead(DirectoryList::APP);
 | 
						|
        $this->rootDir = $filesystem->getDirectoryRead(DirectoryList::ROOT);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @return void
 | 
						|
     */
 | 
						|
    public function testConfiguration()
 | 
						|
    {
 | 
						|
        $uiConfigurationFiles = $this->dirSearch->collectFilesWithContext(
 | 
						|
            ComponentRegistrar::MODULE,
 | 
						|
            'view/*/ui_component/*.xml'
 | 
						|
        );
 | 
						|
        $this->generateXpaths();
 | 
						|
 | 
						|
        $result = [];
 | 
						|
        /** @var ComponentFile $file */
 | 
						|
        foreach ($uiConfigurationFiles as $file) {
 | 
						|
            $this->currentFile = $file;
 | 
						|
            $fullPath = $file->getFullPath();
 | 
						|
            // by default search files in `app` directory but Magento can be installed via composer
 | 
						|
            // or some modules can be in `vendor` directory (like bundled extensions)
 | 
						|
            try {
 | 
						|
                $content = $this->appDir->readFile($this->appDir->getRelativePath($fullPath));
 | 
						|
            } catch (ValidatorException $e) {
 | 
						|
                $content = $this->rootDir->readFile($this->rootDir->getRelativePath($fullPath));
 | 
						|
            }
 | 
						|
            $this->assertConfigurationSemantic($this->getDom($content), $result);
 | 
						|
        }
 | 
						|
        if (!empty($result)) {
 | 
						|
            $this->fail(implode("\n\n", $result));
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @return void
 | 
						|
     */
 | 
						|
    private function generateXpaths()
 | 
						|
    {
 | 
						|
        foreach ($this->map as $name => &$map) {
 | 
						|
            $this->currentComponent = $name;
 | 
						|
            $xpaths = [];
 | 
						|
            $counter = 0;
 | 
						|
            while (!empty($map)) {
 | 
						|
                $this->hasXpaths($map, $xpaths, $counter);
 | 
						|
                $counter++;
 | 
						|
            }
 | 
						|
            $this->map[$name]['xpaths'] = $xpaths;
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param \DOMNode $node
 | 
						|
     * @param array $result
 | 
						|
     * @return void
 | 
						|
     */
 | 
						|
    private function assertConfigurationSemantic(\DOMNode $node, &$result = [])
 | 
						|
    {
 | 
						|
        foreach ($node->childNodes as $child) {
 | 
						|
            if ($child->nodeType === XML_ELEMENT_NODE) {
 | 
						|
                if (isset($this->map[$child->localName])) {
 | 
						|
                    $xpaths = [];
 | 
						|
                    $this->currentComponent = $child->localName;
 | 
						|
                    if (isset($this->map[$this->currentComponent]['xpaths'])) {
 | 
						|
                        $xpaths = $this->map[$this->currentComponent]['xpaths'];
 | 
						|
                    }
 | 
						|
 | 
						|
                    $domXpath = new \DOMXPath($this->getDom());
 | 
						|
                    foreach ($xpaths as $xpathData) {
 | 
						|
                        if ($domXpath->query($xpathData['xpath'], $child)->length !== 0
 | 
						|
                            && !$this->isAvailable($xpathData['xpath'], $child)
 | 
						|
                        ) {
 | 
						|
                            $result[] = 'Xpath: "' . $xpathData['xpath'] . '" is a forbidden.' . "\n" .
 | 
						|
                                'This node should migrate to "' . trim($xpathData['target']) . "\"\n" .
 | 
						|
                                'File: ' . $this->currentFile->getFullPath() . "\n";
 | 
						|
                        }
 | 
						|
                    }
 | 
						|
                }
 | 
						|
                $this->assertConfigurationSemantic($child, $result);
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param string $targetXpath
 | 
						|
     * @param \DOMElement $node
 | 
						|
     * @return bool
 | 
						|
     */
 | 
						|
    private function isAvailable($targetXpath, \DOMElement $node)
 | 
						|
    {
 | 
						|
        $domXpath = new \DOMXPath($this->getDom());
 | 
						|
        if (isset($this->whiteList[$targetXpath])) {
 | 
						|
            $availableForXpath = $this->whiteList[$targetXpath];
 | 
						|
            foreach ($availableForXpath as $xpath) {
 | 
						|
                $result = $domXpath->query($xpath, $node);
 | 
						|
                if ($result->length != 0) {
 | 
						|
                    return true;
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param array $data
 | 
						|
     * @param array $result
 | 
						|
     * @param int $counter
 | 
						|
     * @return bool
 | 
						|
     *
 | 
						|
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
 | 
						|
     */
 | 
						|
    private function hasXpaths(array &$data, array &$result, $counter)
 | 
						|
    {
 | 
						|
 | 
						|
        foreach ($data as $name => &$child) {
 | 
						|
            if (!is_array($child)) {
 | 
						|
                continue;
 | 
						|
            }
 | 
						|
            if (isset($child['value'])) {
 | 
						|
                $result[$counter]['xpath'] .= '[@name="' . $child['name'] . '"]';
 | 
						|
                $result[$counter]['target'] = $child['value'];
 | 
						|
                unset($data[$name]);
 | 
						|
                $this->deleteEmptyNodes($this->map[$this->currentComponent]);
 | 
						|
                return true;
 | 
						|
            }
 | 
						|
 | 
						|
            if (isset($child['name']) && is_string($child['name'])) {
 | 
						|
                $result[$counter]['xpath'] .= '[@name="' . $child['name'] . '"]';
 | 
						|
                $break = false;
 | 
						|
                if (isset($child['item'])) {
 | 
						|
                    $result[$counter]['xpath'] .= '/item';
 | 
						|
                    $break = $this->hasXpaths($child['item'], $result, $counter);
 | 
						|
                } elseif (isset($child['argument'])) {
 | 
						|
                    $result[$counter]['xpath'] .= '/argument';
 | 
						|
                    $break = $this->hasXpaths($child['argument'], $result, $counter);
 | 
						|
                }
 | 
						|
                if ($break) {
 | 
						|
                    return true;
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            if (!isset($result[$counter]['xpath'])) {
 | 
						|
                $result[$counter]['xpath'] = '';
 | 
						|
            }
 | 
						|
            $result[$counter]['xpath'] .= $name;
 | 
						|
            $this->hasXpaths($child, $result, $counter);
 | 
						|
        }
 | 
						|
        return true;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param array $map
 | 
						|
     * @param bool $isRemoveParentNode
 | 
						|
     * @return bool
 | 
						|
     *
 | 
						|
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
 | 
						|
     */
 | 
						|
    private function deleteEmptyNodes(array &$map, $isRemoveParentNode = false)
 | 
						|
    {
 | 
						|
        if (empty($map)) {
 | 
						|
            return true;
 | 
						|
        }
 | 
						|
        foreach ($map as $name => &$child) {
 | 
						|
            if (is_array($child)) {
 | 
						|
                $isRemoveParentNode = $this->deleteEmptyNodes($map[$name]);
 | 
						|
            }
 | 
						|
            if (empty($map[$name]) || $isRemoveParentNode) {
 | 
						|
                if ((isset($map['item']) && empty($map['item']))
 | 
						|
                    || (isset($map['argument']) && empty($map['argument']))
 | 
						|
                ) {
 | 
						|
                    unset($map[$name]);
 | 
						|
                    return true;
 | 
						|
                }
 | 
						|
                unset($map[$name]);
 | 
						|
            }
 | 
						|
        }
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param string|null $content
 | 
						|
     * @return \DOMDocument
 | 
						|
     */
 | 
						|
    private function getDom($content = null)
 | 
						|
    {
 | 
						|
        if ($content) {
 | 
						|
            $this->dom = new \DOMDocument();
 | 
						|
            $this->dom->loadXML($content);
 | 
						|
        }
 | 
						|
        return $this->dom;
 | 
						|
    }
 | 
						|
}
 |