‪TYPO3CMS  ‪main
BrowseLinksController.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\Http\Message\ServerRequestInterface;
21 use TYPO3\CMS\Backend\Controller\AbstractLinkBrowserController;
29 
34 class BrowseLinksController extends AbstractLinkBrowserController
35 {
36  protected string $editorId;
37 
41  protected string $contentsLanguage;
42  protected ?‪LanguageService $contentLanguageService;
43  protected array $buttonConfig = [];
44  protected array $thisConfig = [];
45  protected array $classesAnchorDefault = [];
46  protected array $classesAnchorDefaultTarget = [];
47  protected array $classesAnchorJSOptions = [];
48  protected string $defaultLinkTarget = '';
49  protected string $siteUrl = '';
50 
51  public function __construct(
52  protected readonly ‪LinkService $linkService,
53  protected readonly ‪Richtext $richtext,
54  protected readonly ‪LanguageServiceFactory $languageServiceFactory,
55  ) {}
56 
60  public function getConfiguration(): array
61  {
62  return $this->buttonConfig;
63  }
64 
68  public function getUrlParameters(array $overrides = null): array
69  {
70  return [
71  'act' => $overrides['act'] ?? $this->displayedLinkHandlerId,
72  'P' => $overrides['P'] ?? $this->parameters,
73  'editorId' => $this->editorId,
74  'contentsLanguage' => $this->contentsLanguage,
75  ];
76  }
77 
78  protected function initDocumentTemplate(): void
79  {
80  $this->pageRenderer->getJavaScriptRenderer()->addJavaScriptModuleInstruction(
81  ‪JavaScriptModuleInstruction::create('@typo3/rte-ckeditor/rte-link-browser.js')
82  ->invoke('initialize', $this->editorId)
83  );
84  }
85 
86  protected function getCurrentPageId(): int
87  {
88  return (int)$this->parameters['pid'];
89  }
90 
91  protected function initVariables(ServerRequestInterface $request): void
92  {
93  parent::initVariables($request);
94  $queryParameters = $request->getQueryParams();
95  $this->siteUrl = $request->getAttribute('normalizedParams')->getSiteUrl();
96  $this->currentLinkParts = $queryParameters['P']['curUrl'] ?? [];
97  $this->editorId = $queryParameters['editorId'];
98  $this->contentsLanguage = $queryParameters['contentsLanguage'];
99  $this->contentLanguageService = $this->languageServiceFactory->create($this->contentsLanguage);
100  $tcaFieldConf = [
101  'enableRichtext' => true,
102  'richtextConfiguration' => $this->parameters['richtextConfigurationName'] ?: null,
103  ];
104  $this->thisConfig = $this->richtext->getConfiguration(
105  $this->parameters['table'],
106  $this->parameters['fieldName'],
107  (int)$this->parameters['pid'],
108  $this->parameters['recordType'],
109  $tcaFieldConf
110  );
111  $this->buttonConfig = $this->thisConfig['buttons']['link'] ?? [];
112  }
113 
114  protected function initCurrentUrl(): void
115  {
116  if (empty($this->currentLinkParts)) {
117  return;
118  }
119  if (!empty($this->currentLinkParts['url'])) {
120  $data = $this->linkService->resolve($this->currentLinkParts['url']);
121  $this->currentLinkParts['type'] = $data['type'];
122  unset($data['type']);
123  $this->currentLinkParts['url'] = $data;
124  if (!empty($this->currentLinkParts['url']['parameters'])) {
125  $this->currentLinkParts['params'] = '&' . $this->currentLinkParts['url']['parameters'];
126  }
127  }
128  parent::initCurrentUrl();
129  }
130 
131  protected function renderLinkAttributeFields(ViewInterface $view): string
132  {
133  // Processing the classes configuration
134  if (!empty($this->buttonConfig['properties']['class']['allowedClasses'])) {
135  $classesAnchorArray = is_array($this->buttonConfig['properties']['class']['allowedClasses'])
136  ? $this->buttonConfig['properties']['class']['allowedClasses']
137  : GeneralUtility::trimExplode(',', $this->buttonConfig['properties']['class']['allowedClasses'], true);
138  // Collecting allowed classes and configured default values
139  $classesAnchor = [
140  'all' => [],
141  ];
142 
143  if (is_array($this->thisConfig['classesAnchor'] ?? null)) {
144  foreach ($this->thisConfig['classesAnchor'] as $label => $conf) {
145  if (in_array($conf['class'] ?? null, $classesAnchorArray, true)) {
146  $classesAnchor['all'][] = $conf['class'];
147  if ($conf['type'] === $this->displayedLinkHandlerId) {
148  $classesAnchor[$conf['type']][] = $conf['class'];
149  if (($this->buttonConfig[$conf['type']]['properties']['class']['default'] ?? null) === $conf['class']) {
150  $this->classesAnchorDefault[$conf['type']] = $conf['class'];
151  if (isset($conf['target'])) {
152  $this->classesAnchorDefaultTarget[$conf['type']] = trim((string)$conf['target']);
153  }
154  }
155  }
156  }
157  }
158  }
159 
160  $linkClass = $this->linkAttributeValues['class'] ?? '';
161  if ($linkClass !== '') {
162  $currentLinkClassIsAllowed = true;
163  if (!in_array($linkClass, $classesAnchorArray, true)) {
164  // Current class is not a globally allowed class
165  $currentLinkClassIsAllowed = false;
166  }
167  if (
168  isset($classesAnchor[$this->displayedLinkHandlerId]) &&
169  in_array($linkClass, $classesAnchor['all'], true) &&
170  !in_array($linkClass, $classesAnchor[$this->displayedLinkHandlerId], true)
171  ) {
172  // Current class is limited to specific link types but not available in current link type
173  $currentLinkClassIsAllowed = false;
174  }
175 
176  if (!$currentLinkClassIsAllowed) {
177  $this->classesAnchorJSOptions[$this->displayedLinkHandlerId] ??= '';
178  // Add a dummy option that preserved the current class value (despite being invalid)
179  // in order to prevent unintentional modification of assigned classes.
180  $this->classesAnchorJSOptions[$this->displayedLinkHandlerId] .= sprintf(
181  '<option selected="selected" value="%s">%s</option>',
182  htmlspecialchars($linkClass),
183  htmlspecialchars(
184  @sprintf(
185  '[ ' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noMatchingValue') . ' ]',
186  $linkClass
187  )
188  )
189  );
190  }
191  }
192 
193  // Constructing the class selector options
194  foreach ($classesAnchorArray as $class) {
195  if (
196  !in_array($class, $classesAnchor['all'], true)
197  || (
198  isset($classesAnchor[$this->displayedLinkHandlerId])
199  && in_array($class, $classesAnchor['all'], true)
200  && is_array($classesAnchor[$this->displayedLinkHandlerId])
201  && in_array($class, $classesAnchor[$this->displayedLinkHandlerId])
202  )
203  ) {
204  $selected = '';
205  if (
206  (($this->linkAttributeValues['class'] ?? false) === $class)
207  || ($this->classesAnchorDefault[$this->displayedLinkHandlerId] ?? false) === $class
208  ) {
209  $selected = 'selected="selected"';
210  }
211  $classLabel = !empty($this->thisConfig['classes'][$class]['name'])
212  ? $this->getPageConfigLabel($this->thisConfig['classes'][$class]['name'], false)
213  : $class;
214  $classStyle = !empty($this->thisConfig['classes'][$class]['value'])
215  ? $this->thisConfig['classes'][$class]['value']
216  : '';
217 
218  $this->classesAnchorJSOptions[$this->displayedLinkHandlerId] ??= '';
219  $this->classesAnchorJSOptions[$this->displayedLinkHandlerId] .= '<option ' . $selected . ' value="' . htmlspecialchars($class) . '"'
220  . ($classStyle ? ' style="' . htmlspecialchars($classStyle) . '"' : '')
221  . '>' . htmlspecialchars($classLabel)
222  . '</option>';
223  }
224  }
225  if (
226  ($this->classesAnchorJSOptions[$this->displayedLinkHandlerId] ?? false)
227  && !(
228  ($this->buttonConfig['properties']['class']['required'] ?? false)
229  || ($this->buttonConfig[$this->displayedLinkHandlerId]['properties']['class']['required'] ?? false)
230  )
231  ) {
232  $selected = '';
233  if (!($this->linkAttributeValues['class'] ?? false) && !($this->classesAnchorDefault[$this->displayedLinkHandlerId] ?? false)) {
234  $selected = 'selected="selected"';
235  }
236  $this->classesAnchorJSOptions[$this->displayedLinkHandlerId] = '<option ' . $selected . ' value=""></option>' . $this->classesAnchorJSOptions[$this->displayedLinkHandlerId];
237  }
238  }
239  // Default target
240  $this->defaultLinkTarget = ($this->classesAnchorDefault[$this->displayedLinkHandlerId] ?? false) && ($this->classesAnchorDefaultTarget[$this->displayedLinkHandlerId] ?? false)
241  ? $this->classesAnchorDefaultTarget[$this->displayedLinkHandlerId]
242  : ($this->buttonConfig[$this->displayedLinkHandlerId]['properties']['target']['default'] ?? $this->buttonConfig['properties']['target']['default'] ?? '');
243 
244  return parent::renderLinkAttributeFields($view);
245  }
246 
254  protected function getPageConfigLabel(string $string, bool $JScharCode = true): string
255  {
256  $label = $this->getLanguageService()->sL(trim($string));
257  $label = str_replace(['\\\'', '"'], ['\'', '\\"'], $label);
258  return $JScharCode ? GeneralUtility::quoteJSvalue($label) : $label;
259  }
260 
261  protected function renderCurrentUrl(ViewInterface $view): void
262  {
263  $view->assign('removeCurrentLink', true);
264  parent::renderCurrentUrl($view);
265  }
266 
270  protected function getAllowedItems(): array
271  {
272  $allowedItems = parent::getAllowedItems();
273 
274  if (isset($this->thisConfig['allowedTypes'])) {
275  $allowedItems = array_intersect($allowedItems, GeneralUtility::trimExplode(',', $this->thisConfig['allowedTypes'], true));
276  } elseif (isset($this->thisConfig['blindLinkOptions'])) {
277  // @todo Deprecate this option
278  $allowedItems = array_diff($allowedItems, GeneralUtility::trimExplode(',', $this->thisConfig['blindLinkOptions'], true));
279  }
280 
281  if (is_array($this->buttonConfig['options'] ?? null) && !empty($this->buttonConfig['options']['removeItems'])) {
282  $allowedItems = array_diff($allowedItems, GeneralUtility::trimExplode(',', $this->buttonConfig['options']['removeItems'], true));
283  }
284 
285  return $allowedItems;
286  }
287 
291  protected function getAllowedLinkAttributes(): array
292  {
293  $allowedLinkAttributes = parent::getAllowedLinkAttributes();
294 
295  if (isset($this->thisConfig['allowedOptions'])) {
296  $allowedLinkAttributes = array_intersect($allowedLinkAttributes, GeneralUtility::trimExplode(',', $this->thisConfig['allowedOptions'], true));
297  } elseif (isset($this->thisConfig['blindLinkFields'])) {
298  // @todo Deprecate this option
299  $allowedLinkAttributes = array_diff($allowedLinkAttributes, GeneralUtility::trimExplode(',', $this->thisConfig['blindLinkFields'], true));
300  }
301 
302  return $allowedLinkAttributes;
303  }
304 
310  protected function getLinkAttributeFieldDefinitions(): array
311  {
312  $fieldRenderingDefinitions = parent::getLinkAttributeFieldDefinitions();
313  $fieldRenderingDefinitions['class'] = $this->getClassField();
314  $fieldRenderingDefinitions['target'] = $this->getTargetField();
315  $fieldRenderingDefinitions['rel'] = $this->getRelField();
316  if (empty($this->buttonConfig['queryParametersSelector']['enabled'])) {
317  unset($fieldRenderingDefinitions['params']);
318  }
319  return $fieldRenderingDefinitions;
320  }
321 
322  protected function getRelField(): string
323  {
324  if (empty($this->buttonConfig['relAttribute']['enabled'])) {
325  return '';
326  }
327 
328  $currentRel = '';
329  if ($this->displayedLinkHandler === $this->currentLinkHandler
330  && !empty($this->currentLinkParts)
331  && isset($this->linkAttributeValues['rel'])
332  && is_string($this->linkAttributeValues['rel'])
333  ) {
334  $currentRel = $this->linkAttributeValues['rel'];
335  }
336 
337  return '
338  <div class="element-browser-form-group">
339  <label for="lrel" class="form-label">' .
340  htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_browse_links.xlf:linkRelationship')) .
341  '</label>
342  <input type="text" name="lrel" class="form-control" value="' . htmlspecialchars($currentRel) . '" />
343  </div>
344  ';
345  }
346 
347  protected function getTargetField(): string
348  {
349  $targetSelectorConfig = [];
350  if (is_array($this->buttonConfig['targetSelector'] ?? null)) {
351  $targetSelectorConfig = $this->buttonConfig['targetSelector'];
352  }
353  $target = !empty($this->linkAttributeValues['target']) ? $this->linkAttributeValues['target'] : $this->defaultLinkTarget;
354  $lang = $this->getLanguageService();
355 
356  $disabled = $targetSelectorConfig['disabled'] ?? false;
357  if ($disabled) {
358  return '';
359  }
360 
361  return '
362  <div class="element-browser-form-group">
363  <label for="ltarget" class="form-label">
364  ' . htmlspecialchars($lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_browse_links.xlf:target')) . '
365  </label>
366  <span class="input-group">
367  <input id="ltarget" type="text" name="ltarget" class="t3js-linkTarget form-control"
368  value="' . htmlspecialchars($target) . '" />
369  <select name="ltarget_type" class="t3js-targetPreselect form-select">
370  <option value=""></option>
371  <option value="_top">' . htmlspecialchars($lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_browse_links.xlf:top')) . '</option>
372  <option value="_blank">' . htmlspecialchars($lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_browse_links.xlf:newWindow')) . '</option>
373  </select>
374  </span>
375  </div>';
376  }
377 
383  protected function getClassField(): string
384  {
385  if (!isset($this->classesAnchorJSOptions[$this->displayedLinkHandlerId])) {
386  return '';
387  }
388 
389  return '
390  <div class="element-browser-form-group">
391  <label for="lclass" class="form-label">
392  ' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_browse_links.xlf:class')) . '
393  </label>
394  <select id="lclass" name="lclass" class="t3js-class-selector form-select">
395  ' . $this->classesAnchorJSOptions[$this->displayedLinkHandlerId] . '
396  </select>
397  </div>
398  ';
399  }
400 
404  protected function getBodyTagAttributes(): array
405  {
406  $parameters = parent::getBodyTagAttributes();
407  $parameters['data-site-url'] = $this->siteUrl;
408  $parameters['data-default-link-target'] = $this->defaultLinkTarget;
409  return $parameters;
410  }
411 }
‪TYPO3\CMS\Core\Localization\LanguageServiceFactory
Definition: LanguageServiceFactory.php:25
‪TYPO3\CMS\Core\View\ViewInterface
Definition: ViewInterface.php:24
‪TYPO3\CMS\Core\Page\JavaScriptModuleInstruction\create
‪static create(string $name, string $exportName=null)
Definition: JavaScriptModuleInstruction.php:47
‪TYPO3\CMS\Core\Page\JavaScriptModuleInstruction
Definition: JavaScriptModuleInstruction.php:23
‪TYPO3\CMS\RteCKEditor\Controller
Definition: BrowseLinksController.php:18
‪TYPO3\CMS\Core\Localization\LanguageService
Definition: LanguageService.php:46
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:51
‪TYPO3\CMS\Core\Configuration\Richtext
Definition: Richtext.php:34