vendor/sulu/sulu/src/Sulu/Component/Content/Metadata/Loader/StructureXmlLoader.php line 30

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of Sulu.
  4.  *
  5.  * (c) Sulu GmbH
  6.  *
  7.  * This source file is subject to the MIT license that is bundled
  8.  * with this source code in the file LICENSE.
  9.  */
  10. namespace Sulu\Component\Content\Metadata\Loader;
  11. use Sulu\Bundle\HttpCacheBundle\CacheLifetime\CacheLifetimeResolverInterface;
  12. use Sulu\Component\Content\ContentTypeManagerInterface;
  13. use Sulu\Component\Content\Metadata\Loader\Exception\InvalidXmlException;
  14. use Sulu\Component\Content\Metadata\Loader\Exception\RequiredPropertyNameNotFoundException;
  15. use Sulu\Component\Content\Metadata\Loader\Exception\RequiredTagNotFoundException;
  16. use Sulu\Component\Content\Metadata\Loader\Exception\ReservedPropertyNameException;
  17. use Sulu\Component\Content\Metadata\Parser\PropertiesXmlParser;
  18. use Sulu\Component\Content\Metadata\Parser\SchemaXmlParser;
  19. use Sulu\Component\Content\Metadata\PropertyMetadata;
  20. use Sulu\Component\Content\Metadata\SectionMetadata;
  21. use Sulu\Component\Content\Metadata\StructureMetadata;
  22. use Sulu\Component\Content\Metadata\XmlParserTrait;
  23. /**
  24.  * Reads a template xml and returns a StructureMetadata.
  25.  */
  26. class StructureXmlLoader extends AbstractLoader
  27. {
  28.     use XmlParserTrait;
  29.     public const SCHEME_PATH '/schema/template-1.0.xsd';
  30.     public const SCHEMA_NAMESPACE_URI 'http://schemas.sulu.io/template/template';
  31.     /**
  32.      * Tags that are required in template.
  33.      *
  34.      * @var array
  35.      */
  36.     private $requiredTagNames = [];
  37.     /**
  38.      * reserved names for sulu internals
  39.      * TODO should be possible to inject from config.
  40.      *
  41.      * @var array
  42.      */
  43.     private $reservedPropertyNames = [
  44.         'template',
  45.         'changer',
  46.         'changed',
  47.         'creator',
  48.         'created',
  49.         'published',
  50.         'state',
  51.         'internal',
  52.         'nodeType',
  53.         'navContexts',
  54.         'shadow-on',
  55.         'shadow-base',
  56.         'author',
  57.         'authored',
  58.         'type',
  59.         'id',
  60.         'webspace',
  61.     ];
  62.     /**
  63.      * Properties that are required in template.
  64.      *
  65.      * @var array
  66.      */
  67.     private $requiredPropertyNames = [];
  68.     /**
  69.      * @var CacheLifetimeResolverInterface
  70.      */
  71.     private $cacheLifetimeResolver;
  72.     /**
  73.      * @var PropertiesXmlParser
  74.      */
  75.     private $propertiesXmlParser;
  76.     /**
  77.      * @var SchemaXmlParser
  78.      */
  79.     private $schemaXmlParser;
  80.     /**
  81.      * @var ContentTypeManagerInterface
  82.      */
  83.     private $contentTypeManager;
  84.     public function __construct(
  85.         CacheLifetimeResolverInterface $cacheLifetimeResolver,
  86.         PropertiesXmlParser $propertiesXmlParser,
  87.         SchemaXmlParser $schemaXmlParser,
  88.         ContentTypeManagerInterface $contentTypeManager,
  89.         array $requiredPropertyNames,
  90.         array $requiredTagNames
  91.     ) {
  92.         $this->cacheLifetimeResolver $cacheLifetimeResolver;
  93.         $this->propertiesXmlParser $propertiesXmlParser;
  94.         $this->schemaXmlParser $schemaXmlParser;
  95.         $this->contentTypeManager $contentTypeManager;
  96.         $this->requiredPropertyNames $requiredPropertyNames;
  97.         $this->requiredTagNames $requiredTagNames;
  98.         parent::__construct(
  99.             self::SCHEME_PATH,
  100.             self::SCHEMA_NAMESPACE_URI
  101.         );
  102.     }
  103.     public function load($resource$type null)
  104.     {
  105.         if (null === $type) {
  106.             $type 'page';
  107.         }
  108.         $data parent::load($resource$type);
  109.         $data $this->normalizeStructureData($data);
  110.         $structure = new StructureMetadata();
  111.         $structure->setResource($resource);
  112.         $structure->setName($data['key']);
  113.         $structure->setCacheLifetime($data['cacheLifetime']);
  114.         $structure->setController($data['controller']);
  115.         $structure->setInternal($data['internal']);
  116.         $structure->setCacheLifetime($data['cacheLifetime']);
  117.         $structure->setAreas($data['areas']);
  118.         $structure->setView($data['view']);
  119.         $structure->setTags($data['tags']);
  120.         $structure->setParameters($data['params']);
  121.         if (isset($data['schema'])) {
  122.             $structure->setSchema($data['schema']);
  123.         }
  124.         foreach ($data['properties'] as $property) {
  125.             $structure->addChild($property);
  126.         }
  127.         $structure->burnProperties();
  128.         $this->mapMeta($structure$data['meta']);
  129.         return $structure;
  130.     }
  131.     protected function parse($resource, \DOMXPath $xpath$type)
  132.     {
  133.         // init running vars
  134.         $tags = [];
  135.         // init result
  136.         $result $this->loadTemplateAttributes($resource$xpath$type);
  137.         // load properties
  138.         $propertiesNode $xpath->query('/x:template/x:properties')->item(0);
  139.         $result['properties'] = $this->propertiesXmlParser->load(
  140.             $tags,
  141.             $xpath,
  142.             $propertiesNode,
  143.             $type
  144.         );
  145.         $schemaNode $xpath->query('/x:template/x:schema')->item(0);
  146.         if ($schemaNode) {
  147.             $result['schema'] = $this->schemaXmlParser->load($xpath$schemaNode);
  148.         }
  149.         $missingProperty $this->findMissingRequiredProperties($type$result['properties']);
  150.         if ($missingProperty) {
  151.             throw new RequiredPropertyNameNotFoundException($result['key'], $missingProperty);
  152.         }
  153.         $reservedProperty $this->findReservedProperties($result['properties']);
  154.         if ($reservedProperty) {
  155.             throw new ReservedPropertyNameException($result['key'], $reservedProperty);
  156.         }
  157.         $result['properties'] = \array_filter($result['properties'], function($property) {
  158.             if (!$property instanceof PropertyMetadata) {
  159.                 return true;
  160.             }
  161.             $propertyType $property->getType();
  162.             if ($this->contentTypeManager->has($propertyType)) {
  163.                 return true;
  164.             }
  165.             if ('ignore' === $property->getOnInvalid()) {
  166.                 return false;
  167.             }
  168.             throw new \InvalidArgumentException(\sprintf(
  169.                 'Content type with alias "%s" has not been registered. Known content types are: "%s"',
  170.                 $propertyType,
  171.                 \implode('", "'$this->contentTypeManager->getAll())
  172.             ));
  173.         });
  174.         // FIXME until excerpt-template is no page template anymore
  175.         // - https://github.com/sulu-io/sulu/issues/1220#issuecomment-110704259
  176.         if (!\array_key_exists('internal'$result) || !$result['internal']) {
  177.             if (isset($this->requiredTagNames[$type])) {
  178.                 foreach ($this->requiredTagNames[$type] as $requiredTagName) {
  179.                     if (!\array_key_exists($requiredTagName$tags)) {
  180.                         throw new RequiredTagNotFoundException($result['key'], $requiredTagName);
  181.                     }
  182.                 }
  183.             }
  184.         }
  185.         return $result;
  186.     }
  187.     /**
  188.      * Load template attributes.
  189.      */
  190.     protected function loadTemplateAttributes($resource, \DOMXPath $xpath$type)
  191.     {
  192.         if ('page' === $type || 'home' === $type) {
  193.             $result = [
  194.                 'key' => $this->getValueFromXPath('/x:template/x:key'$xpath),
  195.                 'view' => $this->getValueFromXPath('/x:template/x:view'$xpath),
  196.                 'controller' => $this->getValueFromXPath('/x:template/x:controller'$xpath),
  197.                 'internal' => $this->getValueFromXPath('/x:template/x:internal'$xpath),
  198.                 'cacheLifetime' => $this->loadCacheLifetime('/x:template/x:cacheLifetime'$xpath),
  199.                 'tags' => $this->loadStructureTags('/x:template/x:tag'$xpath),
  200.                 'areas' => $this->loadStructureAreas('/x:template/x:areas/x:area'$xpath),
  201.                 'meta' => $this->loadMeta('/x:template/x:meta/x:*'$xpath),
  202.             ];
  203.             $result = \array_filter(
  204.                 $result,
  205.                 function($value) {
  206.                     return null !== $value;
  207.                 }
  208.             );
  209.             foreach (['key''view''controller''cacheLifetime'] as $requiredProperty) {
  210.                 if (!isset($result[$requiredProperty])) {
  211.                     throw new InvalidXmlException(
  212.                         $type,
  213.                         \sprintf(
  214.                             'Property "%s" is required in XML template file "%s"',
  215.                             $requiredProperty,
  216.                             $resource
  217.                         )
  218.                     );
  219.                 }
  220.             }
  221.         } else {
  222.             $result = [
  223.                 'key' => $this->getValueFromXPath('/x:template/x:key'$xpath),
  224.                 'view' => $this->getValueFromXPath('/x:template/x:view'$xpath),
  225.                 'controller' => $this->getValueFromXPath('/x:template/x:controller'$xpath),
  226.                 'cacheLifetime' => $this->loadCacheLifetime('/x:template/x:cacheLifetime'$xpath),
  227.                 'tags' => $this->loadStructureTags('/x:template/x:tag'$xpath),
  228.                 'areas' => $this->loadStructureAreas('/x:template/x:areas/x:area'$xpath),
  229.                 'meta' => $this->loadMeta('/x:template/x:meta/x:*'$xpath),
  230.             ];
  231.             $result = \array_filter(
  232.                 $result,
  233.                 function($value) {
  234.                     return null !== $value;
  235.                 }
  236.             );
  237.             if (\count($result) < 1) {
  238.                 throw new InvalidXmlException($result['key']);
  239.             }
  240.         }
  241.         return $result;
  242.     }
  243.     /**
  244.      * Load cache lifetime metadata.
  245.      *
  246.      * @param string $path
  247.      *
  248.      * @return array
  249.      */
  250.     private function loadCacheLifetime($path, \DOMXPath $xpath)
  251.     {
  252.         $nodeList $xpath->query($path);
  253.         if (!$nodeList->length) {
  254.             return [
  255.                 'type' => CacheLifetimeResolverInterface::TYPE_SECONDS,
  256.                 'value' => 0,
  257.             ];
  258.         }
  259.         // get first node
  260.         $node $nodeList->item(0);
  261.         $type $node->getAttribute('type');
  262.         if ('' === $type) {
  263.             $type CacheLifetimeResolverInterface::TYPE_SECONDS;
  264.         }
  265.         $value $node->nodeValue;
  266.         if (!$this->cacheLifetimeResolver->supports($type$value)) {
  267.             throw new \InvalidArgumentException(
  268.                 \sprintf('CacheLifetime "%s" with type "%s" not supported.'$value$type)
  269.             );
  270.         }
  271.         return [
  272.             'type' => $type,
  273.             'value' => $value,
  274.         ];
  275.     }
  276.     private function normalizeStructureData($data)
  277.     {
  278.         $data = \array_replace_recursive(
  279.             [
  280.                 'key' => null,
  281.                 'view' => null,
  282.                 'controller' => null,
  283.                 'internal' => false,
  284.                 'cacheLifetime' => null,
  285.                 'areas' => [],
  286.             ],
  287.             $this->normalizeItem($data)
  288.         );
  289.         return $data;
  290.     }
  291.     private function normalizeItem($data)
  292.     {
  293.         $data = \array_merge_recursive(
  294.             [
  295.                 'meta' => [
  296.                     'title' => [],
  297.                     'info_text' => [],
  298.                     'placeholder' => [],
  299.                 ],
  300.                 'params' => [],
  301.                 'tags' => [],
  302.             ],
  303.             $data
  304.         );
  305.         return $data;
  306.     }
  307.     private function mapMeta(StructureMetadata $structure$meta)
  308.     {
  309.         $structure->setTitles($meta['title']);
  310.         $structure->setDescriptions($meta['info_text']);
  311.     }
  312.     private function findMissingRequiredProperties(string $type, array $propertyData): ?string
  313.     {
  314.         if (!\array_key_exists($type$this->requiredPropertyNames)) {
  315.             return null;
  316.         }
  317.         foreach ($this->requiredPropertyNames[$type] as $requiredPropertyName) {
  318.             if ($this->isRequiredPropertyMissing($type$propertyData$requiredPropertyName)) {
  319.                 return $requiredPropertyName;
  320.             }
  321.         }
  322.         return null;
  323.     }
  324.     private function isRequiredPropertyMissing(string $type, array $propertyDatastring $requiredPropertyName): bool
  325.     {
  326.         foreach ($propertyData as $property) {
  327.             if ($property->getName() === $requiredPropertyName) {
  328.                 return false;
  329.             }
  330.             if ($property instanceof SectionMetadata) {
  331.                 $isPropertyMissing $this->findMissingRequiredProperties($type$property->getChildren());
  332.                 if (!$isPropertyMissing) {
  333.                     return false;
  334.                 }
  335.             }
  336.         }
  337.         return true;
  338.     }
  339.     private function findReservedProperties(array $propertyData): ?string
  340.     {
  341.         foreach ($this->reservedPropertyNames as $reservedPropertyName) {
  342.             if ($this->isReservedProperty($propertyData$reservedPropertyName)) {
  343.                 return $reservedPropertyName;
  344.             }
  345.         }
  346.         return null;
  347.     }
  348.     private function isReservedProperty(array $propertyDatastring $reservedPropertyName): bool
  349.     {
  350.         foreach ($propertyData as $property) {
  351.             if ($property->getName() === $reservedPropertyName) {
  352.                 return true;
  353.             }
  354.             if ($property instanceof SectionMetadata) {
  355.                 $isReservedProperty $this->isReservedProperty(
  356.                     $property->getChildren(),
  357.                     $reservedPropertyName
  358.                 );
  359.                 if ($isReservedProperty) {
  360.                     return true;
  361.                 }
  362.             }
  363.         }
  364.         return false;
  365.     }
  366. }