‪TYPO3CMS  ‪main
PreviewUriBuilder.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
20 use Psr\EventDispatcher\EventDispatcherInterface;
21 use Psr\Http\Message\UriInterface;
24 use TYPO3\CMS\Backend\Utility\BackendUtility;
39 
44 {
45  public const ‪OPTION_SWITCH_FOCUS = 'switchFocus';
46  public const ‪OPTION_WINDOW_NAME = 'windowName';
47  public const ‪OPTION_WINDOW_FEATURES = 'windowFeatures';
48  public const ‪OPTION_WINDOW_SCOPE = 'windowScope';
49 
50  public const ‪OPTION_WINDOW_SCOPE_LOCAL = 'local';
51  public const ‪OPTION_WINDOW_SCOPE_GLOBAL = 'global';
52 
53  protected int ‪$pageId;
54  protected int ‪$languageId = 0;
55  protected array ‪$rootLine = [];
56  protected string ‪$section = '';
57  protected array ‪$additionalQueryParameters = [];
58  protected bool ‪$moduleLoading = true;
60 
65  public static function ‪create(int ‪$pageId): self
66  {
67  return GeneralUtility::makeInstance(static::class, ‪$pageId);
68  }
69 
73  public function ‪__construct(int ‪$pageId)
74  {
75  $this->pageId = ‪$pageId;
76  $this->context = clone GeneralUtility::makeInstance(Context::class);
77  $this->context->setAspect(
78  'visibility',
79  GeneralUtility::makeInstance(VisibilityAspect::class, true, false, false, true)
80  );
81  }
82 
87  public function ‪withModuleLoading(bool ‪$moduleLoading): self
88  {
89  if ($this->moduleLoading === ‪$moduleLoading) {
90  return $this;
91  }
92  $target = clone $this;
93  $target->moduleLoading = ‪$moduleLoading;
94  return $target;
95  }
96 
101  public function ‪withRootLine(array ‪$rootLine): self
102  {
103  if ($this->rootLine === ‪$rootLine) {
104  return $this;
105  }
106  $target = clone $this;
107  $target->rootLine = ‪$rootLine;
108  return $this;
109  }
110 
115  public function ‪withLanguage(int $language): self
116  {
117  if ($this->languageId === $language) {
118  return $this;
119  }
120  $target = clone $this;
121  $target->languageId = $language;
122  return $target;
123  }
124 
129  public function ‪withSection(string ‪$section): self
130  {
131  if ($this->section === ‪$section) {
132  return $this;
133  }
134  $target = clone $this;
135  $target->section = ‪$section;
136  return $target;
137  }
138 
144  {
145  if (is_array(‪$additionalQueryParameters)) {
146  $additionalQueryParams = ‪$additionalQueryParameters;
147  } else {
148  $additionalQueryParams = [];
149  parse_str(‪$additionalQueryParameters, $additionalQueryParams);
150  }
152  if (isset($additionalQueryParams['_language'])) {
153  ‪$languageId = (int)$additionalQueryParams['_language'];
154  unset($additionalQueryParams['_language']);
155  }
156  // No change
157  if ($this->languageId === ‪$languageId && $additionalQueryParams === $this->additionalQueryParameters) {
158  return $this;
159  }
160 
161  $target = clone $this;
162  $target->additionalQueryParameters = $additionalQueryParams;
163  $target->languageId = ‪$languageId;
164  return $target;
165  }
166 
170  public function ‪buildUri(array $options = null, ‪Context ‪$context = null): ?UriInterface
171  {
172  $eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class);
173  try {
175  $this->pageId,
176  $this->languageId,
177  $this->rootLine,
178  $this->section,
179  $this->additionalQueryParameters,
180  ‪$context ?? $this->context,
181  $this->‪enrichOptions($options)
182  );
183  $eventDispatcher->dispatch($event);
184 
185  // If there hasn't been a custom preview URI set by an event listener, generate it.
186  if ($event->getPreviewUri() === null) {
187  $permissionClause = ‪$GLOBALS['BE_USER']->getPagePermsClause(‪Permission::PAGE_SHOW);
188  $pageInfo = BackendUtility::readPageAccess($event->getPageId(), $permissionClause) ?: [];
189  // Check if the page (= its rootline) has a site attached, otherwise just keep the URI as is
190  if ($event->getRootline() === []) {
191  $event->setRootline(BackendUtility::BEgetRootLine($event->getPageId()));
192  }
193  // prepare custom context for link generation (to allow for example time based previews)
194  $event->setAdditionalQueryParameters(
195  array_replace_recursive(
196  $event->getAdditionalQueryParameters(),
197  $this->getAdditionalQueryParametersForAccessRestrictedPages($pageInfo, $event->getContext(), $event->getRootline())
198  )
199  );
200 
201  // Build the URI with a site as prefix, if configured
202  $siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
203  try {
204  $site = $siteFinder->getSiteByPageId($event->getPageId(), $event->getRootline());
205  } catch (‪SiteNotFoundException $e) {
206  throw new ‪UnableToLinkToPageException('The page ' . $event->getPageId() . ' had no proper connection to a site, no link could be built.', 1651499353);
207  }
208  try {
209  $previewRouteParameters = $event->getAdditionalQueryParameters();
210  // Reassemble encapsulated language id into route parameters to get proper localized page preview
211  // uri for non-default languages.
212  if ($event->getLanguageId() > 0) {
213  $previewRouteParameters['_language'] = $site->getLanguageById($event->getLanguageId());
214  }
215  $event->setPreviewUri(
216  $site->getRouter($event->getContext())->generateUri(
217  $event->getPageId(),
218  $previewRouteParameters,
219  $event->getSection(),
221  )
222  );
223  } catch (\InvalidArgumentException | ‪InvalidRouteArgumentsException $e) {
224  throw new ‪UnableToLinkToPageException(sprintf('The link to the page with ID "%d" could not be generated: %s', $event->getPageId(), $e->getMessage()), 1651499354, $e);
225  }
226  }
227 
229  $event->getPreviewUri(),
230  $event->getPageId(),
231  $event->getLanguageId(),
232  $event->getRootline(),
233  $event->getSection(),
234  $event->getAdditionalQueryParameters(),
235  $event->getContext(),
236  $event->getOptions(),
237  );
238  $eventDispatcher->dispatch($event);
239 
240  return $event->getPreviewUri();
241  } catch (‪UnableToLinkToPageException $e) {
242  return null;
243  }
244  }
245 
252  public function ‪buildDispatcherDataAttributes(array $options = null): ?array
253  {
254  if (null === ($attributes = $this->‪buildAttributes($options))) {
255  return null;
256  }
257  $this->‪loadActionDispatcher();
258  return $this->‪prefixAttributeNames('dispatch-', $attributes);
259  }
260 
267  public function ‪buildDispatcherAttributes(array $options = null): ?array
268  {
269  if (null === ($attributes = $this->‪buildAttributes($options))) {
270  return null;
271  }
272  $this->‪loadActionDispatcher();
273  return $this->‪prefixAttributeNames('data-dispatch-', $attributes);
274  }
275 
281  public function ‪serializeDispatcherAttributes(array $options = null): ?string
282  {
283  if (null === ($attributes = $this->‪buildDispatcherAttributes($options))) {
284  return null;
285  }
286  return ' ' . GeneralUtility::implodeAttributes($attributes, true);
287  }
288 
295  public function ‪buildImmediateActionElement(array $options = null): ?string
296  {
297  if (null === ($attributes = $this->‪buildAttributes($options))) {
298  return null;
299  }
301  return sprintf(
302  // `<typo3-immediate-action action="TYPO3.WindowManager.localOpen" args="[...]">`
303  '<typo3-immediate-action %s></typo3-immediate-action>',
304  GeneralUtility::implodeAttributes($attributes, true)
305  );
306  }
307 
308  protected function ‪buildAttributes(array $options = null): ?array
309  {
310  $options = $this->‪enrichOptions($options);
311  if (null === ($uri = $this->‪buildUri($options))) {
312  return null;
313  }
314  ‪$args = [
315  // target URI
316  (string)$uri,
317  // whether to switch focus to that window
318  $options[self::OPTION_SWITCH_FOCUS],
319  // name of the window instance for JavaScript references
320  $options[self::OPTION_WINDOW_NAME],
321  ];
322  if (isset($options[self::OPTION_WINDOW_FEATURES])) {
323  // optional window features (e.g. 'width=500,height=300')
325  }
326  return [
327  'action' => $options[‪self::OPTION_WINDOW_SCOPE] === self::OPTION_WINDOW_SCOPE_GLOBAL
328  ? 'TYPO3.WindowManager.globalOpen'
329  : 'TYPO3.WindowManager.localOpen',
330  'args' => json_encode(‪$args),
331  ];
332  }
333 
342  protected function ‪enrichOptions(array $options = null): array
343  {
344  return array_merge(
345  [
346  self::OPTION_SWITCH_FOCUS => null,
347  // 'newTYPO3frontendWindow' was used in BackendUtility::viewOnClick
348  self::OPTION_WINDOW_NAME => 'newTYPO3frontendWindow',
349  self::OPTION_WINDOW_SCOPE => self::OPTION_WINDOW_SCOPE_LOCAL,
350  ],
351  $options ?? []
352  );
353  }
354 
355  protected function ‪loadActionDispatcher(): void
356  {
357  if (!$this->moduleLoading) {
358  return;
359  }
360  $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
361  $pageRenderer->loadJavaScriptModule('@typo3/backend/action-dispatcher.js');
362  }
363 
364  protected function ‪loadImmediateActionElement(): void
365  {
366  if (!$this->moduleLoading) {
367  return;
368  }
369  $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
370  $pageRenderer->loadJavaScriptModule('@typo3/backend/element/immediate-action-element.js');
371  }
372 
373  protected function ‪prefixAttributeNames(string $prefix, array $attributes): array
374  {
375  $attributeNames = array_map(
376  static function (string $name) use ($prefix): string {
377  return $prefix . $name;
378  },
379  array_keys($attributes)
380  );
381  return array_combine(
382  $attributeNames,
383  array_values($attributes)
384  );
385  }
386 
391  {
392  if ($pageInfo === []) {
393  return [];
394  }
395  // Initialize access restriction values from current page
396  $access = [
397  'fe_group' => (string)($pageInfo['fe_group'] ?? ''),
398  'starttime' => (int)($pageInfo['starttime'] ?? 0),
399  'endtime' => (int)($pageInfo['endtime'] ?? 0),
400  ];
401  // Only check rootline if the current page has not set extendToSubpages itself
402  if (!(bool)($pageInfo['extendToSubpages'] ?? false)) {
403  // remove the current page from the rootline
404  array_shift(‪$rootLine);
405  foreach (‪$rootLine as $page) {
406  // Skip root node and pages which do not define extendToSubpages
407  if ((int)($page['uid'] ?? 0) === 0 || !(bool)($page['extendToSubpages'] ?? false)) {
408  continue;
409  }
410  $access['fe_group'] = (string)($page['fe_group'] ?? '');
411  $access['starttime'] = (int)($page['starttime'] ?? 0);
412  $access['endtime'] = (int)($page['endtime'] ?? 0);
413  // Stop as soon as a page in the rootline has extendToSubpages set
414  break;
415  }
416  }
418  if ((int)$access['fe_group'] === -2) {
419  // -2 means "show at any login". We simulate first available fe_group.
420  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
421  ->getQueryBuilderForTable('fe_groups');
422  $queryBuilder->getRestrictions()
423  ->removeAll()
424  ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
425  ->add(GeneralUtility::makeInstance(HiddenRestriction::class));
426 
427  $activeFeGroupId = $queryBuilder->select('uid')
428  ->from('fe_groups')
429  ->executeQuery()
430  ->fetchOne();
431 
432  if ($activeFeGroupId) {
433  ‪$additionalQueryParameters['ADMCMD_simUser'] = $activeFeGroupId;
434  }
435  } elseif (!empty($access['fe_group'])) {
436  ‪$additionalQueryParameters['ADMCMD_simUser'] = $access['fe_group'];
437  }
438  if ($access['starttime'] > ‪$GLOBALS['EXEC_TIME']) {
439  // simulate access time to ensure PageRepository will find the page and in turn PageRouter will generate
440  // a URL for it
441  $dateAspect = GeneralUtility::makeInstance(
442  DateTimeAspect::class,
443  (new \DateTimeImmutable())->setTimestamp($access['starttime'])
444  );
445  ‪$context->‪setAspect('date', $dateAspect);
446  ‪$additionalQueryParameters['ADMCMD_simTime'] = $access['starttime'];
447  }
448  if ($access['endtime'] < ‪$GLOBALS['EXEC_TIME'] && $access['endtime'] !== 0) {
449  // Set access time to page's endtime subtracted one second to ensure PageRepository will find the page and
450  // in turn PageRouter will generate a URL for it
451  $dateAspect = GeneralUtility::makeInstance(
452  DateTimeAspect::class,
453  (new \DateTimeImmutable())->setTimestamp($access['endtime'] - 1)
454  );
455  ‪$context->‪setAspect('date', $dateAspect);
456  ‪$additionalQueryParameters['ADMCMD_simTime'] = ($access['endtime'] - 1);
457  }
459  }
460 }
‪TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction
Definition: HiddenRestriction.php:27
‪TYPO3\CMS\Core\Context\VisibilityAspect
Definition: VisibilityAspect.php:31
‪TYPO3\CMS\Backend\Routing\PreviewUriBuilder\$rootLine
‪array $rootLine
Definition: PreviewUriBuilder.php:55
‪TYPO3\CMS\Core\Routing\RouterInterface
Definition: RouterInterface.php:28
‪TYPO3\CMS\Backend\Routing\PreviewUriBuilder\$context
‪Context $context
Definition: PreviewUriBuilder.php:59
‪TYPO3\CMS\Backend\Routing\Event\AfterPagePreviewUriGeneratedEvent
Definition: AfterPagePreviewUriGeneratedEvent.php:28
‪TYPO3\CMS\Backend\Routing\PreviewUriBuilder\OPTION_WINDOW_FEATURES
‪const OPTION_WINDOW_FEATURES
Definition: PreviewUriBuilder.php:47
‪TYPO3\CMS\Backend\Routing\PreviewUriBuilder\withModuleLoading
‪static withModuleLoading(bool $moduleLoading)
Definition: PreviewUriBuilder.php:87
‪TYPO3\CMS\Backend\Routing\PreviewUriBuilder\$additionalQueryParameters
‪array $additionalQueryParameters
Definition: PreviewUriBuilder.php:57
‪TYPO3\CMS\Core\Exception\SiteNotFoundException
Definition: SiteNotFoundException.php:25
‪TYPO3\CMS\Core\Site\SiteFinder
Definition: SiteFinder.php:31
‪TYPO3\CMS\Backend\Routing\PreviewUriBuilder\serializeDispatcherAttributes
‪serializeDispatcherAttributes(array $options=null)
Definition: PreviewUriBuilder.php:281
‪TYPO3\CMS\Backend\Routing\PreviewUriBuilder\buildAttributes
‪buildAttributes(array $options=null)
Definition: PreviewUriBuilder.php:308
‪TYPO3\CMS\Core\Context\Context
Definition: Context.php:54
‪TYPO3\CMS\Backend\Routing\PreviewUriBuilder\loadActionDispatcher
‪loadActionDispatcher()
Definition: PreviewUriBuilder.php:355
‪TYPO3\CMS\Core\Type\Bitmask\Permission
Definition: Permission.php:26
‪TYPO3\CMS\Backend\Routing\PreviewUriBuilder\buildImmediateActionElement
‪buildImmediateActionElement(array $options=null)
Definition: PreviewUriBuilder.php:295
‪TYPO3\CMS\Backend\Routing\PreviewUriBuilder\OPTION_WINDOW_SCOPE_LOCAL
‪const OPTION_WINDOW_SCOPE_LOCAL
Definition: PreviewUriBuilder.php:50
‪TYPO3\CMS\Core\Context\Context\setAspect
‪setAspect(string $name, AspectInterface $aspect)
Definition: Context.php:131
‪TYPO3\CMS\Backend\Routing\PreviewUriBuilder\withLanguage
‪static withLanguage(int $language)
Definition: PreviewUriBuilder.php:115
‪TYPO3\CMS\Backend\Routing\PreviewUriBuilder\prefixAttributeNames
‪prefixAttributeNames(string $prefix, array $attributes)
Definition: PreviewUriBuilder.php:373
‪TYPO3\CMS\Backend\Routing\PreviewUriBuilder\enrichOptions
‪enrichOptions(array $options=null)
Definition: PreviewUriBuilder.php:342
‪TYPO3\CMS\Core\Page\PageRenderer
Definition: PageRenderer.php:44
‪TYPO3\CMS\Backend\Routing\PreviewUriBuilder\buildUri
‪buildUri(array $options=null, Context $context=null)
Definition: PreviewUriBuilder.php:170
‪TYPO3\CMS\Backend\Routing\PreviewUriBuilder\OPTION_WINDOW_NAME
‪const OPTION_WINDOW_NAME
Definition: PreviewUriBuilder.php:46
‪TYPO3\CMS\Backend\Routing\PreviewUriBuilder\$pageId
‪int $pageId
Definition: PreviewUriBuilder.php:53
‪TYPO3\CMS\Backend\Routing\PreviewUriBuilder\withAdditionalQueryParameters
‪static withAdditionalQueryParameters(array|string $additionalQueryParameters)
Definition: PreviewUriBuilder.php:143
‪TYPO3\CMS\Backend\Routing\PreviewUriBuilder\OPTION_WINDOW_SCOPE_GLOBAL
‪const OPTION_WINDOW_SCOPE_GLOBAL
Definition: PreviewUriBuilder.php:51
‪TYPO3\CMS\Backend\Routing\PreviewUriBuilder\create
‪static static create(int $pageId)
Definition: PreviewUriBuilder.php:65
‪TYPO3\CMS\Core\Routing\InvalidRouteArgumentsException
Definition: InvalidRouteArgumentsException.php:25
‪TYPO3\CMS\Core\Type\Bitmask\Permission\PAGE_SHOW
‪const PAGE_SHOW
Definition: Permission.php:35
‪TYPO3\CMS\Backend\Routing\PreviewUriBuilder\$languageId
‪int $languageId
Definition: PreviewUriBuilder.php:54
‪$args
‪$args
Definition: validateRstFiles.php:258
‪TYPO3\CMS\Backend\Routing\PreviewUriBuilder\getAdditionalQueryParametersForAccessRestrictedPages
‪getAdditionalQueryParametersForAccessRestrictedPages(array $pageInfo, Context $context, array $rootLine)
Definition: PreviewUriBuilder.php:390
‪TYPO3\CMS\Backend\Routing\PreviewUriBuilder\loadImmediateActionElement
‪loadImmediateActionElement()
Definition: PreviewUriBuilder.php:364
‪TYPO3\CMS\Backend\Routing\Event\BeforePagePreviewUriGeneratedEvent
Definition: BeforePagePreviewUriGeneratedEvent.php:29
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Backend\Routing\PreviewUriBuilder\buildDispatcherDataAttributes
‪buildDispatcherDataAttributes(array $options=null)
Definition: PreviewUriBuilder.php:252
‪TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction
Definition: DeletedRestriction.php:28
‪TYPO3\CMS\Backend\Routing\PreviewUriBuilder
Definition: PreviewUriBuilder.php:44
‪TYPO3\CMS\Backend\Routing\PreviewUriBuilder\$section
‪string $section
Definition: PreviewUriBuilder.php:56
‪TYPO3\CMS\Backend\Routing\PreviewUriBuilder\$moduleLoading
‪bool $moduleLoading
Definition: PreviewUriBuilder.php:58
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Core\Routing\RouterInterface\ABSOLUTE_URL
‪const ABSOLUTE_URL
Definition: RouterInterface.php:32
‪TYPO3\CMS\Backend\Routing\PreviewUriBuilder\OPTION_WINDOW_SCOPE
‪const OPTION_WINDOW_SCOPE
Definition: PreviewUriBuilder.php:48
‪TYPO3\CMS\Backend\Routing\PreviewUriBuilder\OPTION_SWITCH_FOCUS
‪const OPTION_SWITCH_FOCUS
Definition: PreviewUriBuilder.php:45
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Backend\Routing\PreviewUriBuilder\withRootLine
‪static withRootLine(array $rootLine)
Definition: PreviewUriBuilder.php:101
‪TYPO3\CMS\Backend\Routing\PreviewUriBuilder\buildDispatcherAttributes
‪buildDispatcherAttributes(array $options=null)
Definition: PreviewUriBuilder.php:267
‪TYPO3\CMS\Backend\Routing\PreviewUriBuilder\withSection
‪static withSection(string $section)
Definition: PreviewUriBuilder.php:129
‪TYPO3\CMS\Core\Context\DateTimeAspect
Definition: DateTimeAspect.php:35
‪TYPO3\CMS\Backend\Routing
‪TYPO3\CMS\Backend\Routing\PreviewUriBuilder\__construct
‪__construct(int $pageId)
Definition: PreviewUriBuilder.php:73