vendor/sulu/sulu/src/Sulu/Component/Webspace/Manager/WebspaceManager.php line 428

  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\Webspace\Manager;
  11. use Sulu\Component\Content\Metadata\Factory\StructureMetadataFactoryInterface;
  12. use Sulu\Component\Content\Metadata\StructureMetadata;
  13. use Sulu\Component\Localization\Localization;
  14. use Sulu\Component\Util\WildcardUrlUtil;
  15. use Sulu\Component\Webspace\Analyzer\Attributes\RequestAttributes;
  16. use Sulu\Component\Webspace\Analyzer\RequestAnalyzerInterface;
  17. use Sulu\Component\Webspace\Manager\Dumper\PhpWebspaceCollectionDumper;
  18. use Sulu\Component\Webspace\Portal;
  19. use Sulu\Component\Webspace\PortalInformation;
  20. use Sulu\Component\Webspace\Url\ReplacerInterface;
  21. use Sulu\Component\Webspace\Webspace;
  22. use Symfony\Component\Config\ConfigCache;
  23. use Symfony\Component\Config\Loader\LoaderInterface;
  24. use Symfony\Component\HttpFoundation\RequestStack;
  25. /**
  26.  * This class is responsible for loading, reading and caching the portal configuration files.
  27.  */
  28. class WebspaceManager implements WebspaceManagerInterface
  29. {
  30.     /**
  31.      * @var WebspaceCollection
  32.      */
  33.     private $webspaceCollection;
  34.     /**
  35.      * @var LoaderInterface
  36.      */
  37.     private $loader;
  38.     /**
  39.      * @var ReplacerInterface
  40.      */
  41.     private $urlReplacer;
  42.     /**
  43.      * @var RequestStack
  44.      */
  45.     private $requestStack;
  46.     /**
  47.      * @var array
  48.      */
  49.     private $options;
  50.     /**
  51.      * @var string
  52.      */
  53.     private $environment;
  54.     /**
  55.      * @var string
  56.      */
  57.     private $defaultHost;
  58.     /**
  59.      * @var string
  60.      */
  61.     private $defaultScheme;
  62.     /**
  63.      * @var StructureMetadataFactoryInterface
  64.      */
  65.     private $structureMetadataFactory;
  66.     /**
  67.      * @var mixed[]
  68.      */
  69.     private $portalUrlCache = [];
  70.     public function __construct(
  71.         LoaderInterface $loader,
  72.         ReplacerInterface $urlReplacer,
  73.         RequestStack $requestStack,
  74.         array $options,
  75.         string $environment,
  76.         string $defaultHost,
  77.         string $defaultScheme,
  78.         StructureMetadataFactoryInterface $structureMetadataFactory
  79.     ) {
  80.         $this->loader $loader;
  81.         $this->urlReplacer $urlReplacer;
  82.         $this->requestStack $requestStack;
  83.         $this->setOptions($options);
  84.         $this->environment $environment;
  85.         $this->defaultHost $defaultHost;
  86.         $this->defaultScheme $defaultScheme;
  87.         $this->structureMetadataFactory $structureMetadataFactory;
  88.     }
  89.     public function findWebspaceByKey(?string $key): ?Webspace
  90.     {
  91.         if (!$key) {
  92.             return null;
  93.         }
  94.         return $this->getWebspaceCollection()->getWebspace($key);
  95.     }
  96.     public function findPortalByKey(?string $key): ?Portal
  97.     {
  98.         if (!$key) {
  99.             return null;
  100.         }
  101.         return $this->getWebspaceCollection()->getPortal($key);
  102.     }
  103.     public function findPortalInformationByUrl(string $url, ?string $environment null): ?PortalInformation
  104.     {
  105.         if (null === $environment) {
  106.             $environment $this->environment;
  107.         }
  108.         $portalInformations $this->getWebspaceCollection()->getPortalInformations($environment);
  109.         foreach ($portalInformations as $portalInformation) {
  110.             if ($this->matchUrl($url$portalInformation->getUrl())) {
  111.                 return $portalInformation;
  112.             }
  113.         }
  114.         return null;
  115.     }
  116.     public function findPortalInformationsByHostIncludingSubdomains(string $host, ?string $environment null): array
  117.     {
  118.         if (null === $environment) {
  119.             $environment $this->environment;
  120.         }
  121.         return \array_filter(
  122.             $this->getWebspaceCollection()->getPortalInformations($environment),
  123.             function(PortalInformation $portalInformation) use ($host) {
  124.                 $portalHost $portalInformation->getHost();
  125.                 // add a slash to avoid problems with "example.co" and "example.com"
  126.                 return false !== \strpos($portalHost '/'$host '/');
  127.             }
  128.         );
  129.     }
  130.     public function findPortalInformationsByUrl(string $url, ?string $environment null): array
  131.     {
  132.         if (null === $environment) {
  133.             $environment $this->environment;
  134.         }
  135.         return \array_filter(
  136.             $this->getWebspaceCollection()->getPortalInformations($environment),
  137.             function(PortalInformation $portalInformation) use ($url) {
  138.                 return $this->matchUrl($url$portalInformation->getUrl());
  139.             }
  140.         );
  141.     }
  142.     public function findPortalInformationsByWebspaceKeyAndLocale(
  143.         string $webspaceKey,
  144.         string $locale,
  145.         ?string $environment null
  146.     ): array {
  147.         if (null === $environment) {
  148.             $environment $this->environment;
  149.         }
  150.         return \array_filter(
  151.             $this->getWebspaceCollection()->getPortalInformations($environment),
  152.             function(PortalInformation $portalInformation) use ($webspaceKey$locale) {
  153.                 return $portalInformation->getWebspace()->getKey() === $webspaceKey
  154.                     && $portalInformation->getLocale() === $locale;
  155.             }
  156.         );
  157.     }
  158.     public function findPortalInformationsByPortalKeyAndLocale(
  159.         string $portalKey,
  160.         string $locale,
  161.         ?string $environment null
  162.     ): array {
  163.         if (null === $environment) {
  164.             $environment $this->environment;
  165.         }
  166.         return \array_filter(
  167.             $this->getWebspaceCollection()->getPortalInformations($environment),
  168.             function(PortalInformation $portalInformation) use ($portalKey$locale) {
  169.                 return $portalInformation->getPortal()
  170.                     && $portalInformation->getPortal()->getKey() === $portalKey
  171.                     && $portalInformation->getLocale() === $locale;
  172.             }
  173.         );
  174.     }
  175.     public function findUrlsByResourceLocator(
  176.         string $resourceLocator,
  177.         ?string $environment,
  178.         string $languageCode,
  179.         ?string $webspaceKey null,
  180.         ?string $domain null,
  181.         ?string $scheme null
  182.     ): array {
  183.         if (null === $environment) {
  184.             $environment $this->environment;
  185.         }
  186.         if (null === $webspaceKey) {
  187.             $currentWebspace $this->getCurrentWebspace();
  188.             $webspaceKey $currentWebspace $currentWebspace->getKey() : $webspaceKey;
  189.         }
  190.         $urls = [];
  191.         $portals $this->getWebspaceCollection()->getPortalInformations(
  192.             $environment,
  193.             [RequestAnalyzerInterface::MATCH_TYPE_FULL]
  194.         );
  195.         foreach ($portals as $portalInformation) {
  196.             $sameLocalization $portalInformation->getLocalization()->getLocale() === $languageCode;
  197.             $sameWebspace null === $webspaceKey || $portalInformation->getWebspace()->getKey() === $webspaceKey;
  198.             $url $this->createResourceLocatorUrl($portalInformation->getUrl(), $resourceLocator$scheme);
  199.             if ($sameLocalization && $sameWebspace && $this->isFromDomain($url$domain)) {
  200.                 $urls[] = $url;
  201.             }
  202.         }
  203.         return $urls;
  204.     }
  205.     public function findUrlByResourceLocator(
  206.         ?string $resourceLocator,
  207.         ?string $environment,
  208.         string $languageCode,
  209.         ?string $webspaceKey null,
  210.         ?string $domain null,
  211.         ?string $scheme null
  212.     ): ?string {
  213.         if (null === $environment) {
  214.             $environment $this->environment;
  215.         }
  216.         if (null === $webspaceKey) {
  217.             $currentWebspace $this->getCurrentWebspace();
  218.             $webspaceKey $currentWebspace $currentWebspace->getKey() : $webspaceKey;
  219.         }
  220.         if (isset($this->portalUrlCache[$webspaceKey][$domain][$environment][$languageCode])) {
  221.             $portalUrl $this->portalUrlCache[$webspaceKey][$domain][$environment][$languageCode];
  222.             if (!$portalUrl) {
  223.                 return null;
  224.             }
  225.             return $this->createResourceLocatorUrl($portalUrl$resourceLocator$scheme);
  226.         }
  227.         $sameDomainUrl null;
  228.         $fullMatchedUrl null;
  229.         $partialMatchedUrl null;
  230.         $portals $this->getWebspaceCollection()->getPortalInformations(
  231.             $environment
  232.         );
  233.         foreach ($portals as $portalInformation) {
  234.             if (!\in_array($portalInformation->getType(), [
  235.                 RequestAnalyzerInterface::MATCH_TYPE_FULL,
  236.                 RequestAnalyzerInterface::MATCH_TYPE_PARTIAL,
  237.                 RequestAnalyzerInterface::MATCH_TYPE_REDIRECT,
  238.             ])) {
  239.                 continue;
  240.             }
  241.             $sameWebspace null === $webspaceKey || $portalInformation->getWebspace()->getKey() === $webspaceKey;
  242.             if (!$sameWebspace) {
  243.                 continue;
  244.             }
  245.             $portalLocalization $portalInformation->getLocalization();
  246.             $sameLocalization = (
  247.                 null === $portalLocalization
  248.                 || $portalLocalization->getLocale() === $languageCode
  249.             );
  250.             if (!$sameLocalization) {
  251.                 continue;
  252.             }
  253.             $portalUrl $portalInformation->getUrl();
  254.             if (RequestAnalyzerInterface::MATCH_TYPE_FULL === $portalInformation->getType()) {
  255.                 if ($this->isFromDomain('http://' $portalUrl$domain)) {
  256.                     if ($portalInformation->isMain()) {
  257.                         $sameDomainUrl $portalUrl;
  258.                     } elseif (!$sameDomainUrl) {
  259.                         $sameDomainUrl $portalUrl;
  260.                     }
  261.                 } elseif ($sameDomainUrl) {
  262.                     continue;
  263.                 } elseif ($portalInformation->isMain()) {
  264.                     $fullMatchedUrl $portalUrl;
  265.                 } elseif (!$fullMatchedUrl) {
  266.                     $fullMatchedUrl $portalUrl;
  267.                 }
  268.             } elseif ($fullMatchedUrl || $sameDomainUrl) {
  269.                 continue;
  270.             } elseif (!$partialMatchedUrl) {
  271.                 $partialMatchedUrl $portalUrl;
  272.             }
  273.         }
  274.         if ($sameDomainUrl) {
  275.             $portalUrl $sameDomainUrl;
  276.         } elseif ($fullMatchedUrl) {
  277.             $portalUrl $fullMatchedUrl;
  278.         } elseif ($partialMatchedUrl) {
  279.             $portalUrl $partialMatchedUrl;
  280.         } else {
  281.             $portalUrl null;
  282.         }
  283.         $this->portalUrlCache[$webspaceKey][$domain][$environment][$languageCode] = $portalUrl;
  284.         if (!$portalUrl) {
  285.             return null;
  286.         }
  287.         return $this->createResourceLocatorUrl($portalUrl$resourceLocator$scheme);
  288.     }
  289.     public function getPortals(): array
  290.     {
  291.         return $this->getWebspaceCollection()->getPortals();
  292.     }
  293.     public function getUrls(?string $environment null): array
  294.     {
  295.         if (null === $environment) {
  296.             $environment $this->environment;
  297.         }
  298.         $urls = [];
  299.         foreach ($this->getWebspaceCollection()->getPortalInformations($environment) as $portalInformation) {
  300.             $urls[] = $portalInformation->getUrl();
  301.         }
  302.         return $urls;
  303.     }
  304.     public function getPortalInformations(?string $environment null): array
  305.     {
  306.         if (null === $environment) {
  307.             $environment $this->environment;
  308.         }
  309.         return $this->getWebspaceCollection()->getPortalInformations($environment);
  310.     }
  311.     public function getPortalInformationsByWebspaceKey(?string $environmentstring $webspaceKey): array
  312.     {
  313.         if (null === $environment) {
  314.             $environment $this->environment;
  315.         }
  316.         return \array_filter(
  317.             $this->getWebspaceCollection()->getPortalInformations($environment),
  318.             function(PortalInformation $portal) use ($webspaceKey) {
  319.                 return $portal->getWebspaceKey() === $webspaceKey;
  320.             }
  321.         );
  322.     }
  323.     public function getAllLocalizations(): array
  324.     {
  325.         $localizations = [];
  326.         /** @var Webspace $webspace */
  327.         foreach ($this->getWebspaceCollection() as $webspace) {
  328.             foreach ($webspace->getAllLocalizations() as $localization) {
  329.                 $localizations[$localization->getLocale()] = $localization;
  330.             }
  331.         }
  332.         return $localizations;
  333.     }
  334.     public function getAllLocales(): array
  335.     {
  336.         return \array_values(
  337.             \array_map(
  338.                 function(Localization $localization) {
  339.                     return $localization->getLocale();
  340.                 },
  341.                 $this->getAllLocalizations()
  342.             )
  343.         );
  344.     }
  345.     public function getAllLocalesByWebspaces(): array
  346.     {
  347.         $webspaces = [];
  348.         foreach ($this->getWebspaceCollection() as $webspace) {
  349.             /** @var Webspace $webspace */
  350.             $locales = [];
  351.             $defaultLocale $webspace->getDefaultLocalization();
  352.             $locales[$defaultLocale->getLocale()] = $defaultLocale;
  353.             foreach ($webspace->getAllLocalizations() as $localization) {
  354.                 if (!\array_key_exists($localization->getLocale(), $locales)) {
  355.                     $locales[$localization->getLocale()] = $localization;
  356.                 }
  357.             }
  358.             $webspaces[$webspace->getKey()] = $locales;
  359.         }
  360.         return $webspaces;
  361.     }
  362.     public function getWebspaceCollection(): WebspaceCollection
  363.     {
  364.         if (null === $this->webspaceCollection) {
  365.             /** @var class-string<WebspaceCollection> $class */
  366.             $class $this->options['cache_class'];
  367.             $cache = new ConfigCache(
  368.                 $this->options['cache_dir'] . '/' $class '.php',
  369.                 $this->options['debug']
  370.             );
  371.             if (!$cache->isFresh()) {
  372.                 $availableTemplates \array_map(
  373.                     function(StructureMetadata $structure) {
  374.                         return $structure->getName();
  375.                     },
  376.                     $this->structureMetadataFactory->getStructures('page')
  377.                 );
  378.                 $webspaceCollectionBuilder = new WebspaceCollectionBuilder(
  379.                     $this->loader,
  380.                     $this->urlReplacer,
  381.                     $this->options['config_dir'],
  382.                     $availableTemplates
  383.                 );
  384.                 $webspaceCollection $webspaceCollectionBuilder->build();
  385.                 $dumper = new PhpWebspaceCollectionDumper($webspaceCollection);
  386.                 $cache->write(
  387.                     $dumper->dump(
  388.                         [
  389.                             'cache_class' => $class,
  390.                             'base_class' => $this->options['base_class'],
  391.                         ]
  392.                     ),
  393.                     $webspaceCollection->getResources()
  394.                 );
  395.             }
  396.             require_once $cache->getPath();
  397.             $this->webspaceCollection = new $class();
  398.             $currentRequest $this->requestStack->getCurrentRequest();
  399.             $host $currentRequest $currentRequest->getHost() : $this->defaultHost;
  400.             foreach ($this->getPortalInformations() as $portalInformation) {
  401.                 $portalInformation->setUrl($this->urlReplacer->replaceHost($portalInformation->getUrl(), $host));
  402.                 $portalInformation->setUrlExpression(
  403.                     $this->urlReplacer->replaceHost($portalInformation->getUrlExpression(), $host)
  404.                 );
  405.                 $portalInformation->setRedirect(
  406.                     $this->urlReplacer->replaceHost($portalInformation->getRedirect(), $host)
  407.                 );
  408.             }
  409.         }
  410.         return $this->webspaceCollection;
  411.     }
  412.     /**
  413.      * Sets the options for the manager.
  414.      *
  415.      * @param mixed[] $options
  416.      */
  417.     public function setOptions($options)
  418.     {
  419.         $this->options = [
  420.             'config_dir' => null,
  421.             'cache_dir' => null,
  422.             'debug' => false,
  423.             'cache_class' => 'WebspaceCollectionCache',
  424.             'base_class' => 'WebspaceCollection',
  425.         ];
  426.         // overwrite the default values with the given options
  427.         $this->options \array_merge($this->options$options);
  428.     }
  429.     /**
  430.      * Url is from domain.
  431.      *
  432.      * @param string $url
  433.      * @param string $domain
  434.      *
  435.      * @return bool
  436.      */
  437.     protected function isFromDomain($url$domain)
  438.     {
  439.         if (!$domain) {
  440.             return true;
  441.         }
  442.         $parsedUrl \parse_url($url);
  443.         // if domain or subdomain
  444.         if (
  445.             isset($parsedUrl['host'])
  446.             && (
  447.                 $parsedUrl['host'] == $domain
  448.                 || \fnmatch('*.' $domain$parsedUrl['host'])
  449.             )
  450.         ) {
  451.             return true;
  452.         }
  453.         return false;
  454.     }
  455.     /**
  456.      * Matches given url with portal-url.
  457.      *
  458.      * @param string $url
  459.      * @param string $portalUrl
  460.      *
  461.      * @return bool
  462.      */
  463.     protected function matchUrl($url$portalUrl)
  464.     {
  465.         return WildcardUrlUtil::match($url$portalUrl);
  466.     }
  467.     private function getCurrentWebspace(): ?Webspace
  468.     {
  469.         $currentRequest $this->requestStack->getCurrentRequest();
  470.         if (!$currentRequest) {
  471.             return null;
  472.         }
  473.         $suluAttributes $currentRequest->attributes->get('_sulu');
  474.         if (!$suluAttributes instanceof RequestAttributes) {
  475.             return null;
  476.         }
  477.         return $suluAttributes->getAttribute('webspace');
  478.     }
  479.     /**
  480.      * Return a valid resource locator url.
  481.      *
  482.      * @param string $portalUrl
  483.      * @param string $resourceLocator
  484.      * @param string|null $scheme
  485.      *
  486.      * @return string
  487.      */
  488.     private function createResourceLocatorUrl($portalUrl$resourceLocator$scheme null)
  489.     {
  490.         $currentRequest $this->requestStack->getCurrentRequest();
  491.         if (!$scheme) {
  492.             $scheme $currentRequest $currentRequest->getScheme() : $this->defaultScheme;
  493.         }
  494.         if (false !== \strpos($portalUrl'/')) {
  495.             // trim slash when resourceLocator is not domain root
  496.             $resourceLocator \rtrim($resourceLocator'/');
  497.         }
  498.         $url \rtrim(\sprintf('%s://%s'$scheme$portalUrl), '/') . $resourceLocator;
  499.         // add port if url points to host of current request and current request uses a custom port
  500.         if ($currentRequest) {
  501.             $host $currentRequest->getHost();
  502.             $port $currentRequest->getPort();
  503.             if ($url && $host && false !== \strpos($url$host)) {
  504.                 if (!('http' == $scheme && 80 == $port) && !('https' == $scheme && 443 == $port)) {
  505.                     $url \str_replace($host$host ':' $port$url);
  506.                 }
  507.             }
  508.         }
  509.         return $url;
  510.     }
  511. }