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

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