[TASK] Unify element- and linkbrowser styling
[Packages/TYPO3.CMS.git] / typo3 / sysext / rtehtmlarea / Classes / Controller / BrowseLinksController.php
1 <?php
2 namespace TYPO3\CMS\Rtehtmlarea\Controller;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use Psr\Http\Message\ServerRequestInterface;
18 use TYPO3\CMS\Core\Configuration\Richtext;
19 use TYPO3\CMS\Core\LinkHandling\LinkService;
20 use TYPO3\CMS\Core\Page\PageRenderer;
21 use TYPO3\CMS\Core\Utility\ArrayUtility;
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 * Active with TYPO3 Element Browser: Contains the name of the form field for which this window
33 * opens - thus allows us to make references back to the main window in which the form is.
34 * Example value: "data[pages][39][bodytext]|||tt_content|"
35 * or "data[tt_content][NEW3fba56fde763d][image]|||gif,jpg,jpeg,tif,bmp,pcx,tga,png,pdf,ai|"
36 *
37 * Values:
38 * 0: form field name reference, eg. "data[tt_content][123][image]"
39 * 1: htmlArea RTE parameters: editorNo:contentTypo3Language
40 * 2: RTE config parameters: RTEtsConfigParams
41 * 3: allowed types. Eg. "tt_content" or "gif,jpg,jpeg,tif,bmp,pcx,tga,png,pdf,ai"
42 *
43 * $pArr = explode('|', $this->bparams);
44 * $formFieldName = $pArr[0];
45 * $allowedTablesOrFileTypes = $pArr[3];
46 *
47 * @var string
48 */
49 protected $bparams;
50
51 /**
52 * @var int
53 */
54 protected $editorNo;
55
56 /**
57 * TYPO3 language code of the content language
58 *
59 * @var int
60 */
61 protected $contentTypo3Language;
62
63 /**
64 * Language service object for localization to the content language
65 *
66 * @var LanguageService
67 */
68 protected $contentLanguageService;
69
70 /**
71 * @var array
72 */
73 protected $buttonConfig = [];
74
75 /**
76 * @var array
77 */
78 protected $thisConfig = [];
79
80 /**
81 * Used with the Rich Text Editor.
82 * Example value: "tt_content:NEW3fba58c969f5c:bodytext:23:text:23:"
83 *
84 * @var string
85 */
86 protected $RTEtsConfigParams;
87
88 /**
89 * @var array
90 */
91 protected $classesAnchorDefault = [];
92
93 /**
94 * @var array
95 */
96 protected $classesAnchorDefaultTitle = [];
97
98 /**
99 * @var array
100 */
101 protected $classesAnchorClassTitle = [];
102
103 /**
104 * @var array
105 */
106 protected $classesAnchorDefaultTarget = [];
107
108 /**
109 * @var array
110 */
111 protected $classesAnchorJSOptions = [];
112
113 /**
114 * @var string
115 */
116 protected $defaultLinkTarget = '';
117
118 /**
119 * @var array
120 */
121 protected $additionalAttributes = [];
122
123 /**
124 * @var string
125 */
126 protected $siteUrl = '';
127
128 /**
129 * Initialize controller
130 */
131 protected function init()
132 {
133 parent::init();
134
135 $lang = $this->getLanguageService();
136 $lang->includeLLFile('EXT:rtehtmlarea/Resources/Private/Language/locallang_browselinkscontroller.xlf');
137 $lang->includeLLFile('EXT:rtehtmlarea/Resources/Private/Language/locallang_dialogs.xlf');
138
139 $this->contentLanguageService = GeneralUtility::makeInstance(LanguageService::class);
140 }
141
142 /**
143 * @param ServerRequestInterface $request
144 */
145 protected function initVariables(ServerRequestInterface $request)
146 {
147 parent::initVariables($request);
148
149 $queryParameters = $request->getQueryParams();
150 $this->bparams = isset($queryParameters['bparams']) ? $queryParameters['bparams'] : '';
151
152 $this->siteUrl = GeneralUtility::getIndpEnv('TYPO3_SITE_URL');
153
154 $currentLinkParts = isset($queryParameters['curUrl']) ? $queryParameters['curUrl'] : [];
155 if (isset($currentLinkParts['all'])) {
156 $currentLinkParts = GeneralUtility::get_tag_attributes($queryParameters['curUrl']['all']);
157 $currentLinkParts['url'] = htmlspecialchars_decode($currentLinkParts['href']);
158 unset($currentLinkParts['href']);
159 }
160 $this->currentLinkParts = $currentLinkParts;
161
162 // Process bparams
163 $pArr = explode('|', $this->bparams);
164 $pRteArr = explode(':', $pArr[1]);
165 $this->editorNo = $pRteArr[0];
166 $this->contentTypo3Language = $pRteArr[1];
167 $this->RTEtsConfigParams = $pArr[2];
168 if (!$this->editorNo) {
169 $this->editorNo = GeneralUtility::_GP('editorNo');
170 $this->contentTypo3Language = GeneralUtility::_GP('contentTypo3Language');
171 $this->RTEtsConfigParams = GeneralUtility::_GP('RTEtsConfigParams');
172 }
173 $pArr[1] = implode(':', [$this->editorNo, $this->contentTypo3Language]);
174 $pArr[2] = $this->RTEtsConfigParams;
175 $this->bparams = implode('|', $pArr);
176
177 $this->contentLanguageService->init($this->contentTypo3Language);
178
179 // @todo: This needs refactoring to enable sane config in flex form, either transfer parts of 'config', or use data providers
180 $RTEtsConfigParts = explode(':', $this->RTEtsConfigParams);
181 $table = $RTEtsConfigParts[0];
182 $field = $RTEtsConfigParts[2];
183 $recordType = $RTEtsConfigParts[3];
184 $tcaConfigOfField = $GLOBALS['TCA'][$table][$field]['config'] ?? [];
185 $columnsOverridesConfigOfField = $GLOBALS['TCA'][$table]['types'][$recordType]['columnsOverrides'][$field]['config'] ?? [];
186 if (!empty($columnsOverridesConfigOfField)) {
187 ArrayUtility::mergeRecursiveWithOverrule($tcaConfigOfField, $columnsOverridesConfigOfField);
188 }
189 $richtextConfigurationProvider = GeneralUtility::makeInstance(Richtext::class);
190 $richtextConfiguration = $richtextConfigurationProvider->getConfiguration(
191 $RTEtsConfigParts[0],
192 $RTEtsConfigParts[2],
193 $RTEtsConfigParts[3],
194 $RTEtsConfigParts[4],
195 $tcaConfigOfField
196 );
197 $this->thisConfig = $richtextConfiguration;
198
199 $this->buttonConfig = isset($this->thisConfig['buttons.']['link.'])
200 ? $this->thisConfig['buttons.']['link.']
201 : [];
202 }
203
204 /**
205 * Initialize document template object
206 */
207 protected function initDocumentTemplate()
208 {
209 parent::initDocumentTemplate();
210
211 $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
212 $pageRenderer->loadRequireJsModule('TYPO3/CMS/Rtehtmlarea/RteLinkBrowser');
213 }
214
215 /**
216 * Initialize $this->currentLink and $this->currentLinkHandler
217 */
218 protected function initCurrentUrl()
219 {
220 if (empty($this->currentLinkParts)) {
221 return;
222 }
223
224 if (!empty($this->currentLinkParts['url'])) {
225 $linkService = GeneralUtility::makeInstance(LinkService::class);
226 $data = $linkService->resolve($this->currentLinkParts['url']);
227 $this->currentLinkParts['type'] = $data['type'];
228 unset($data['type']);
229 $this->currentLinkParts['url'] = $data;
230 }
231
232 if (!empty($this->currentLinkParts['class'])) {
233 // remove required classes
234 $currentClasses = GeneralUtility::trimExplode(' ', $this->currentLinkParts['class'], true);
235 if (count($currentClasses) > 1) {
236 $this->currentLinkParts['class'] = end($currentClasses);
237 }
238 }
239 parent::initCurrentUrl();
240 }
241
242 /**
243 * Renders the link attributes for the selected link handler
244 *
245 * @return string
246 */
247 public function renderLinkAttributeFields()
248 {
249 // Processing the classes configuration
250 if (!empty($this->buttonConfig['properties.']['class.']['allowedClasses'])) {
251 $classesAnchorArray = GeneralUtility::trimExplode(',', $this->buttonConfig['properties.']['class.']['allowedClasses'], true);
252 // Collecting allowed classes and configured default values
253 $classesAnchor = [
254 'all' => []
255 ];
256 $titleReadOnly = $this->buttonConfig['properties.']['title.']['readOnly']
257 || $this->buttonConfig[$this->displayedLinkHandlerId . '.']['properties.']['title.']['readOnly'];
258 if (is_array($this->thisConfig['classesAnchor.'])) {
259 foreach ($this->thisConfig['classesAnchor.'] as $label => $conf) {
260 if (in_array($conf['class'], $classesAnchorArray)) {
261 $classesAnchor['all'][] = $conf['class'];
262 if ($conf['type'] === $this->displayedLinkHandlerId) {
263 $classesAnchor[$conf['type']][] = $conf['class'];
264 if ($this->buttonConfig[$conf['type'] . '.']['properties.']['class.']['default'] == $conf['class']) {
265 $this->classesAnchorDefault[$conf['type']] = $conf['class'];
266 if ($conf['titleText']) {
267 $this->classesAnchorDefaultTitle[$conf['type']] = $this->contentLanguageService->sL(trim($conf['titleText']));
268 }
269 if (isset($conf['target'])) {
270 $this->classesAnchorDefaultTarget[$conf['type']] = trim($conf['target']);
271 }
272 }
273 }
274 if ($titleReadOnly && $conf['titleText']) {
275 $this->classesAnchorClassTitle[$conf['class']] = ($this->classesAnchorDefaultTitle[$conf['type']] = $this->contentLanguageService->sL(trim($conf['titleText'])));
276 }
277 }
278 }
279 }
280 if (isset($this->linkAttributeValues['class'])
281 && isset($classesAnchor[$this->displayedLinkHandlerId])
282 && !in_array($this->linkAttributeValues['class'], $classesAnchor[$this->displayedLinkHandlerId], true)
283 ) {
284 unset($this->linkAttributeValues['class']);
285 }
286 // Constructing the class selector options
287 foreach ($classesAnchorArray as $class) {
288 if (!in_array($class, $classesAnchor['all']) || in_array($class, $classesAnchor['all']) && is_array($classesAnchor[$this->displayedLinkHandlerId]) && in_array($class, $classesAnchor[$this->displayedLinkHandlerId])) {
289 $selected = '';
290 if ($this->linkAttributeValues['class'] === $class || !$this->linkAttributeValues['class'] && $this->classesAnchorDefault[$this->displayedLinkHandlerId] == $class) {
291 $selected = 'selected="selected"';
292 }
293 $classLabel = !empty($this->thisConfig['classes.'][$class . '.']['name'])
294 ? $this->getPageConfigLabel($this->thisConfig['classes.'][$class . '.']['name'], 0)
295 : $class;
296 $classStyle = !empty($this->thisConfig['classes.'][$class . '.']['value'])
297 ? $this->thisConfig['classes.'][$class . '.']['value']
298 : '';
299 $this->classesAnchorJSOptions[$this->displayedLinkHandlerId] .= '<option ' . $selected . ' value="' . $class . '"' . ($classStyle ? ' style="' . $classStyle . '"' : '') . '>' . $classLabel . '</option>';
300 }
301 }
302 if ($this->classesAnchorJSOptions[$this->displayedLinkHandlerId] && !($this->buttonConfig['properties.']['class.']['required'] || $this->buttonConfig[$this->displayedLinkHandlerId . '.']['properties.']['class.']['required'])) {
303 $selected = '';
304 if (!$this->linkAttributeValues['class'] && !$this->classesAnchorDefault[$this->displayedLinkHandlerId]) {
305 $selected = 'selected="selected"';
306 }
307 $this->classesAnchorJSOptions[$this->displayedLinkHandlerId] = '<option ' . $selected . ' value=""></option>' . $this->classesAnchorJSOptions[$this->displayedLinkHandlerId];
308 }
309 }
310 // Default target
311 $this->defaultLinkTarget = $this->classesAnchorDefault[$this->displayedLinkHandlerId] && $this->classesAnchorDefaultTarget[$this->displayedLinkHandlerId]
312 ? $this->classesAnchorDefaultTarget[$this->displayedLinkHandlerId]
313 : (isset($this->buttonConfig[$this->displayedLinkHandlerId . '.']['properties.']['target.']['default'])
314 ? $this->buttonConfig[$this->displayedLinkHandlerId . '.']['properties.']['target.']['default']
315 : (isset($this->buttonConfig['properties.']['target.']['default'])
316 ? $this->buttonConfig['properties.']['target.']['default']
317 : ''));
318 // Initializing additional attributes
319 if ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['rtehtmlarea']['plugins']['TYPO3Link']['additionalAttributes']) {
320 $addAttributes = GeneralUtility::trimExplode(',', $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['rtehtmlarea']['plugins']['TYPO3Link']['additionalAttributes'], true);
321 foreach ($addAttributes as $attribute) {
322 $this->additionalAttributes[$attribute] = isset($this->linkAttributeValues[$attribute]) ? $this->linkAttributeValues[$attribute] : '';
323 }
324 }
325 return parent::renderLinkAttributeFields();
326 }
327
328 /**
329 * Localize a label obtained from Page TSConfig
330 *
331 * @param string $string The label to be localized
332 * @param bool $JScharCode If needs to be converted to an array of char numbers
333 * @return string Localized string
334 */
335 public function getPageConfigLabel($string, $JScharCode = true)
336 {
337 if (substr($string, 0, 4) !== 'LLL:') {
338 $label = $string;
339 } else {
340 $label = $this->getLanguageService()->sL(trim($string));
341 }
342 $label = str_replace('"', '\\"', str_replace('\\\'', '\'', $label));
343 return $JScharCode ? GeneralUtility::quoteJSvalue($label) : $label;
344 }
345
346 /**
347 * @return string
348 */
349 protected function renderCurrentUrl()
350 {
351 $removeLink = ' <a href="#" class="t3js-removeCurrentLink">' . htmlspecialchars($this->getLanguageService()->getLL('removeLink')) . '</a>';
352 return '
353 <div class="element-browser-panel element-browser-title">' .
354 htmlspecialchars($this->getLanguageService()->getLL('currentLink')) .
355 ': ' .
356 htmlspecialchars($this->currentLinkHandler->formatCurrentUrl()) .
357 '<span class="pull-right">' . $removeLink . '</span>' .
358 '</div>';
359 }
360
361 /**
362 * Get the allowed items or tabs
363 *
364 * @return string[]
365 */
366 protected function getAllowedItems()
367 {
368 $allowedItems = parent::getAllowedItems();
369
370 $blindLinkOptions = isset($this->thisConfig['blindLinkOptions'])
371 ? GeneralUtility::trimExplode(',', $this->thisConfig['blindLinkOptions'], true)
372 : [];
373 $allowedItems = array_diff($allowedItems, $blindLinkOptions);
374
375 if (is_array($this->buttonConfig['options.']) && $this->buttonConfig['options.']['removeItems']) {
376 $allowedItems = array_diff($allowedItems, GeneralUtility::trimExplode(',', $this->buttonConfig['options.']['removeItems'], true));
377 }
378
379 return $allowedItems;
380 }
381
382 /**
383 * Get the allowed link attributes
384 *
385 * @return string[]
386 */
387 protected function getAllowedLinkAttributes()
388 {
389 $allowedLinkAttributes = parent::getAllowedLinkAttributes();
390
391 $blindLinkFields = isset($this->thisConfig['blindLinkFields'])
392 ? GeneralUtility::trimExplode(',', $this->thisConfig['blindLinkFields'], true)
393 : [];
394 $allowedLinkAttributes = array_diff($allowedLinkAttributes, $blindLinkFields);
395
396 return $allowedLinkAttributes;
397 }
398
399 /**
400 * Create an array of link attribute field rendering definitions
401 *
402 * @return string[]
403 */
404 protected function getLinkAttributeFieldDefinitions()
405 {
406 $fieldRenderingDefinitions = parent::getLinkAttributeFieldDefinitions();
407 $fieldRenderingDefinitions['title'] = $this->getTitleField();
408 $fieldRenderingDefinitions['class'] = $this->getClassField();
409 $fieldRenderingDefinitions['target'] = $this->getTargetField();
410 $fieldRenderingDefinitions['rel'] = $this->getRelField();
411 if (empty($this->buttonConfig['queryParametersSelector.']['enabled'])) {
412 unset($fieldRenderingDefinitions['params']);
413 }
414 return $fieldRenderingDefinitions;
415 }
416
417 /**
418 * Add rel field
419 *
420 * @return string
421 */
422 protected function getRelField()
423 {
424 if (empty($this->buttonConfig['relAttribute.']['enabled'])) {
425 return '';
426 }
427 // @todo add rel to attributes
428 $currentRel = $this->displayedLinkHandler === $this->currentLinkHandler && !empty($this->currentLinkParts)
429 ? $this->linkAttributeValues['rel']
430 : '';
431 // @todo define label "linkRelationship" below in xlf
432 return '
433 <form action="" name="lrelform" id="lrelform" class="t3js-dummyform form-horizontal">
434 <div class="form-group form-group-sm">
435 <label class="col-xs-4 control-label">' .
436 htmlspecialchars($this->getLanguageService()->getLL('linkRelationship')) .
437 '</label>
438 <div class="col-xs-8">
439 <input type="text" name="lrel" class="form-control" value="' . $currentRel . '" />
440 </div>
441 </div>
442 </form>
443 ';
444 }
445
446 /**
447 * Add target selector
448 *
449 * @return string
450 */
451 protected function getTargetField()
452 {
453 $targetSelectorConfig = [];
454 if (is_array($this->buttonConfig['targetSelector.'])) {
455 $targetSelectorConfig = $this->buttonConfig['targetSelector.'];
456 }
457 $target = $this->linkAttributeValues['target'] ?: $this->defaultLinkTarget;
458 $lang = $this->getLanguageService();
459 $targetSelector = '';
460
461 if (!$targetSelectorConfig['disabled']) {
462 $targetSelector = '
463 <select name="ltarget_type" class="t3js-targetPreselect form-control">
464 <option value=""></option>
465 <option value="_top">' . htmlspecialchars($lang->getLL('top')) . '</option>
466 <option value="_blank">' . htmlspecialchars($lang->getLL('newWindow')) . '</option>
467 </select>
468 ';
469 }
470
471 return '
472 <form action="" name="ltargetform" id="ltargetform" class="t3js-dummyform form-horizontal">
473 <div class="form-group form-group-sm" ' . ($targetSelectorConfig['disabled'] ? ' style="display: none;"' : '') . '>
474 <label class="col-xs-4 control-label">' . htmlspecialchars($lang->getLL('target')) . '</label>
475 <div class="col-xs-4">
476 <input type="text" name="ltarget" class="t3js-linkTarget form-control"
477 value="' . htmlspecialchars($target) . '" />
478 </div>
479 <div class="col-xs-4">
480 ' . $targetSelector . '
481 </div>
482 </div>
483 </form>
484 ';
485 }
486
487 /**
488 * Add title selector
489 *
490 * @return string
491 */
492 protected function getTitleField()
493 {
494 if ($this->linkAttributeValues['title']) {
495 $title = $this->linkAttributeValues['title'];
496 } else {
497 $title = $this->classesAnchorDefaultTitle[$this->displayedLinkHandlerId] ?: '';
498 }
499 if (isset($this->buttonConfig[$this->displayedLinkHandlerId . '.']['properties.']['title.']['readOnly'])) {
500 $readOnly = (bool)$this->buttonConfig[$this->displayedLinkHandlerId . '.']['properties.']['title.']['readOnly'];
501 } else {
502 $readOnly = isset($this->buttonConfig['properties.']['title.']['readOnly'])
503 ? (bool)$this->buttonConfig['properties.']['title.']['readOnly']
504 : false;
505 }
506
507 if ($readOnly) {
508 $currentClass = $this->linkAttributeFields['class'];
509 if (!$currentClass) {
510 $currentClass = empty($this->classesAnchorDefault[$this->displayedLinkHandlerId]) ? '' : $this->classesAnchorDefault[$this->displayedLinkHandlerId];
511 }
512 $title = $currentClass
513 ? $this->classesAnchorClassTitle[$currentClass]
514 : $this->classesAnchorDefaultTitle[$this->displayedLinkHandlerId];
515 }
516 return '
517 <form action="" name="ltitleform" id="ltitleform" class="t3js-dummyform form-horizontal">
518 <div class="form-group form-group-sm">
519 <label class="col-xs-4 control-label">
520 ' . htmlspecialchars($this->getLanguageService()->getLL('anchor_title')) . '
521 </label>
522 <div class="col-xs-8">
523 <span style="display: ' . ($readOnly ? 'none' : 'inline') . ';">
524 <input type="text" name="ltitle" class="form-control"
525 value="' . htmlspecialchars($title) . '" />
526 </span>
527 <span id="rtehtmlarea-browse-links-title-readonly"
528 style="display: ' . ($readOnly ? 'inline' : 'none') . ';">
529 ' . htmlspecialchars($title) . '</span>
530 </div>
531 </div>
532 </form>
533 ';
534 }
535
536 /**
537 * Return html code for the class selector
538 *
539 * @return string the html code to be added to the form
540 */
541 protected function getClassField()
542 {
543 $selectClass = '';
544 if ($this->classesAnchorJSOptions[$this->displayedLinkHandlerId]) {
545 $selectClass = '
546 <form action="" name="lclassform" id="lclassform" class="t3js-dummyform form-horizontal">
547 <div class="form-group form-group-sm">
548 <label class="col-xs-4 control-label">
549 ' . htmlspecialchars($this->getLanguageService()->getLL('anchor_class')) . '
550 </label>
551 <div class="col-xs-8">
552 <select name="lclass" class="t3js-class-selector form-control">
553 ' . $this->classesAnchorJSOptions[$this->displayedLinkHandlerId] . '
554 </select>
555 </div>
556 </div>
557 </form>
558 ';
559 }
560 return $selectClass;
561 }
562
563 /**
564 * Return the ID of current page
565 *
566 * @return int
567 */
568 protected function getCurrentPageId()
569 {
570 return explode(':', $this->RTEtsConfigParams)[5];
571 }
572
573 /**
574 * Retrieve the configuration
575 *
576 * This is only used by RTE currently.
577 *
578 * @return array
579 */
580 public function getConfiguration()
581 {
582 return $this->buttonConfig;
583 }
584
585 /**
586 * Get attributes for the body tag
587 *
588 * @return string[] Array of body-tag attributes
589 */
590 protected function getBodyTagAttributes()
591 {
592 $parameters = parent::getBodyTagAttributes();
593 $parameters['data-site-url'] = $this->siteUrl;
594 return $parameters;
595 }
596
597 /**
598 * @param array $overrides
599 *
600 * @return array Array of parameters which have to be added to URLs
601 */
602 public function getUrlParameters(array $overrides = null)
603 {
604 return [
605 'act' => isset($overrides['act']) ? $overrides['act'] : $this->displayedLinkHandlerId,
606 'bparams' => $this->bparams,
607 'editorNo' => $this->editorNo,
608 'contentTypo3Language' => $this->contentTypo3Language
609 ];
610 }
611 }