‪TYPO3CMS  ‪main
AbstractLinkBrowserController.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\ResponseInterface;
22 use Psr\Http\Message\ServerRequestInterface;
30 use TYPO3\CMS\Backend\Utility\BackendUtility;
40 
46 abstract class AbstractLinkBrowserController
47 {
48  use PageRendererBackendSetupTrait;
49 
53  protected array $linkHandlers = [];
54 
61  protected array $currentLinkParts = [];
62 
66  protected ?LinkHandlerInterface $currentLinkHandler = null;
67 
71  protected string $currentLinkHandlerId;
72 
76  protected ?LinkHandlerInterface $displayedLinkHandler = null;
77 
82  protected string $displayedLinkHandlerId = '';
83 
89  protected array $linkAttributeFields = [];
90 
96  protected array $linkAttributeValues = [];
97 
98  protected array $parameters;
99 
100  protected DependencyOrderingService $dependencyOrderingService;
101  protected PageRenderer $pageRenderer;
102  protected UriBuilder $uriBuilder;
103  protected ExtensionConfiguration $extensionConfiguration;
104  protected BackendViewFactory $backendViewFactory;
105  protected EventDispatcherInterface $eventDispatcher;
106 
107  public function injectDependencyOrderingService(DependencyOrderingService $dependencyOrderingService): void
108  {
109  $this->dependencyOrderingService = $dependencyOrderingService;
110  }
111 
112  public function injectPageRenderer(PageRenderer $pageRenderer): void
113  {
114  $this->pageRenderer = $pageRenderer;
115  }
116 
117  public function injectUriBuilder(UriBuilder $uriBuilder): void
118  {
119  $this->uriBuilder = $uriBuilder;
120  }
121 
122  public function injectExtensionConfiguration(ExtensionConfiguration $extensionConfiguration): void
123  {
124  $this->extensionConfiguration = $extensionConfiguration;
125  }
126 
127  public function injectBackendViewFactory(BackendViewFactory $backendViewFactory): void
128  {
129  $this->backendViewFactory = $backendViewFactory;
130  }
131 
132  public function injectEventDispatcher(EventDispatcherInterface $eventDispatcher): void
133  {
134  $this->eventDispatcher = $eventDispatcher;
135  }
136 
137  abstract public function getConfiguration(): array;
138 
139  abstract protected function initDocumentTemplate(): void;
140 
141  abstract protected function getCurrentPageId(): int;
142 
150  public function mainAction(ServerRequestInterface $request): ResponseInterface
151  {
152  $this->setUpBasicPageRendererForBackend($this->pageRenderer, $this->extensionConfiguration, $request, $this->getLanguageService());
153  $this->pageRenderer->addInlineLanguageLabelFile('EXT:core/Resources/Private/Language/locallang_misc.xlf');
154  $this->pageRenderer->addInlineLanguageLabelFile('EXT:core/Resources/Private/Language/locallang_core.xlf');
155 
156  $this->initVariables($request);
157  $this->loadLinkHandlers();
158  $this->initCurrentUrl();
159 
160  $menuData = $this->buildMenuArray($request);
161  if ($this->displayedLinkHandler instanceof LinkHandlerViewProviderInterface) {
162  $view = $this->displayedLinkHandler->createView($this->backendViewFactory, $request);
163  } else {
164  $view = $this->backendViewFactory->create($request, ['typo3/cms-backend']);
165  }
166  if ($this->displayedLinkHandler instanceof LinkHandlerVariableProviderInterface) {
167  $this->displayedLinkHandler->initializeVariables($request);
168  }
169  $renderLinkAttributeFields = $this->renderLinkAttributeFields($view);
170  if (!empty($this->currentLinkParts)) {
171  $this->renderCurrentUrl($view);
172  }
173  if (method_exists($this->displayedLinkHandler, 'setView')) {
174  $this->displayedLinkHandler->setView($view);
175  }
176  $view->assignMultiple([
177  'initialNavigationWidth' => $this->getBackendUser()->uc['selector']['navigation']['width'] ?? 250,
178  'menuItems' => $menuData,
179  'linkAttributes' => $renderLinkAttributeFields,
180  'contentOnly' => $request->getQueryParams()['contentOnly'] ?? false,
181  ]);
182  $content = $this->displayedLinkHandler->render($request);
183  if (empty($content)) {
184  // @todo: b/w compat layer for link handler that don't render full view but return empty
185  // string instead. This case is unfortunate and should be removed if it gives
186  // headaches at some point. If so, above method_exists($this->displayedLinkHandler, 'setView')
187  // should be removed and setView() method should be made mandatory, or the entire
188  // construct should be refactored a bit.
189  $content = $view->render();
190  }
191  $this->initDocumentTemplate();
192  $this->pageRenderer->setTitle('Link Browser');
193  if ($request->getQueryParams()['contentOnly'] ?? false) {
194  return new HtmlResponse($content);
195  }
196  $this->pageRenderer->setBodyContent('<body ' . GeneralUtility::implodeAttributes($this->getBodyTagAttributes(), true, true) . '>' . $content);
197  return $this->pageRenderer->renderResponse();
198  }
199 
203  public function getUrlParameters(array $overrides = null): array
204  {
205  return [
206  'act' => $overrides['act'] ?? $this->displayedLinkHandlerId,
207  'P' => $overrides['P'] ?? $this->parameters,
208  ];
209  }
210 
211  public function getParameters(): array
212  {
213  return $this->parameters;
214  }
215 
216  protected function initVariables(ServerRequestInterface $request): void
217  {
218  $queryParams = $request->getQueryParams();
219  $this->displayedLinkHandlerId = $queryParams['act'] ?? '';
220  $this->parameters = $queryParams['P'] ?? [];
221  $this->linkAttributeValues = $queryParams['linkAttributes'] ?? [];
222  }
223 
227  protected function loadLinkHandlers(): void
228  {
229  $linkHandlers = $this->getLinkHandlers();
230  if (empty($linkHandlers)) {
231  throw new \UnexpectedValueException('No link handlers are configured. Check page TSconfig TCEMAIN.linkHandler.', 1442787911);
232  }
233 
234  $lang = $this->getLanguageService();
235  foreach ($linkHandlers as ‪$identifier => $configuration) {
236  ‪$identifier = rtrim(‪$identifier, '.');
237 
238  if (empty($configuration['handler'])) {
239  throw new \UnexpectedValueException(sprintf('Missing handler for link handler "%1$s", check page TSconfig TCEMAIN.linkHandler.%1$s.handler', ‪$identifier), 1494579849);
240  }
241 
243  $handler = GeneralUtility::makeInstance($configuration['handler']);
244  $handler->initialize(
245  $this,
247  $configuration['configuration.'] ?? []
248  );
249 
250  $label = !empty($configuration['label']) ? $lang->sL($configuration['label']) : '';
251  $label = $label ?: $lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:error.linkHandlerTitleMissing');
252  $this->linkHandlers[‪$identifier] = [
253  'handlerInstance' => $handler,
254  'label' => htmlspecialchars($label),
255  'displayBefore' => isset($configuration['displayBefore']) ? ‪GeneralUtility::trimExplode(',', $configuration['displayBefore']) : [],
256  'displayAfter' => isset($configuration['displayAfter']) ? GeneralUtility::trimExplode(',', $configuration['displayAfter']) : [],
257  'scanBefore' => isset($configuration['scanBefore']) ? GeneralUtility::trimExplode(',', $configuration['scanBefore']) : [],
258  'scanAfter' => isset($configuration['scanAfter']) ? GeneralUtility::trimExplode(',', $configuration['scanAfter']) : [],
259  'addParams' => $configuration['addParams'] ?? '',
260  ];
261  }
262  }
263 
269  protected function getLinkHandlers(): array
270  {
271  $linkHandlers = (array)(BackendUtility::getPagesTSconfig($this->getCurrentPageId())['TCEMAIN.']['linkHandler.'] ?? []);
272  return $this->eventDispatcher
273  ->dispatch(new ModifyLinkHandlersEvent($linkHandlers, $this->currentLinkParts))
274  ->getLinkHandlers();
275  }
276 
280  protected function initCurrentUrl(): void
281  {
282  if (empty($this->currentLinkParts)) {
283  return;
284  }
285 
286  $orderedHandlers = $this->dependencyOrderingService->orderByDependencies($this->linkHandlers, 'scanBefore', 'scanAfter');
287 
288  // find responsible handler for current link
289  foreach ($orderedHandlers as $key => $configuration) {
291  $handler = $configuration['handlerInstance'];
292  if ($handler->canHandleLink($this->currentLinkParts)) {
293  $this->currentLinkHandler = $handler;
294  $this->currentLinkHandlerId = $key;
295  break;
296  }
297  }
298  // reset the link if we have no handler for it
299  if (!$this->currentLinkHandler) {
300  $this->currentLinkParts = [];
301  }
302 
303  // overwrite any preexisting
304  foreach ($this->currentLinkParts as $key => $part) {
305  if ($key !== 'url') {
306  $this->linkAttributeValues[$key] = $part;
307  }
308  }
309  }
310 
314  protected function renderCurrentUrl(ViewInterface $view): void
315  {
316  $view->assign('currentUrl', $this->currentLinkHandler->formatCurrentUrl());
317  }
318 
324  protected function buildMenuArray(ServerRequestInterface $request): array
325  {
326  $allowedItems = $this->getAllowedItems();
327  if ($this->displayedLinkHandlerId && !in_array($this->displayedLinkHandlerId, $allowedItems, true)) {
328  $this->displayedLinkHandlerId = '';
329  }
330 
331  $allowedHandlers = array_flip($allowedItems);
332  $menuDef = [];
333  foreach ($this->linkHandlers as ‪$identifier => $configuration) {
334  if (!isset($allowedHandlers[‪$identifier])) {
335  continue;
336  }
337 
339  $handlerInstance = $configuration['handlerInstance'];
340  $isActive = $this->displayedLinkHandlerId === ‪$identifier || (!$this->displayedLinkHandlerId && $handlerInstance === $this->currentLinkHandler);
341  if ($isActive) {
342  $this->displayedLinkHandler = $handlerInstance;
343  if (!$this->displayedLinkHandlerId) {
344  $this->displayedLinkHandlerId = $this->currentLinkHandlerId;
345  }
346  }
347 
348  $menuDef[‪$identifier] = [
349  'isActive' => $isActive,
350  'label' => $configuration['label'],
351  'url' => $this->uriBuilder->buildUriFromRequest($request, $this->getUrlParameters(['act' => ‪$identifier])),
352  'addParams' => $configuration['addParams'] ?? '',
353  'before' => $configuration['displayBefore'],
354  'after' => $configuration['displayAfter'],
355  ];
356  }
357 
358  $menuDef = $this->dependencyOrderingService->orderByDependencies($menuDef);
359 
360  // if there is no active tab
361  if (!$this->displayedLinkHandler) {
362  // empty the current link
363  $this->currentLinkParts = [];
364  $this->currentLinkHandler = null;
365  // select first tab
366  $this->displayedLinkHandlerId = (string)array_key_first($menuDef);
367  $this->displayedLinkHandler = $this->linkHandlers[$this->displayedLinkHandlerId]['handlerInstance'];
368  $menuDef[$this->displayedLinkHandlerId]['isActive'] = true;
369  }
370 
371  return $menuDef;
372  }
373 
377  protected function getAllowedItems(): array
378  {
379  $allowedItems = $this->eventDispatcher
380  ->dispatch(new ModifyAllowedItemsEvent(array_keys($this->linkHandlers), $this->currentLinkParts))
381  ->getAllowedItems();
382 
383  if (isset($this->parameters['params']['allowedTypes'])) {
384  $allowedItems = array_intersect($allowedItems, ‪GeneralUtility::trimExplode(',', $this->parameters['params']['allowedTypes'], true));
385  } elseif (isset($this->parameters['params']['blindLinkOptions'])) {
386  // @todo Deprecate this option
387  $allowedItems = array_diff($allowedItems, ‪GeneralUtility::trimExplode(',', $this->parameters['params']['blindLinkOptions'], true));
388  }
389 
390  return $allowedItems;
391  }
392 
396  protected function getAllowedLinkAttributes(): array
397  {
398  $allowedLinkAttributes = $this->displayedLinkHandler->getLinkAttributes();
399 
400  if (isset($this->parameters['params']['allowedOptions'])) {
401  $allowedLinkAttributes = array_intersect($allowedLinkAttributes, ‪GeneralUtility::trimExplode(',', $this->parameters['params']['allowedOptions'], true));
402  } elseif (isset($this->parameters['params']['blindLinkFields'])) {
403  // @todo Deprecate this option
404  $allowedLinkAttributes = array_diff($allowedLinkAttributes, ‪GeneralUtility::trimExplode(',', $this->parameters['params']['blindLinkFields'], true));
405  }
406 
407  return $allowedLinkAttributes;
408  }
409 
413  protected function renderLinkAttributeFields(ViewInterface $view): string
414  {
415  $fieldRenderingDefinitions = $this->getLinkAttributeFieldDefinitions();
416  $fieldRenderingDefinitions = $this->displayedLinkHandler->modifyLinkAttributes($fieldRenderingDefinitions);
417  $this->linkAttributeFields = $this->getAllowedLinkAttributes();
418  $content = '';
419  foreach ($this->linkAttributeFields as $attribute) {
420  $content .= $fieldRenderingDefinitions[$attribute] ?? '';
421  }
422  $view->assign('allowedLinkAttributes', array_combine($this->linkAttributeFields, $this->linkAttributeFields));
423 
424  // add update button if appropriate
425  if (!empty($this->currentLinkParts) && $this->displayedLinkHandler === $this->currentLinkHandler && $this->currentLinkHandler->isUpdateSupported()) {
426  $view->assign('showUpdateParametersButton', true);
427  }
428  return $content;
429  }
430 
436  protected function getLinkAttributeFieldDefinitions(): array
437  {
438  $lang = $this->getLanguageService();
439 
440  $fieldRenderingDefinitions = [];
441  $fieldRenderingDefinitions['target'] = '
442  <!-- Selecting target for link: -->
443  <div class="element-browser-form-group">
444  <label for="ltarget" class="form-label">' . htmlspecialchars($lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_browse_links.xlf:target')) . '</label>
445  <span class="input-group">
446  <input id="ltarget" type="text" name="ltarget" class="t3js-linkTarget form-control"
447  value="' . htmlspecialchars($this->linkAttributeValues['target'] ?? '') . '" />
448  <select name="ltarget_type" class="t3js-targetPreselect form-select">
449  <option value=""></option>
450  <option value="_top">' . htmlspecialchars($lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_browse_links.xlf:top')) . '</option>
451  <option value="_blank">' . htmlspecialchars($lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_browse_links.xlf:newWindow')) . '</option>
452  </select>
453  </span>
454  </div>';
455 
456  $fieldRenderingDefinitions['title'] = '
457  <!-- Selecting title for link: -->
458  <div class="element-browser-form-group">
459  <label for="ltitle" class="form-label">' . htmlspecialchars($lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_browse_links.xlf:title')) . '</label>
460  <input id="ltitle" type="text" name="ltitle" class="form-control"
461  value="' . htmlspecialchars($this->linkAttributeValues['title'] ?? '') . '" />
462  </div>';
463 
464  $fieldRenderingDefinitions['class'] = '
465  <!-- Selecting class for link: -->
466  <div class="element-browser-form-group">
467  <label for="lclass" class="form-label">
468  ' . htmlspecialchars($lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_browse_links.xlf:class')) . '
469  </label>
470  <input id="lclass" type="text" name="lclass" class="form-control"
471  value="' . htmlspecialchars($this->linkAttributeValues['class'] ?? '') . '" />
472  </div>';
473 
474  $fieldRenderingDefinitions['params'] = '
475  <!-- Selecting params for link: -->
476  <div class="element-browser-form-group">
477  <label for="lparams" class="form-label">' . htmlspecialchars($lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_browse_links.xlf:params')) . '</label>
478  <input id="lparams" type="text" name="lparams" class="form-control"
479  value="' . htmlspecialchars($this->linkAttributeValues['params'] ?? '') . '" />
480  </div>';
481 
482  return $fieldRenderingDefinitions;
483  }
484 
488  protected function getBodyTagAttributes(): array
489  {
490  $attributes = $this->displayedLinkHandler->getBodyTagAttributes();
491  return array_merge(
492  $attributes,
493  [
494  'data-linkbrowser-parameters' => json_encode($this->parameters) ?: '',
495  'data-linkbrowser-attribute-fields' => json_encode(array_values($this->linkAttributeFields)) ?: '',
496  ]
497  );
498  }
499 
500  protected function getDisplayedLinkHandlerId(): string
501  {
502  return $this->displayedLinkHandlerId;
503  }
504 
505  protected function getLanguageService(): LanguageService
506  {
507  return ‪$GLOBALS['LANG'];
508  }
509 
510  protected function getBackendUser(): BackendUserAuthentication
511  {
512  return ‪$GLOBALS['BE_USER'];
513  }
514 }
‪TYPO3\CMS\Core\View\ViewInterface
Definition: ViewInterface.php:24
‪TYPO3\CMS\Backend\View\BackendViewFactory
Definition: BackendViewFactory.php:35
‪TYPO3\CMS\Core\Configuration\ExtensionConfiguration
Definition: ExtensionConfiguration.php:47
‪TYPO3\CMS\Backend\Template\PageRendererBackendSetupTrait
Definition: PageRendererBackendSetupTrait.php:45
‪TYPO3\CMS\Core\Page\PageRenderer
Definition: PageRenderer.php:44
‪TYPO3\CMS\Backend\Routing\UriBuilder
Definition: UriBuilder.php:44
‪TYPO3\CMS\Core\Service\DependencyOrderingService
Definition: DependencyOrderingService.php:32
‪TYPO3\CMS\Core\Authentication\BackendUserAuthentication
Definition: BackendUserAuthentication.php:62
‪TYPO3\CMS\Backend\Controller\Event\ModifyAllowedItemsEvent
Definition: ModifyAllowedItemsEvent.php:24
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Localization\LanguageService
Definition: LanguageService.php:46
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Backend\Controller
Definition: AboutController.php:18
‪TYPO3\CMS\Core\Utility\GeneralUtility\trimExplode
‪static list< string > trimExplode(string $delim, string $string, bool $removeEmptyValues=false, int $limit=0)
Definition: GeneralUtility.php:822
‪TYPO3\CMS\Webhooks\Message\$identifier
‪identifier readonly string $identifier
Definition: FileAddedMessage.php:37
‪TYPO3\CMS\Core\Http\HtmlResponse
Definition: HtmlResponse.php:28