0c97f6ebf26558c788a06f8242f55e5bca41cc13
[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 /** @var Richtext $richtextConfigurationProvider */
129 $richtextConfigurationProvider = GeneralUtility::makeInstance(Richtext::class);
130 $this->thisConfig = $richtextConfigurationProvider->getConfiguration(
131 $this->parameters['table'],
132 $this->parameters['fieldName'],
133 (int)$this->parameters['pid'],
134 $this->parameters['recordType'],
135 ['richtext' => true]
136 );
137 $this->buttonConfig = $this->thisConfig['buttons.']['link.'] ?? [];
138 }
139
140 /**
141 * Initialize document template object
142 */
143 protected function initDocumentTemplate()
144 {
145 parent::initDocumentTemplate();
146
147 $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
148 $pageRenderer->loadRequireJsModule(
149 'TYPO3/CMS/RteCkeditor/RteLinkBrowser',
150 'function(RteLinkBrowser) {
151 RteLinkBrowser.initialize(' . GeneralUtility::quoteJSvalue($this->editorId) . ');
152 }'
153 );
154 }
155
156 /**
157 * Initialize $this->currentLink and $this->currentLinkHandler
158 */
159 protected function initCurrentUrl()
160 {
161 if (empty($this->currentLinkParts)) {
162 return;
163 }
164
165 if (!empty($this->currentLinkParts['url'])) {
166 $linkService = GeneralUtility::makeInstance(LinkService::class);
167 $data = $linkService->resolve($this->currentLinkParts['url']);
168 $this->currentLinkParts['type'] = $data['type'];
169 unset($data['type']);
170 $this->currentLinkParts['url'] = $data;
171 }
172
173 if (!empty($this->currentLinkParts['class'])) {
174 // Only keep last class value (others are automatically added again by required option)
175 // https://review.typo3.org/#/c/29643
176 $currentClasses = GeneralUtility::trimExplode(' ', $this->currentLinkParts['class'], true);
177 if (count($currentClasses) > 1) {
178 $this->currentLinkParts['class'] = end($currentClasses);
179 }
180 }
181 parent::initCurrentUrl();
182 }
183
184 /**
185 * Renders the link attributes for the selected link handler
186 *
187 * @return string
188 */
189 public function renderLinkAttributeFields()
190 {
191 // Processing the classes configuration
192 if (!empty($this->buttonConfig['properties.']['class.']['allowedClasses'])) {
193 $classesAnchorArray = GeneralUtility::trimExplode(',', $this->buttonConfig['properties.']['class.']['allowedClasses'], true);
194 // Collecting allowed classes and configured default values
195 $classesAnchor = [
196 'all' => []
197 ];
198 $titleReadOnly = $this->buttonConfig['properties.']['title.']['readOnly']
199 || $this->buttonConfig[$this->displayedLinkHandlerId . '.']['properties.']['title.']['readOnly'];
200 if (is_array($this->thisConfig['classesAnchor.'])) {
201 foreach ($this->thisConfig['classesAnchor.'] as $label => $conf) {
202 if (in_array($conf['class'], $classesAnchorArray, true)) {
203 $classesAnchor['all'][] = $conf['class'];
204 if ($conf['type'] === $this->displayedLinkHandlerId) {
205 $classesAnchor[$conf['type']][] = $conf['class'];
206 if ($this->buttonConfig[$conf['type'] . '.']['properties.']['class.']['default'] == $conf['class']) {
207 $this->classesAnchorDefault[$conf['type']] = $conf['class'];
208 if ($conf['titleText']) {
209 $this->classesAnchorDefaultTitle[$conf['type']] = $this->contentLanguageService->sL(trim($conf['titleText']));
210 }
211 if (isset($conf['target'])) {
212 $this->classesAnchorDefaultTarget[$conf['type']] = trim($conf['target']);
213 }
214 }
215 }
216 if ($titleReadOnly && $conf['titleText']) {
217 $this->classesAnchorClassTitle[$conf['class']] = ($this->classesAnchorDefaultTitle[$conf['type']] = $this->contentLanguageService->sL(trim($conf['titleText'])));
218 }
219 }
220 }
221 }
222 if (isset($this->linkAttributeValues['class'])
223 && isset($classesAnchor[$this->displayedLinkHandlerId])
224 && !in_array($this->linkAttributeValues['class'], $classesAnchor[$this->displayedLinkHandlerId], true)
225 ) {
226 unset($this->linkAttributeValues['class']);
227 }
228 // Constructing the class selector options
229 foreach ($classesAnchorArray as $class) {
230 if (!in_array($class, $classesAnchor['all']) || in_array($class, $classesAnchor['all']) && is_array($classesAnchor[$this->displayedLinkHandlerId]) && in_array($class, $classesAnchor[$this->displayedLinkHandlerId])) {
231 $selected = '';
232 if ($this->linkAttributeValues['class'] === $class || !$this->linkAttributeValues['class'] && $this->classesAnchorDefault[$this->displayedLinkHandlerId] == $class) {
233 $selected = 'selected="selected"';
234 }
235 $classLabel = !empty($this->thisConfig['classes.'][$class . '.']['name'])
236 ? $this->getPageConfigLabel($this->thisConfig['classes.'][$class . '.']['name'], 0)
237 : $class;
238 $classStyle = !empty($this->thisConfig['classes.'][$class . '.']['value'])
239 ? $this->thisConfig['classes.'][$class . '.']['value']
240 : '';
241 $this->classesAnchorJSOptions[$this->displayedLinkHandlerId] .= '<option ' . $selected . ' value="' . $class . '"' . ($classStyle ? ' style="' . $classStyle . '"' : '') . '>' . $classLabel . '</option>';
242 }
243 }
244 if ($this->classesAnchorJSOptions[$this->displayedLinkHandlerId] && !($this->buttonConfig['properties.']['class.']['required'] || $this->buttonConfig[$this->displayedLinkHandlerId . '.']['properties.']['class.']['required'])) {
245 $selected = '';
246 if (!$this->linkAttributeValues['class'] && !$this->classesAnchorDefault[$this->displayedLinkHandlerId]) {
247 $selected = 'selected="selected"';
248 }
249 $this->classesAnchorJSOptions[$this->displayedLinkHandlerId] = '<option ' . $selected . ' value=""></option>' . $this->classesAnchorJSOptions[$this->displayedLinkHandlerId];
250 }
251 }
252 // Default target
253 $this->defaultLinkTarget = $this->classesAnchorDefault[$this->displayedLinkHandlerId] && $this->classesAnchorDefaultTarget[$this->displayedLinkHandlerId]
254 ? $this->classesAnchorDefaultTarget[$this->displayedLinkHandlerId]
255 : (isset($this->buttonConfig[$this->displayedLinkHandlerId . '.']['properties.']['target.']['default'])
256 ? $this->buttonConfig[$this->displayedLinkHandlerId . '.']['properties.']['target.']['default']
257 : (isset($this->buttonConfig['properties.']['target.']['default'])
258 ? $this->buttonConfig['properties.']['target.']['default']
259 : ''));
260
261 // todo: find new name for this option
262 // Initializing additional attributes
263 if ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['rte_ckeditor']['plugins']['TYPO3Link']['additionalAttributes']) {
264 $addAttributes = GeneralUtility::trimExplode(',', $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['rte_ckeditor']['plugins']['TYPO3Link']['additionalAttributes'], true);
265 foreach ($addAttributes as $attribute) {
266 $this->additionalAttributes[$attribute] = $this->linkAttributeValues[$attribute] ?? '';
267 }
268 }
269 return parent::renderLinkAttributeFields();
270 }
271
272 /**
273 * Localize a label obtained from Page TSConfig
274 *
275 * @param string $string The label to be localized
276 * @param bool $JScharCode If needs to be converted to an array of char numbers
277 * @return string Localized string
278 */
279 public function getPageConfigLabel($string, $JScharCode = true)
280 {
281 if (strpos($string, 'LLL:') !== 0) {
282 $label = $string;
283 } else {
284 $label = $this->getLanguageService()->sL(trim($string));
285 }
286 $label = str_replace('"', '\\"', str_replace('\\\'', '\'', $label));
287 return $JScharCode ? GeneralUtility::quoteJSvalue($label) : $label;
288 }
289
290 /**
291 * @return string
292 */
293 protected function renderCurrentUrl()
294 {
295 $removeLink = ' <a href="#" class="btn btn-default t3js-removeCurrentLink">' . htmlspecialchars($this->getLanguageService()->getLL('removeLink')) . '</a>';
296 return '
297 <div class="link-browser-section link-browser-current-link">
298 <strong>' .
299 htmlspecialchars($this->getLanguageService()->getLL('currentLink')) .
300 ': ' .
301 htmlspecialchars($this->currentLinkHandler->formatCurrentUrl()) .
302 '</strong>' .
303 '<span class="pull-right">' . $removeLink . '</span>' .
304 '</div>';
305 }
306
307 /**
308 * Get the allowed items or tabs
309 *
310 * @return string[]
311 */
312 protected function getAllowedItems()
313 {
314 $allowedItems = parent::getAllowedItems();
315
316 $blindLinkOptions = isset($this->thisConfig['blindLinkOptions'])
317 ? GeneralUtility::trimExplode(',', $this->thisConfig['blindLinkOptions'], true)
318 : [];
319 $allowedItems = array_diff($allowedItems, $blindLinkOptions);
320
321 if (is_array($this->buttonConfig['options.']) && $this->buttonConfig['options.']['removeItems']) {
322 $allowedItems = array_diff($allowedItems, GeneralUtility::trimExplode(',', $this->buttonConfig['options.']['removeItems'], true));
323 }
324
325 return $allowedItems;
326 }
327
328 /**
329 * Get the allowed link attributes
330 *
331 * @return string[]
332 */
333 protected function getAllowedLinkAttributes()
334 {
335 $allowedLinkAttributes = parent::getAllowedLinkAttributes();
336
337 $blindLinkFields = isset($this->thisConfig['blindLinkFields'])
338 ? GeneralUtility::trimExplode(',', $this->thisConfig['blindLinkFields'], true)
339 : [];
340 $allowedLinkAttributes = array_diff($allowedLinkAttributes, $blindLinkFields);
341
342 return $allowedLinkAttributes;
343 }
344
345 /**
346 * Create an array of link attribute field rendering definitions
347 *
348 * @return string[]
349 */
350 protected function getLinkAttributeFieldDefinitions()
351 {
352 $fieldRenderingDefinitions = parent::getLinkAttributeFieldDefinitions();
353 $fieldRenderingDefinitions['title'] = $this->getTitleField();
354 $fieldRenderingDefinitions['class'] = $this->getClassField();
355 $fieldRenderingDefinitions['target'] = $this->getTargetField();
356 $fieldRenderingDefinitions['rel'] = $this->getRelField();
357 if (empty($this->buttonConfig['queryParametersSelector.']['enabled'])) {
358 unset($fieldRenderingDefinitions['params']);
359 }
360 return $fieldRenderingDefinitions;
361 }
362
363 /**
364 * Add rel field
365 *
366 * @return string
367 */
368 protected function getRelField()
369 {
370 if (empty($this->buttonConfig['relAttribute.']['enabled'])) {
371 return '';
372 }
373
374 $currentRel = $this->displayedLinkHandler === $this->currentLinkHandler && !empty($this->currentLinkParts)
375 ? $this->linkAttributeValues['rel']
376 : '';
377
378 return '
379 <form action="" name="lrelform" id="lrelform" class="t3js-dummyform form-horizontal">
380 <div class="form-group form-group-sm">
381 <label class="col-xs-4 control-label">' .
382 htmlspecialchars($this->getLanguageService()->getLL('linkRelationship')) .
383 '</label>
384 <div class="col-xs-8">
385 <input type="text" name="lrel" class="form-control" value="' . $currentRel . '" />
386 </div>
387 </div>
388 </form>
389 ';
390 }
391
392 /**
393 * Add target selector
394 *
395 * @return string
396 */
397 protected function getTargetField()
398 {
399 $targetSelectorConfig = [];
400 if (is_array($this->buttonConfig['targetSelector.'])) {
401 $targetSelectorConfig = $this->buttonConfig['targetSelector.'];
402 }
403 $target = $this->linkAttributeValues['target'] ?: $this->defaultLinkTarget;
404 $lang = $this->getLanguageService();
405 $targetSelector = '';
406
407 if (!$targetSelectorConfig['disabled']) {
408 $targetSelector = '
409 <select name="ltarget_type" class="t3js-targetPreselect form-control">
410 <option value=""></option>
411 <option value="_top">' . htmlspecialchars($lang->getLL('top')) . '</option>
412 <option value="_blank">' . htmlspecialchars($lang->getLL('newWindow')) . '</option>
413 </select>
414 ';
415 }
416
417 return '
418 <form action="" name="ltargetform" id="ltargetform" class="t3js-dummyform form-horizontal">
419 <div class="form-group form-group-sm" ' . ($targetSelectorConfig['disabled'] ? ' style="display: none;"' : '') . '>
420 <label class="col-xs-4 control-label">' . htmlspecialchars($lang->getLL('target')) . '</label>
421 <div class="col-xs-4">
422 <input type="text" name="ltarget" class="t3js-linkTarget form-control"
423 value="' . htmlspecialchars($target) . '" />
424 </div>
425 <div class="col-xs-4">
426 ' . $targetSelector . '
427 </div>
428 </div>
429 </form>
430 ';
431 }
432
433 /**
434 * Add title selector
435 *
436 * @return string
437 */
438 protected function getTitleField()
439 {
440 if ($this->linkAttributeValues['title']) {
441 $title = $this->linkAttributeValues['title'];
442 } else {
443 $title = $this->classesAnchorDefaultTitle[$this->displayedLinkHandlerId] ?: '';
444 }
445 if (isset($this->buttonConfig[$this->displayedLinkHandlerId . '.']['properties.']['title.']['readOnly'])) {
446 $readOnly = (bool)$this->buttonConfig[$this->displayedLinkHandlerId . '.']['properties.']['title.']['readOnly'];
447 } else {
448 $readOnly = isset($this->buttonConfig['properties.']['title.']['readOnly'])
449 ? (bool)$this->buttonConfig['properties.']['title.']['readOnly']
450 : false;
451 }
452
453 if ($readOnly) {
454 $currentClass = $this->linkAttributeFields['class'];
455 if (!$currentClass) {
456 $currentClass = empty($this->classesAnchorDefault[$this->displayedLinkHandlerId]) ? '' : $this->classesAnchorDefault[$this->displayedLinkHandlerId];
457 }
458 $title = $currentClass
459 ? $this->classesAnchorClassTitle[$currentClass]
460 : $this->classesAnchorDefaultTitle[$this->displayedLinkHandlerId];
461 }
462 return '
463 <form action="" name="ltitleform" id="ltitleform" class="t3js-dummyform form-horizontal">
464 <div class="form-group form-group-sm">
465 <label class="col-xs-4 control-label">
466 ' . htmlspecialchars($this->getLanguageService()->getLL('title')) . '
467 </label>
468 <div class="col-xs-8">
469 <span style="display: ' . ($readOnly ? 'none' : 'inline') . ';">
470 <input type="text" name="ltitle" class="form-control"
471 value="' . htmlspecialchars($title) . '" />
472 </span>
473 <span id="rte-ckeditor-browse-links-title-readonly"
474 style="display: ' . ($readOnly ? 'inline' : 'none') . ';">
475 ' . htmlspecialchars($title) . '</span>
476 </div>
477 </div>
478 </form>
479 ';
480 }
481
482 /**
483 * Return html code for the class selector
484 *
485 * @return string the html code to be added to the form
486 */
487 protected function getClassField()
488 {
489 $selectClass = '';
490 if ($this->classesAnchorJSOptions[$this->displayedLinkHandlerId]) {
491 $selectClass = '
492 <form action="" name="lclassform" id="lclassform" class="t3js-dummyform form-horizontal">
493 <div class="form-group form-group-sm">
494 <label class="col-xs-4 control-label">
495 ' . htmlspecialchars($this->getLanguageService()->getLL('class')) . '
496 </label>
497 <div class="col-xs-8">
498 <select name="lclass" class="t3js-class-selector form-control">
499 ' . $this->classesAnchorJSOptions[$this->displayedLinkHandlerId] . '
500 </select>
501 </div>
502 </div>
503 </form>
504 ';
505 }
506 return $selectClass;
507 }
508
509 /**
510 * Return the ID of current page
511 *
512 * @return int
513 */
514 protected function getCurrentPageId()
515 {
516 return (int)$this->parameters['pid'];
517 }
518
519 /**
520 * Retrieve the configuration
521 *
522 * This is only used by RTE currently.
523 *
524 * @return array
525 */
526 public function getConfiguration()
527 {
528 return $this->buttonConfig;
529 }
530
531 /**
532 * Get attributes for the body tag
533 *
534 * @return string[] Array of body-tag attributes
535 */
536 protected function getBodyTagAttributes()
537 {
538 $parameters = parent::getBodyTagAttributes();
539 $parameters['data-site-url'] = $this->siteUrl;
540 $parameters['data-default-link-target'] = $this->defaultLinkTarget;
541 return $parameters;
542 }
543
544 /**
545 * @param array $overrides
546 *
547 * @return array Array of parameters which have to be added to URLs
548 */
549 public function getUrlParameters(array $overrides = null)
550 {
551 return [
552 'act' => isset($overrides['act']) ? $overrides['act'] : $this->displayedLinkHandlerId,
553 'editorId' => $this->editorId,
554 'contentsLanguage' => $this->contentsLanguage,
555 'P' => $this->parameters
556 ];
557 }
558 }