*/ protected array $linkHandlers = []; /** * All parts of the current link. * Comprised of url information and additional link parameters. * * @var array */ protected array $currentLinkParts = []; /** * Link handler responsible for the current active link */ protected ?LinkHandlerInterface $currentLinkHandler = null; /** * The ID of the currently active link handler */ protected string $currentLinkHandlerId; /** * Link handler to be displayed */ protected ?LinkHandlerInterface $displayedLinkHandler = null; /** * The ID of the displayed link handler * This is read from the 'act' GET parameter */ protected string $displayedLinkHandlerId = ''; /** * List of available link attribute fields * * @var string[] */ protected array $linkAttributeFields = []; /** * Values of the link attributes * * @var string[] */ protected array $linkAttributeValues = []; protected array $parameters; protected DependencyOrderingService $dependencyOrderingService; protected PageRenderer $pageRenderer; protected UriBuilder $uriBuilder; protected ExtensionConfiguration $extensionConfiguration; protected BackendViewFactory $backendViewFactory; protected EventDispatcherInterface $eventDispatcher; public function injectDependencyOrderingService(DependencyOrderingService $dependencyOrderingService): void { $this->dependencyOrderingService = $dependencyOrderingService; } public function injectPageRenderer(PageRenderer $pageRenderer): void { $this->pageRenderer = $pageRenderer; } public function injectUriBuilder(UriBuilder $uriBuilder): void { $this->uriBuilder = $uriBuilder; } public function injectExtensionConfiguration(ExtensionConfiguration $extensionConfiguration): void { $this->extensionConfiguration = $extensionConfiguration; } public function injectBackendViewFactory(BackendViewFactory $backendViewFactory): void { $this->backendViewFactory = $backendViewFactory; } public function injectEventDispatcher(EventDispatcherInterface $eventDispatcher): void { $this->eventDispatcher = $eventDispatcher; } abstract public function getConfiguration(): array; abstract protected function initDocumentTemplate(): void; abstract protected function getCurrentPageId(): int; /** * Injects the request object for the current request or subrequest * As this controller goes only through the main() method, it is rather simple for now * * @param ServerRequestInterface $request the current request * @return ResponseInterface the response with the content */ public function mainAction(ServerRequestInterface $request): ResponseInterface { $this->setUpBasicPageRendererForBackend($this->pageRenderer, $this->extensionConfiguration, $request, $this->getLanguageService()); $this->pageRenderer->addInlineLanguageLabelFile('EXT:core/Resources/Private/Language/locallang_misc.xlf'); $this->pageRenderer->addInlineLanguageLabelFile('EXT:core/Resources/Private/Language/locallang_core.xlf'); $this->initVariables($request); $this->loadLinkHandlers(); $this->initCurrentUrl(); $menuData = $this->buildMenuArray($request); if ($this->displayedLinkHandler instanceof LinkHandlerViewProviderInterface) { $view = $this->displayedLinkHandler->createView($this->backendViewFactory, $request); } else { $view = $this->backendViewFactory->create($request, ['typo3/cms-backend']); } if ($this->displayedLinkHandler instanceof LinkHandlerVariableProviderInterface) { $this->displayedLinkHandler->initializeVariables($request); } $renderLinkAttributeFields = $this->renderLinkAttributeFields($view); if (!empty($this->currentLinkParts)) { $this->renderCurrentUrl($view); } if (method_exists($this->displayedLinkHandler, 'setView')) { $this->displayedLinkHandler->setView($view); } $view->assignMultiple([ 'initialNavigationWidth' => $this->getBackendUser()->uc['selector']['navigation']['width'] ?? 250, 'menuItems' => $menuData, 'linkAttributes' => $renderLinkAttributeFields, 'contentOnly' => $request->getQueryParams()['contentOnly'] ?? false, ]); $content = $this->displayedLinkHandler->render($request); if (empty($content)) { // @todo: b/w compat layer for link handler that don't render full view but return empty // string instead. This case is unfortunate and should be removed if it gives // headaches at some point. If so, above method_exists($this->displayedLinkHandler, 'setView') // should be removed and setView() method should be made mandatory, or the entire // construct should be refactored a bit. $content = $view->render(); } $this->initDocumentTemplate(); $this->pageRenderer->setTitle($this->getLanguageService()->sL( 'LLL:EXT:backend/Resources/Private/Language/locallang_browse_links.xlf:linkBrowser' )); if ($request->getQueryParams()['contentOnly'] ?? false) { return new HtmlResponse($content); } $this->pageRenderer->setBodyContent('getBodyTagAttributes(), true, true) . '>' . $content); return $this->pageRenderer->renderResponse($request); } /** * @return array{act: string, P: array} Array of parameters which have to be added to URLs */ public function getUrlParameters(?array $overrides = null): array { return [ 'act' => $overrides['act'] ?? $this->displayedLinkHandlerId, 'P' => $overrides['P'] ?? $this->parameters, ]; } public function getParameters(): array { return $this->parameters; } protected function initVariables(ServerRequestInterface $request): void { $queryParams = $request->getQueryParams(); $this->displayedLinkHandlerId = $queryParams['act'] ?? ''; $this->parameters = $queryParams['P'] ?? []; $this->linkAttributeValues = $queryParams['linkAttributes'] ?? []; $pageTsConfig = BackendUtility::getPagesTSconfig((int)($this->parameters['pid'] ?? 0)); $handlerId = $this->displayedLinkHandlerId ?: 'page'; if (empty($this->linkAttributeValues['target'])) { $defaultTarget = $pageTsConfig['TCEMAIN.']['linkHandler.'][$handlerId . '.']['target.']['default'] ?? $pageTsConfig['TCEMAIN.']['linkHandler.']['properties.']['target.']['default'] ?? ''; if (!empty($defaultTarget)) { $this->linkAttributeValues['target'] = $defaultTarget; } } if (empty($this->linkAttributeValues['class'])) { $defaultCssClass = $pageTsConfig['TCEMAIN.']['linkHandler.'][$handlerId . '.']['cssClass.']['default'] ?? $pageTsConfig['TCEMAIN.']['linkHandler.']['properties.']['cssClass.']['default'] ?? ''; if (!empty($defaultCssClass)) { $this->linkAttributeValues['class'] = $defaultCssClass; } } } /** * @throws \UnexpectedValueException */ protected function loadLinkHandlers(): void { $linkHandlers = $this->getLinkHandlers(); if (empty($linkHandlers)) { throw new \UnexpectedValueException('No link handlers are configured. Check page TSconfig TCEMAIN.linkHandler.', 1442787911); } $lang = $this->getLanguageService(); foreach ($linkHandlers as $identifier => $configuration) { $identifier = rtrim($identifier, '.'); if ($identifier === 'properties') { continue; } if (empty($configuration['handler'])) { throw new \UnexpectedValueException(sprintf('Missing handler for link handler "%1$s", check page TSconfig TCEMAIN.linkHandler.%1$s.handler', $identifier), 1494579849); } /** @var LinkHandlerInterface $handler */ $handler = GeneralUtility::makeInstance($configuration['handler']); $handler->initialize( $this, $identifier, $configuration['configuration.'] ?? [] ); $label = !empty($configuration['label']) ? $lang->sL($configuration['label']) : ''; $label = $label ?: $lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:error.linkHandlerTitleMissing'); $this->linkHandlers[$identifier] = [ 'handlerInstance' => $handler, 'label' => $label, 'displayBefore' => isset($configuration['displayBefore']) ? GeneralUtility::trimExplode(',', $configuration['displayBefore']) : [], 'displayAfter' => isset($configuration['displayAfter']) ? GeneralUtility::trimExplode(',', $configuration['displayAfter']) : [], 'scanBefore' => isset($configuration['scanBefore']) ? GeneralUtility::trimExplode(',', $configuration['scanBefore']) : [], 'scanAfter' => isset($configuration['scanAfter']) ? GeneralUtility::trimExplode(',', $configuration['scanAfter']) : [], 'addParams' => $configuration['addParams'] ?? '', ]; } } /** * Reads the configured link handlers from page TSconfig * * @return array */ protected function getLinkHandlers(): array { $linkHandlers = (array)(BackendUtility::getPagesTSconfig($this->getCurrentPageId())['TCEMAIN.']['linkHandler.'] ?? []); return $this->eventDispatcher ->dispatch(new ModifyLinkHandlersEvent($linkHandlers, $this->currentLinkParts)) ->getLinkHandlers(); } /** * Initialize $this->currentLinkParts and $this->currentLinkHandler */ protected function initCurrentUrl(): void { if (empty($this->currentLinkParts)) { return; } $orderedHandlers = $this->dependencyOrderingService->orderByDependencies($this->linkHandlers, 'scanBefore', 'scanAfter'); // find responsible handler for current link foreach ($orderedHandlers as $key => $configuration) { /** @var LinkHandlerInterface $handler */ $handler = $configuration['handlerInstance']; if ($handler->canHandleLink($this->currentLinkParts)) { $this->currentLinkHandler = $handler; $this->currentLinkHandlerId = $key; break; } } // reset the link if we have no handler for it if (!$this->currentLinkHandler) { $this->currentLinkParts = []; } // overwrite any preexisting foreach ($this->currentLinkParts as $key => $part) { if ($key !== 'url') { $this->linkAttributeValues[$key] = $part; } } } /** * Add the currently set Link URL to the view */ protected function renderCurrentUrl(ViewInterface $view): void { $view->assign('currentLink', $this->currentLinkHandler->formatCurrentUrl()); } /** * Returns an array definition of the top menu * * @return array[] */ protected function buildMenuArray(ServerRequestInterface $request): array { $allowedItems = $this->getAllowedItems(); if ($this->displayedLinkHandlerId && !in_array($this->displayedLinkHandlerId, $allowedItems, true)) { $this->displayedLinkHandlerId = ''; } $allowedHandlers = array_flip($allowedItems); $menuDef = []; foreach ($this->linkHandlers as $identifier => $configuration) { if (!isset($allowedHandlers[$identifier])) { continue; } /** @var LinkHandlerInterface $handlerInstance */ $handlerInstance = $configuration['handlerInstance']; $isActive = $this->displayedLinkHandlerId === $identifier || (!$this->displayedLinkHandlerId && $handlerInstance === $this->currentLinkHandler); if ($isActive) { $this->displayedLinkHandler = $handlerInstance; if (!$this->displayedLinkHandlerId) { $this->displayedLinkHandlerId = $this->currentLinkHandlerId; } } $menuDef[$identifier] = [ 'isActive' => $isActive, 'label' => $configuration['label'], 'url' => $this->uriBuilder->buildUriFromRequest($request, $this->getUrlParameters(['act' => $identifier])), 'addParams' => $configuration['addParams'] ?? '', 'before' => $configuration['displayBefore'], 'after' => $configuration['displayAfter'], ]; } $menuDef = $this->dependencyOrderingService->orderByDependencies($menuDef); // if there is no active tab if (!$this->displayedLinkHandler) { // empty the current link $this->currentLinkParts = []; $this->currentLinkHandler = null; // select first tab $this->displayedLinkHandlerId = (string)array_key_first($menuDef); $this->displayedLinkHandler = $this->linkHandlers[$this->displayedLinkHandlerId]['handlerInstance']; $menuDef[$this->displayedLinkHandlerId]['isActive'] = true; } return $menuDef; } /** * @return string[] */ protected function getAllowedItems(): array { $allowedItems = $this->eventDispatcher ->dispatch(new ModifyAllowedItemsEvent(array_keys($this->linkHandlers), $this->currentLinkParts)) ->getAllowedItems(); if (isset($this->parameters['params']['allowedTypes'])) { $allowedItems = array_intersect($allowedItems, GeneralUtility::trimExplode(',', $this->parameters['params']['allowedTypes'], true)); } elseif (isset($this->parameters['params']['blindLinkOptions'])) { // @todo Deprecate this option $allowedItems = array_diff($allowedItems, GeneralUtility::trimExplode(',', $this->parameters['params']['blindLinkOptions'], true)); } return $allowedItems; } /** * @return string[] */ protected function getAllowedLinkAttributes(): array { $allowedLinkAttributes = $this->displayedLinkHandler->getLinkAttributes(); if (isset($this->parameters['params']['allowedOptions'])) { $allowedLinkAttributes = array_intersect($allowedLinkAttributes, GeneralUtility::trimExplode(',', $this->parameters['params']['allowedOptions'], true)); } elseif (isset($this->parameters['params']['blindLinkFields'])) { // @todo Deprecate this option $allowedLinkAttributes = array_diff($allowedLinkAttributes, GeneralUtility::trimExplode(',', $this->parameters['params']['blindLinkFields'], true)); } return $allowedLinkAttributes; } /** * Renders the link attributes for the selected link handler */ protected function renderLinkAttributeFields(ViewInterface $view): string { $fieldRenderingDefinitions = $this->getLinkAttributeFieldDefinitions(); $fieldRenderingDefinitions = $this->displayedLinkHandler->modifyLinkAttributes($fieldRenderingDefinitions); $this->linkAttributeFields = $this->getAllowedLinkAttributes(); $content = ''; foreach ($this->linkAttributeFields as $attribute) { $content .= $fieldRenderingDefinitions[$attribute] ?? ''; } $view->assign('allowedLinkAttributes', array_combine($this->linkAttributeFields, $this->linkAttributeFields)); // add update button if appropriate if (!empty($this->currentLinkParts) && $this->displayedLinkHandler === $this->currentLinkHandler && $this->currentLinkHandler->isUpdateSupported()) { $view->assign('showUpdateParametersButton', true); } return $content; } /** * Create an array of link attribute field rendering definitions * * @return string[] */ protected function getLinkAttributeFieldDefinitions(): array { $lang = $this->getLanguageService(); $fieldRenderingDefinitions = []; $fieldRenderingDefinitions['target'] = '
' . $lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_browse_links.xlf:top') . ' ' . $lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_browse_links.xlf:newWindow') . '
'; $fieldRenderingDefinitions['title'] = '
'; $fieldRenderingDefinitions['class'] = '
'; $fieldRenderingDefinitions['params'] = '
'; $fieldRenderingDefinitions['rel'] = '
'; $fieldRenderingDefinitions['download'] = '
'; return $fieldRenderingDefinitions; } /** * @return string[] Array of body-tag attributes */ protected function getBodyTagAttributes(): array { $attributes = $this->displayedLinkHandler->getBodyTagAttributes(); return array_merge( $attributes, [ 'data-linkbrowser-parameters' => json_encode($this->parameters) ?: '', 'data-linkbrowser-attribute-fields' => json_encode(array_values($this->linkAttributeFields)) ?: '', ] ); } protected function getDisplayedLinkHandlerId(): string { return $this->displayedLinkHandlerId; } protected function getLanguageService(): LanguageService { return $GLOBALS['LANG']; } protected function getBackendUser(): BackendUserAuthentication { return $GLOBALS['BE_USER']; } }