[BUGFIX] Make ckeditor link browser not drop additional link params
[Packages/TYPO3.CMS.git] / typo3 / sysext / rte_ckeditor / Classes / Controller / BrowseLinksController.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\RteCKEditor\Controller;
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
18 use Psr\Http\Message\ServerRequestInterface;
19 use TYPO3\CMS\Core\Configuration\Richtext;
20 use TYPO3\CMS\Core\LinkHandling\LinkService;
21 use TYPO3\CMS\Core\Page\PageRenderer;
22 use TYPO3\CMS\Core\Utility\GeneralUtility;
23 use TYPO3\CMS\Lang\LanguageService;
24 use TYPO3\CMS\Recordlist\Controller\AbstractLinkBrowserController;
25
26 /**
27 * Extended controller for link browser
28 */
29 class BrowseLinksController extends AbstractLinkBrowserController
30 {
31 /**
32 * @var string
33 */
34 protected $editorId;
35
36 /**
37 * TYPO3 language code of the content language
38 *
39 * @var int
40 */
41 protected $contentsLanguage;
42
43 /**
44 * Language service object for localization to the content language
45 *
46 * @var LanguageService
47 */
48 protected $contentLanguageService;
49
50 /**
51 * @var array
52 */
53 protected $buttonConfig = [];
54
55 /**
56 * @var array
57 */
58 protected $thisConfig = [];
59
60 /**
61 * @var array
62 */
63 protected $classesAnchorDefault = [];
64
65 /**
66 * @var array
67 */
68 protected $classesAnchorDefaultTitle = [];
69
70 /**
71 * @var array
72 */
73 protected $classesAnchorClassTitle = [];
74
75 /**
76 * @var array
77 */
78 protected $classesAnchorDefaultTarget = [];
79
80 /**
81 * @var array
82 */
83 protected $classesAnchorJSOptions = [];
84
85 /**
86 * @var string
87 */
88 protected $defaultLinkTarget = '';
89
90 /**
91 * @var array
92 */
93 protected $additionalAttributes = [];
94
95 /**
96 * @var string
97 */
98 protected $siteUrl = '';
99
100 /**
101 * Initialize controller
102 */
103 protected function init()
104 {
105 parent::init();
106
107 $this->contentLanguageService = GeneralUtility::makeInstance(LanguageService::class);
108 }
109
110 /**
111 * @param ServerRequestInterface $request
112 */
113 protected function initVariables(ServerRequestInterface $request)
114 {
115 parent::initVariables($request);
116
117 $queryParameters = $request->getQueryParams();
118
119 $this->siteUrl = GeneralUtility::getIndpEnv('TYPO3_SITE_URL');
120
121 $this->currentLinkParts = $queryParameters['curUrl'] ?? [];
122 $this->editorId = $queryParameters['editorId'];
123 $this->contentsLanguage = $queryParameters['contentsLanguage'];
124 $this->RTEtsConfigParams = $queryParameters['RTEtsConfigParams'] ?? null;
125
126 $this->contentLanguageService->init($this->contentsLanguage);
127
128 $tcaFieldConf = ['enableRichtext' => true];
129 if (!empty($queryParameters['P']['richtextConfigurationName'])) {
130 $tcaFieldConf['richtextConfiguration'] = $queryParameters['P']['richtextConfigurationName'];
131 }
132
133 /** @var Richtext $richtextConfigurationProvider */
134 $richtextConfigurationProvider = GeneralUtility::makeInstance(Richtext::class);
135 $this->thisConfig = $richtextConfigurationProvider->getConfiguration(
136 $this->parameters['table'],
137 $this->parameters['fieldName'],
138 (int)$this->parameters['pid'],
139 $this->parameters['recordType'],
140 $tcaFieldConf
141 );
142 $this->buttonConfig = $this->thisConfig['buttons']['link'] ?? [];
143 }
144
145 /**
146 * Initialize document template object
147 */
148 protected function initDocumentTemplate()
149 {
150 parent::initDocumentTemplate();
151
152 $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
153 $pageRenderer->loadRequireJsModule(
154 'TYPO3/CMS/RteCkeditor/RteLinkBrowser',
155 'function(RteLinkBrowser) {
156 RteLinkBrowser.initialize(' . GeneralUtility::quoteJSvalue($this->editorId) . ');
157 }'
158 );
159 }
160
161 /**
162 * Initialize $this->currentLink and $this->currentLinkHandler
163 */
164 protected function initCurrentUrl()
165 {
166 if (empty($this->currentLinkParts)) {
167 return;
168 }
169
170 if (!empty($this->currentLinkParts['url'])) {
171 $linkService = GeneralUtility::makeInstance(LinkService::class);
172 $data = $linkService->resolve($this->currentLinkParts['url']);
173 $this->currentLinkParts['type'] = $data['type'];
174 unset($data['type']);
175 $this->currentLinkParts['url'] = $data;
176 if (!empty($this->currentLinkParts['url']['parameters'])) {
177 $this->currentLinkParts['params'] = '&' . $this->currentLinkParts['url']['parameters'];
178 }
179 }
180
181 if (!empty($this->currentLinkParts['class'])) {
182 // Only keep last class value (others are automatically added again by required option)
183 // https://review.typo3.org/#/c/29643
184 $currentClasses = GeneralUtility::trimExplode(' ', $this->currentLinkParts['class'], true);
185 if (count($currentClasses) > 1) {
186 $this->currentLinkParts['class'] = end($currentClasses);
187 }
188 }
189 parent::initCurrentUrl();
190 }
191
192 /**
193 * Renders the link attributes for the selected link handler
194 *
195 * @return string
196 */
197 public function renderLinkAttributeFields()
198 {
199 // Processing the classes configuration
200 if (!empty($this->buttonConfig['properties']['class']['allowedClasses'])) {
201 $classesAnchorArray = is_array($this->buttonConfig['properties']['class']['allowedClasses']) ? $this->buttonConfig['properties']['class']['allowedClasses'] : GeneralUtility::trimExplode(',', $this->buttonConfig['properties']['class']['allowedClasses'], true);
202 // Collecting allowed classes and configured default values
203 $classesAnchor = [
204 'all' => []
205 ];
206
207 if (is_array($this->thisConfig['classesAnchor'])) {
208 foreach ($this->thisConfig['classesAnchor'] as $label => $conf) {
209 if (in_array($conf['class'], $classesAnchorArray, true)) {
210 $classesAnchor['all'][] = $conf['class'];
211 if ($conf['type'] === $this->displayedLinkHandlerId) {
212 $classesAnchor[$conf['type']][] = $conf['class'];
213 if ($this->buttonConfig[$conf['type']]['properties']['class']['default'] == $conf['class']) {
214 $this->classesAnchorDefault[$conf['type']] = $conf['class'];
215 if ($conf['titleText']) {
216 $this->classesAnchorDefaultTitle[$conf['type']] = $this->contentLanguageService->sL(trim($conf['titleText']));
217 }
218 if (isset($conf['target'])) {
219 $this->classesAnchorDefaultTarget[$conf['type']] = trim($conf['target']);
220 }
221 }
222 }
223 if ($conf['titleText']) {
224 $this->classesAnchorClassTitle[$conf['class']] = ($this->classesAnchorDefaultTitle[$conf['type']] = $this->contentLanguageService->sL(trim($conf['titleText'])));
225 }
226 }
227 }
228 }
229 if (isset($this->linkAttributeValues['class'])
230 && isset($classesAnchor[$this->displayedLinkHandlerId])
231 && !in_array($this->linkAttributeValues['class'], $classesAnchor[$this->displayedLinkHandlerId], true)
232 ) {
233 unset($this->linkAttributeValues['class']);
234 }
235 // Constructing the class selector options
236 foreach ($classesAnchorArray as $class) {
237 if (!in_array($class, $classesAnchor['all']) || in_array($class, $classesAnchor['all']) && is_array($classesAnchor[$this->displayedLinkHandlerId]) && in_array($class, $classesAnchor[$this->displayedLinkHandlerId])) {
238 $selected = '';
239 if ($this->linkAttributeValues['class'] === $class || !$this->linkAttributeValues['class'] && $this->classesAnchorDefault[$this->displayedLinkHandlerId] == $class) {
240 $selected = 'selected="selected"';
241 }
242 $classLabel = !empty($this->thisConfig['classes'][$class]['name'])
243 ? $this->getPageConfigLabel($this->thisConfig['classes'][$class]['name'], 0)
244 : $class;
245 $classStyle = !empty($this->thisConfig['classes'][$class]['value'])
246 ? $this->thisConfig['classes'][$class]['value']
247 : '';
248 $title = $this->classesAnchorClassTitle[$class] ?? $this->classesAnchorDefaultTitle[$class] ?? '';
249 $this->classesAnchorJSOptions[$this->displayedLinkHandlerId] .= '<option ' . $selected . ' value="' . htmlspecialchars($class) . '"'
250 . ($classStyle ? ' style="' . htmlspecialchars($classStyle) . '"' : '')
251 . 'data-link-title="' . htmlspecialchars($title) . '"'
252 . '>' . htmlspecialchars($classLabel)
253 . '</option>';
254 }
255 }
256 if ($this->classesAnchorJSOptions[$this->displayedLinkHandlerId] && !($this->buttonConfig['properties']['class']['required'] || $this->buttonConfig[$this->displayedLinkHandlerId]['properties']['class']['required'])) {
257 $selected = '';
258 if (!$this->linkAttributeValues['class'] && !$this->classesAnchorDefault[$this->displayedLinkHandlerId]) {
259 $selected = 'selected="selected"';
260 }
261 $this->classesAnchorJSOptions[$this->displayedLinkHandlerId] = '<option ' . $selected . ' value=""></option>' . $this->classesAnchorJSOptions[$this->displayedLinkHandlerId];
262 }
263 }
264 // Default target
265 $this->defaultLinkTarget = $this->classesAnchorDefault[$this->displayedLinkHandlerId] && $this->classesAnchorDefaultTarget[$this->displayedLinkHandlerId]
266 ? $this->classesAnchorDefaultTarget[$this->displayedLinkHandlerId]
267 : (isset($this->buttonConfig[$this->displayedLinkHandlerId]['properties']['target']['default'])
268 ? $this->buttonConfig[$this->displayedLinkHandlerId]['properties']['target']['default']
269 : (isset($this->buttonConfig['properties']['target']['default'])
270 ? $this->buttonConfig['properties']['target']['default']
271 : ''));
272
273 // todo: find new name for this option
274 // Initializing additional attributes
275 if ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['rte_ckeditor']['plugins']['TYPO3Link']['additionalAttributes']) {
276 $addAttributes = GeneralUtility::trimExplode(',', $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['rte_ckeditor']['plugins']['TYPO3Link']['additionalAttributes'], true);
277 foreach ($addAttributes as $attribute) {
278 $this->additionalAttributes[$attribute] = $this->linkAttributeValues[$attribute] ?? '';
279 }
280 }
281 return parent::renderLinkAttributeFields();
282 }
283
284 /**
285 * Localize a label obtained from Page TSConfig
286 *
287 * @param string $string The label to be localized
288 * @param bool $JScharCode If needs to be converted to an array of char numbers
289 * @return string Localized string
290 */
291 public function getPageConfigLabel($string, $JScharCode = true)
292 {
293 if (strpos($string, 'LLL:') !== 0) {
294 $label = $string;
295 } else {
296 $label = $this->getLanguageService()->sL(trim($string));
297 }
298 $label = str_replace('"', '\\"', str_replace('\\\'', '\'', $label));
299 return $JScharCode ? GeneralUtility::quoteJSvalue($label) : $label;
300 }
301
302 /**
303 * @return string
304 */
305 protected function renderCurrentUrl()
306 {
307 $removeLink = ' <a href="#" class="t3js-removeCurrentLink">' . htmlspecialchars($this->getLanguageService()->getLL('removeLink')) . '</a>';
308 return '
309 <div class="element-browser-panel element-browser-title">' .
310 htmlspecialchars($this->getLanguageService()->getLL('currentLink')) .
311 ': ' .
312 htmlspecialchars($this->currentLinkHandler->formatCurrentUrl()) .
313 '<span class="pull-right">' . $removeLink . '</span>' .
314 '</div>';
315 }
316
317 /**
318 * Get the allowed items or tabs
319 *
320 * @return string[]
321 */
322 protected function getAllowedItems()
323 {
324 $allowedItems = parent::getAllowedItems();
325
326 $blindLinkOptions = isset($this->thisConfig['blindLinkOptions'])
327 ? GeneralUtility::trimExplode(',', $this->thisConfig['blindLinkOptions'], true)
328 : [];
329 $allowedItems = array_diff($allowedItems, $blindLinkOptions);
330
331 if (is_array($this->buttonConfig['options']) && $this->buttonConfig['options']['removeItems']) {
332 $allowedItems = array_diff($allowedItems, GeneralUtility::trimExplode(',', $this->buttonConfig['options']['removeItems'], true));
333 }
334
335 return $allowedItems;
336 }
337
338 /**
339 * Get the allowed link attributes
340 *
341 * @return string[]
342 */
343 protected function getAllowedLinkAttributes()
344 {
345 $allowedLinkAttributes = parent::getAllowedLinkAttributes();
346
347 $blindLinkFields = isset($this->thisConfig['blindLinkFields'])
348 ? GeneralUtility::trimExplode(',', $this->thisConfig['blindLinkFields'], true)
349 : [];
350 $allowedLinkAttributes = array_diff($allowedLinkAttributes, $blindLinkFields);
351
352 return $allowedLinkAttributes;
353 }
354
355 /**
356 * Create an array of link attribute field rendering definitions
357 *
358 * @return string[]
359 */
360 protected function getLinkAttributeFieldDefinitions()
361 {
362 $fieldRenderingDefinitions = parent::getLinkAttributeFieldDefinitions();
363 $fieldRenderingDefinitions['title'] = $this->getTitleField();
364 $fieldRenderingDefinitions['class'] = $this->getClassField();
365 $fieldRenderingDefinitions['target'] = $this->getTargetField();
366 $fieldRenderingDefinitions['rel'] = $this->getRelField();
367 if (empty($this->buttonConfig['queryParametersSelector']['enabled'])) {
368 unset($fieldRenderingDefinitions['params']);
369 }
370 return $fieldRenderingDefinitions;
371 }
372
373 /**
374 * Add rel field
375 *
376 * @return string
377 */
378 protected function getRelField()
379 {
380 if (empty($this->buttonConfig['relAttribute']['enabled'])) {
381 return '';
382 }
383
384 $currentRel = '';
385 if ($this->displayedLinkHandler === $this->currentLinkHandler
386 && !empty($this->currentLinkParts)
387 && isset($this->linkAttributeValues['rel'])
388 && is_string($this->linkAttributeValues['rel'])
389 ) {
390 $currentRel = $this->linkAttributeValues['rel'];
391 }
392
393 return '
394 <form action="" name="lrelform" id="lrelform" class="t3js-dummyform form-horizontal">
395 <div class="form-group form-group-sm">
396 <label class="col-xs-4 control-label">' .
397 htmlspecialchars($this->getLanguageService()->getLL('linkRelationship')) .
398 '</label>
399 <div class="col-xs-8">
400 <input type="text" name="lrel" class="form-control" value="' . htmlspecialchars($currentRel) . '" />
401 </div>
402 </div>
403 </form>
404 ';
405 }
406
407 /**
408 * Add target selector
409 *
410 * @return string
411 */
412 protected function getTargetField()
413 {
414 $targetSelectorConfig = [];
415 if (is_array($this->buttonConfig['targetSelector'])) {
416 $targetSelectorConfig = $this->buttonConfig['targetSelector'];
417 }
418 $target = $this->linkAttributeValues['target'] ?: $this->defaultLinkTarget;
419 $lang = $this->getLanguageService();
420 $targetSelector = '';
421
422 if (!$targetSelectorConfig['disabled']) {
423 $targetSelector = '
424 <select name="ltarget_type" class="t3js-targetPreselect form-control">
425 <option value=""></option>
426 <option value="_top">' . htmlspecialchars($lang->getLL('top')) . '</option>
427 <option value="_blank">' . htmlspecialchars($lang->getLL('newWindow')) . '</option>
428 </select>
429 ';
430 }
431
432 return '
433 <form action="" name="ltargetform" id="ltargetform" class="t3js-dummyform form-horizontal">
434 <div class="form-group form-group-sm" ' . ($targetSelectorConfig['disabled'] ? ' style="display: none;"' : '') . '>
435 <label class="col-xs-4 control-label">' . htmlspecialchars($lang->getLL('target')) . '</label>
436 <div class="col-xs-4">
437 <input type="text" name="ltarget" class="t3js-linkTarget form-control"
438 value="' . htmlspecialchars($target) . '" />
439 </div>
440 <div class="col-xs-4">
441 ' . $targetSelector . '
442 </div>
443 </div>
444 </form>
445 ';
446 }
447
448 /**
449 * Add title selector
450 *
451 * @return string
452 */
453 protected function getTitleField()
454 {
455 if ($this->linkAttributeValues['title']) {
456 $title = $this->linkAttributeValues['title'];
457 } else {
458 $title = $this->classesAnchorDefaultTitle[$this->displayedLinkHandlerId] ?: '';
459 }
460 if (isset($this->buttonConfig[$this->displayedLinkHandlerId]['properties']['title']['readOnly'])) {
461 $readOnly = (bool)$this->buttonConfig[$this->displayedLinkHandlerId]['properties']['title']['readOnly'];
462 } else {
463 $readOnly = isset($this->buttonConfig['properties']['title']['readOnly'])
464 ? (bool)$this->buttonConfig['properties']['title']['readOnly']
465 : false;
466 }
467
468 if ($readOnly) {
469 $currentClass = $this->linkAttributeFields['class'];
470 if (!$currentClass) {
471 $currentClass = empty($this->classesAnchorDefault[$this->displayedLinkHandlerId]) ? '' : $this->classesAnchorDefault[$this->displayedLinkHandlerId];
472 }
473 $title = $currentClass
474 ? $this->classesAnchorClassTitle[$currentClass]
475 : ($this->classesAnchorDefaultTitle[$this->displayedLinkHandlerId] ?? '');
476 }
477 return '
478 <form action="" name="ltitleform" id="ltitleform" class="t3js-dummyform form-horizontal">
479 <div class="form-group form-group-sm">
480 <label class="col-xs-4 control-label">
481 ' . htmlspecialchars($this->getLanguageService()->getLL('title')) . '
482 </label>
483 <div class="col-xs-8">
484 <input ' . ($readOnly ? 'disabled' : '') . ' type="text" name="ltitle" class="form-control t3js-linkTitle"
485 value="' . htmlspecialchars($title) . '" />
486 </div>
487 </div>
488 </form>
489 ';
490 }
491
492 /**
493 * Return html code for the class selector
494 *
495 * @return string the html code to be added to the form
496 */
497 protected function getClassField()
498 {
499 $selectClass = '';
500 if ($this->classesAnchorJSOptions[$this->displayedLinkHandlerId]) {
501 $selectClass = '
502 <form action="" name="lclassform" id="lclassform" class="t3js-dummyform form-horizontal">
503 <div class="form-group form-group-sm">
504 <label class="col-xs-4 control-label">
505 ' . htmlspecialchars($this->getLanguageService()->getLL('class')) . '
506 </label>
507 <div class="col-xs-8">
508 <select name="lclass" class="t3js-class-selector form-control">
509 ' . $this->classesAnchorJSOptions[$this->displayedLinkHandlerId] . '
510 </select>
511 </div>
512 </div>
513 </form>
514 ';
515 }
516 return $selectClass;
517 }
518
519 /**
520 * Return the ID of current page
521 *
522 * @return int
523 */
524 protected function getCurrentPageId()
525 {
526 return (int)$this->parameters['pid'];
527 }
528
529 /**
530 * Retrieve the configuration
531 *
532 * This is only used by RTE currently.
533 *
534 * @return array
535 */
536 public function getConfiguration()
537 {
538 return $this->buttonConfig;
539 }
540
541 /**
542 * Get attributes for the body tag
543 *
544 * @return string[] Array of body-tag attributes
545 */
546 protected function getBodyTagAttributes()
547 {
548 $parameters = parent::getBodyTagAttributes();
549 $parameters['data-site-url'] = $this->siteUrl;
550 $parameters['data-default-link-target'] = $this->defaultLinkTarget;
551 return $parameters;
552 }
553
554 /**
555 * @param array $overrides
556 *
557 * @return array Array of parameters which have to be added to URLs
558 */
559 public function getUrlParameters(array $overrides = null)
560 {
561 return [
562 'act' => isset($overrides['act']) ? $overrides['act'] : $this->displayedLinkHandlerId,
563 'editorId' => $this->editorId,
564 'contentsLanguage' => $this->contentsLanguage,
565 'P' => $this->parameters
566 ];
567 }
568 }