[TASK] Use null coalescing operator where possible
[Packages/TYPO3.CMS.git] / typo3 / sysext / recordlist / Classes / Controller / AbstractLinkBrowserController.php
1 <?php
2 namespace TYPO3\CMS\Recordlist\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\ResponseInterface;
18 use Psr\Http\Message\ServerRequestInterface;
19 use TYPO3\CMS\Backend\Routing\UriBuilder;
20 use TYPO3\CMS\Backend\Template\DocumentTemplate;
21 use TYPO3\CMS\Backend\Utility\BackendUtility;
22 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
23 use TYPO3\CMS\Core\Localization\LanguageService;
24 use TYPO3\CMS\Core\Service\DependencyOrderingService;
25 use TYPO3\CMS\Core\Utility\GeneralUtility;
26 use TYPO3\CMS\Recordlist\LinkHandler\LinkHandlerInterface;
27
28 /**
29 * Script class for the Link Browser window.
30 */
31 abstract class AbstractLinkBrowserController
32 {
33 /**
34 * @var DocumentTemplate
35 */
36 protected $doc;
37
38 /**
39 * @var array
40 */
41 protected $parameters;
42
43 /**
44 * URL of current request
45 *
46 * @var string
47 */
48 protected $thisScript = '';
49
50 /**
51 * @var LinkHandlerInterface[]
52 */
53 protected $linkHandlers = [];
54
55 /**
56 * All parts of the current link
57 *
58 * Comprised of url information and additional link parameters.
59 *
60 * @var string[]
61 */
62 protected $currentLinkParts = [];
63
64 /**
65 * Link handler responsible for the current active link
66 *
67 * @var LinkHandlerInterface $currentLinkHandler
68 */
69 protected $currentLinkHandler;
70
71 /**
72 * The ID of the currently active link handler
73 *
74 * @var string
75 */
76 protected $currentLinkHandlerId;
77
78 /**
79 * Link handler to be displayed
80 *
81 * @var LinkHandlerInterface $displayedLinkHandler
82 */
83 protected $displayedLinkHandler;
84
85 /**
86 * The ID of the displayed link handler
87 *
88 * This is read from the 'act' GET parameter
89 *
90 * @var string
91 */
92 protected $displayedLinkHandlerId = '';
93
94 /**
95 * List of available link attribute fields
96 *
97 * @var string[]
98 */
99 protected $linkAttributeFields = [];
100
101 /**
102 * Values of the link attributes
103 *
104 * @var string[]
105 */
106 protected $linkAttributeValues = [];
107
108 /**
109 * @var array
110 */
111 protected $hookObjects = [];
112
113 /**
114 * Constructor
115 */
116 public function __construct()
117 {
118 $this->initHookObjects();
119 $this->init();
120 }
121
122 /**
123 * Initialize the controller
124 */
125 protected function init()
126 {
127 $this->getLanguageService()->includeLLFile('EXT:lang/Resources/Private/Language/locallang_browse_links.xlf');
128 }
129
130 /**
131 * Initialize hook objects implementing the interface
132 *
133 * @throws \UnexpectedValueException
134 */
135 protected function initHookObjects()
136 {
137 $hooks = GeneralUtility::makeInstance(DependencyOrderingService::class)->orderByDependencies(
138 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['LinkBrowser']['hooks'] ?? []
139 );
140 foreach ($hooks as $key => $hook) {
141 $this->hookObjects[] = GeneralUtility::makeInstance($hook['handler']);
142 }
143 }
144
145 /**
146 * Injects the request object for the current request or subrequest
147 * As this controller goes only through the main() method, it is rather simple for now
148 *
149 * @param ServerRequestInterface $request the current request
150 * @param ResponseInterface $response the prepared response object
151 * @return ResponseInterface the response with the content
152 */
153 public function mainAction(ServerRequestInterface $request, ResponseInterface $response)
154 {
155 $this->determineScriptUrl($request);
156 $this->initVariables($request);
157 $this->loadLinkHandlers();
158 $this->initCurrentUrl();
159
160 $menuData = $this->buildMenuArray();
161 $renderLinkAttributeFields = $this->renderLinkAttributeFields();
162 $browserContent = $this->displayedLinkHandler->render($request);
163
164 $this->initDocumentTemplate();
165 $content = $this->doc->startPage('Link Browser');
166 $content .= $this->doc->getFlashMessages();
167
168 if (!empty($this->currentLinkParts)) {
169 $content .= $this->renderCurrentUrl();
170 }
171
172 $options = '';
173 foreach ($menuData as $id => $def) {
174 $class = $def['isActive'] ? ' class="active"' : '';
175
176 $options .= '<li' . $class . '>'
177 . '<a href="' . htmlspecialchars($def['url']) . '" ' . $def['addParams'] . '>' . htmlspecialchars($def['label']) . '</a>'
178 . '</li>';
179 }
180
181 $content .= '<div class="element-browser-panel element-browser-tabs"><ul class="nav nav-tabs" role="tablist">' .
182 $options . '</ul></div>';
183
184 $content .= $renderLinkAttributeFields;
185
186 $content .= $browserContent;
187 $content .= $this->doc->endPage();
188
189 $response->getBody()->write($this->doc->insertStylesAndJS($content));
190 return $response;
191 }
192
193 /**
194 * Sets the script url depending on being a module or script request
195 *
196 * @param ServerRequestInterface $request
197 *
198 * @throws \TYPO3\CMS\Backend\Routing\Exception\ResourceNotFoundException
199 * @throws \TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException
200 */
201 protected function determineScriptUrl(ServerRequestInterface $request)
202 {
203 if ($routePath = $request->getQueryParams()['route']) {
204 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
205 $this->thisScript = (string)$uriBuilder->buildUriFromRoutePath($routePath);
206 } else {
207 $this->thisScript = GeneralUtility::getIndpEnv('SCRIPT_NAME');
208 }
209 }
210
211 /**
212 * @param ServerRequestInterface $request
213 */
214 protected function initVariables(ServerRequestInterface $request)
215 {
216 $queryParams = $request->getQueryParams();
217 $this->displayedLinkHandlerId = $queryParams['act'] ?? '';
218 $this->parameters = $queryParams['P'] ?? [];
219 $this->linkAttributeValues = $queryParams['linkAttributes'] ?? [];
220 }
221
222 /**
223 * @throws \UnexpectedValueException
224 */
225 protected function loadLinkHandlers()
226 {
227 $linkHandlers = $this->getLinkHandlers();
228 if (empty($linkHandlers)) {
229 throw new \UnexpectedValueException('No link handlers are configured. Check page TSconfig TCEMAIN.linkHandler.', 1442787911);
230 }
231
232 $lang = $this->getLanguageService();
233 foreach ($linkHandlers as $identifier => $configuration) {
234 $identifier = rtrim($identifier, '.');
235
236 if (empty($configuration['handler'])) {
237 throw new \UnexpectedValueException(sprintf('Missing handler for link handler "%1$s", check page TSconfig TCEMAIN.linkHandler.%1$s.handler', $identifier), 1494579849);
238 }
239
240 /** @var LinkHandlerInterface $handler */
241 $handler = GeneralUtility::makeInstance($configuration['handler']);
242 $handler->initialize(
243 $this,
244 $identifier,
245 $configuration['configuration.'] ?? []
246 );
247
248 $label = !empty($configuration['label']) ? $lang->sL($configuration['label']) : '';
249 $label = $label ?: $lang->sL('LLL:EXT:recordlist/Resources/Private/Language/locallang.xlf:error.linkHandlerTitleMissing');
250 $this->linkHandlers[$identifier] = [
251 'handlerInstance' => $handler,
252 'label' => htmlspecialchars($label),
253 'displayBefore' => isset($configuration['displayBefore']) ? GeneralUtility::trimExplode(',', $configuration['displayBefore']) : [],
254 'displayAfter' => isset($configuration['displayAfter']) ? GeneralUtility::trimExplode(',', $configuration['displayAfter']) : [],
255 'scanBefore' => isset($configuration['scanBefore']) ? GeneralUtility::trimExplode(',', $configuration['scanBefore']) : [],
256 'scanAfter' => isset($configuration['scanAfter']) ? GeneralUtility::trimExplode(',', $configuration['scanAfter']) : [],
257 'addParams' => $configuration['addParams'] ?? '',
258 ];
259 }
260 }
261
262 /**
263 * Reads the configured link handlers from page TSconfig
264 *
265 * @return array
266 */
267 protected function getLinkHandlers()
268 {
269 $pageTSconfig = BackendUtility::getPagesTSconfig($this->getCurrentPageId());
270 $pageTSconfig = $this->getBackendUser()->getTSConfig('TCEMAIN.linkHandler.', $pageTSconfig);
271 $linkHandlers = (array)$pageTSconfig['properties'];
272
273 foreach ($this->hookObjects as $hookObject) {
274 if (method_exists($hookObject, 'modifyLinkHandlers')) {
275 $linkHandlers = $hookObject->modifyLinkHandlers($linkHandlers, $this->currentLinkParts);
276 }
277 }
278
279 return $linkHandlers;
280 }
281
282 /**
283 * Initialize $this->currentLinkParts and $this->currentLinkHandler
284 */
285 protected function initCurrentUrl()
286 {
287 if (empty($this->currentLinkParts)) {
288 return;
289 }
290
291 $orderedHandlers = GeneralUtility::makeInstance(DependencyOrderingService::class)->orderByDependencies($this->linkHandlers, 'scanBefore', 'scanAfter');
292
293 // find responsible handler for current link
294 foreach ($orderedHandlers as $key => $configuration) {
295 /** @var LinkHandlerInterface $handler */
296 $handler = $configuration['handlerInstance'];
297 if ($handler->canHandleLink($this->currentLinkParts)) {
298 $this->currentLinkHandler = $handler;
299 $this->currentLinkHandlerId = $key;
300 break;
301 }
302 }
303 // reset the link if we have no handler for it
304 if (!$this->currentLinkHandler) {
305 $this->currentLinkParts = [];
306 }
307
308 // overwrite any preexisting
309 foreach ($this->currentLinkParts as $key => $part) {
310 if ($key !== 'url') {
311 $this->linkAttributeValues[$key] = $part;
312 }
313 }
314 }
315
316 /**
317 * Initialize document template object
318 */
319 protected function initDocumentTemplate()
320 {
321 $this->doc = GeneralUtility::makeInstance(DocumentTemplate::class);
322 $this->doc->divClass = 'element-browser';
323
324 foreach ($this->getBodyTagAttributes() as $attributeName => $value) {
325 $this->doc->bodyTagAdditions .= ' ' . $attributeName . '="' . htmlspecialchars($value) . '"';
326 }
327
328 // Finally, add the accumulated JavaScript to the template object:
329 // also unset the default jumpToUrl() function before
330 unset($this->doc->JScodeArray['jumpToUrl']);
331 }
332
333 /**
334 * Render the currently set URL
335 *
336 * @return string
337 */
338 protected function renderCurrentUrl()
339 {
340 return '<!-- Print current URL -->
341 <div class="element-browser-panel element-browser-title">' .
342 htmlspecialchars($this->getLanguageService()->getLL('currentLink')) .
343 ': ' .
344 htmlspecialchars($this->currentLinkHandler->formatCurrentUrl()) .
345 '</div>';
346 }
347
348 /**
349 * Returns an array definition of the top menu
350 *
351 * @return mixed[][]
352 */
353 protected function buildMenuArray()
354 {
355 $allowedItems = $this->getAllowedItems();
356 if ($this->displayedLinkHandlerId && !in_array($this->displayedLinkHandlerId, $allowedItems, true)) {
357 $this->displayedLinkHandlerId = '';
358 }
359
360 $allowedHandlers = array_flip($allowedItems);
361 $menuDef = [];
362 foreach ($this->linkHandlers as $identifier => $configuration) {
363 if (!isset($allowedHandlers[$identifier])) {
364 continue;
365 }
366
367 /** @var LinkHandlerInterface $handlerInstance */
368 $handlerInstance = $configuration['handlerInstance'];
369 $isActive = $this->displayedLinkHandlerId === $identifier || !$this->displayedLinkHandlerId && $handlerInstance === $this->currentLinkHandler;
370 if ($isActive) {
371 $this->displayedLinkHandler = $handlerInstance;
372 if (!$this->displayedLinkHandlerId) {
373 $this->displayedLinkHandlerId = $this->currentLinkHandlerId;
374 }
375 }
376
377 if ($configuration['addParams']) {
378 $addParams = $configuration['addParams'];
379 } else {
380 $parameters = GeneralUtility::implodeArrayForUrl('', $this->getUrlParameters(['act' => $identifier]));
381 $addParams = 'onclick="jumpToUrl(' . htmlspecialchars(GeneralUtility::quoteJSvalue('?' . ltrim($parameters, '&'))) . ');return false;"';
382 }
383 $menuDef[$identifier] = [
384 'isActive' => $isActive,
385 'label' => $configuration['label'],
386 'url' => '#',
387 'addParams' => $addParams,
388 'before' => $configuration['displayBefore'],
389 'after' => $configuration['displayAfter']
390 ];
391 }
392
393 $menuDef = GeneralUtility::makeInstance(DependencyOrderingService::class)->orderByDependencies($menuDef);
394
395 // if there is no active tab
396 if (!$this->displayedLinkHandler) {
397 // empty the current link
398 $this->currentLinkParts = [];
399 $this->currentLinkHandler = null;
400 $this->currentLinkHandler = '';
401 // select first tab
402 reset($menuDef);
403 $this->displayedLinkHandlerId = key($menuDef);
404 $this->displayedLinkHandler = $this->linkHandlers[$this->displayedLinkHandlerId]['handlerInstance'];
405 $menuDef[$this->displayedLinkHandlerId]['isActive'] = true;
406 }
407
408 return $menuDef;
409 }
410
411 /**
412 * Get the allowed items or tabs
413 *
414 * @return string[]
415 */
416 protected function getAllowedItems()
417 {
418 $allowedItems = array_keys($this->linkHandlers);
419
420 foreach ($this->hookObjects as $hookObject) {
421 if (method_exists($hookObject, 'modifyAllowedItems')) {
422 $allowedItems = $hookObject->modifyAllowedItems($allowedItems, $this->currentLinkParts);
423 }
424 }
425
426 // Initializing the action value, possibly removing blinded values etc:
427 $blindLinkOptions = isset($this->parameters['params']['blindLinkOptions'])
428 ? GeneralUtility::trimExplode(',', $this->parameters['params']['blindLinkOptions'])
429 : [];
430 $allowedItems = array_diff($allowedItems, $blindLinkOptions);
431
432 return $allowedItems;
433 }
434
435 /**
436 * Get the allowed link attributes
437 *
438 * @return string[]
439 */
440 protected function getAllowedLinkAttributes()
441 {
442 $allowedLinkAttributes = $this->displayedLinkHandler->getLinkAttributes();
443
444 // Removing link fields if configured
445 $blindLinkFields = isset($this->parameters['params']['blindLinkFields'])
446 ? GeneralUtility::trimExplode(',', $this->parameters['params']['blindLinkFields'], true)
447 : [];
448 $allowedLinkAttributes = array_diff($allowedLinkAttributes, $blindLinkFields);
449
450 return $allowedLinkAttributes;
451 }
452
453 /**
454 * Renders the link attributes for the selected link handler
455 *
456 * @return string
457 */
458 public function renderLinkAttributeFields()
459 {
460 $fieldRenderingDefinitions = $this->getLinkAttributeFieldDefinitions();
461
462 $fieldRenderingDefinitions = $this->displayedLinkHandler->modifyLinkAttributes($fieldRenderingDefinitions);
463
464 $this->linkAttributeFields = $this->getAllowedLinkAttributes();
465
466 $content = '';
467 foreach ($this->linkAttributeFields as $attribute) {
468 $content .= $fieldRenderingDefinitions[$attribute];
469 }
470
471 // add update button if appropriate
472 if (!empty($this->currentLinkParts) && $this->displayedLinkHandler === $this->currentLinkHandler && $this->currentLinkHandler->isUpdateSupported()) {
473 $content .= '
474 <form action="" name="lparamsform" id="lparamsform" class="form-horizontal">
475 <div class="form-group form-group-sm">
476 <div class="col-xs-12">
477 <input class="btn btn-default t3js-linkCurrent" type="submit" value="' . htmlspecialchars($this->getLanguageService()->getLL('update')) . '" />
478 </div>
479 </div>
480 </form>';
481 }
482
483 return '<div class="element-browser-panel element-browser-attributes">' . $content . '</div>';
484 }
485
486 /**
487 * Create an array of link attribute field rendering definitions
488 *
489 * @return string[]
490 */
491 protected function getLinkAttributeFieldDefinitions()
492 {
493 $lang = $this->getLanguageService();
494
495 $fieldRenderingDefinitions = [];
496 $fieldRenderingDefinitions['target'] = '
497 <!-- Selecting target for link: -->
498 <form action="" name="ltargetform" id="ltargetform" class="t3js-dummyform form-horizontal">
499 <div class="form-group form-group-sm" id="typo3-linkTarget">
500 <label class="col-xs-4 control-label">' . htmlspecialchars($lang->getLL('target')) . '</label>
501 <div class="col-xs-3">
502 <input type="text" name="ltarget" class="t3js-linkTarget form-control"
503 value="' . htmlspecialchars($this->linkAttributeValues['target']) . '" />
504 </div>
505 <div class="col-xs-5">
506 <select name="ltarget_type" class="t3js-targetPreselect form-control">
507 <option value=""></option>
508 <option value="_top">' . htmlspecialchars($lang->getLL('top')) . '</option>
509 <option value="_blank">' . htmlspecialchars($lang->getLL('newWindow')) . '</option>
510 </select>
511 </div>
512 </div>
513 </form>';
514
515 $fieldRenderingDefinitions['title'] = '
516 <!-- Selecting title for link: -->
517 <form action="" name="ltitleform" id="ltitleform" class="t3js-dummyform form-horizontal">
518 <div class="form-group form-group-sm" id="typo3-linkTitle">
519 <label class="col-xs-4 control-label">' . htmlspecialchars($lang->getLL('title')) . '</label>
520 <div class="col-xs-8">
521 <input type="text" name="ltitle" class="form-control"
522 value="' . htmlspecialchars($this->linkAttributeValues['title']) . '" />
523 </div>
524 </div>
525 </form>';
526
527 $fieldRenderingDefinitions['class'] = '
528 <!-- Selecting class for link: -->
529 <form action="" name="lclassform" id="lclassform" class="t3js-dummyform form-horizontal">
530 <div class="form-group form-group-sm" id="typo3-linkClass">
531 <label class="col-xs-4 control-label">' . htmlspecialchars($lang->getLL('class')) . '</label>
532 <div class="col-xs-8">
533 <input type="text" name="lclass" class="form-control"
534 value="' . htmlspecialchars($this->linkAttributeValues['class']) . '" />
535 </div>
536 </div>
537 </form>';
538
539 $fieldRenderingDefinitions['params'] = '
540 <!-- Selecting params for link: -->
541 <form action="" name="lparamsform" id="lparamsform" class="t3js-dummyform form-horizontal">
542 <div class="form-group form-group-sm" id="typo3-linkParams">
543 <label class="col-xs-4 control-label">' . htmlspecialchars($lang->getLL('params')) . '</label>
544 <div class="col-xs-8">
545 <input type="text" name="lparams" class="form-control"
546 value="' . htmlspecialchars($this->linkAttributeValues['params']) . '" />
547 </div>
548 </div>
549 </form>';
550
551 return $fieldRenderingDefinitions;
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' => $overrides['act'] ?? $this->displayedLinkHandlerId
563 ];
564 }
565
566 /**
567 * Get attributes for the body tag
568 *
569 * @return string[] Array of body-tag attributes
570 */
571 protected function getBodyTagAttributes()
572 {
573 $parameters = [];
574 $parameters['uid'] = $this->parameters['uid'];
575 $parameters['pid'] = $this->parameters['pid'];
576 $parameters['itemName'] = $this->parameters['itemName'];
577 $parameters['formName'] = $this->parameters['formName'];
578 $parameters['params']['allowedExtensions'] = $this->parameters['params']['allowedExtensions'] ?? '';
579 $parameters['params']['blindLinkOptions'] = $this->parameters['params']['blindLinkOptions'] ?? '';
580 $parameters['params']['blindLinkFields'] = $this->parameters['params']['blindLinkFields'] ?? '';
581 $addPassOnParams = GeneralUtility::implodeArrayForUrl('P', $parameters);
582
583 $attributes = $this->displayedLinkHandler->getBodyTagAttributes();
584 return array_merge(
585 $attributes,
586 [
587 'data-this-script-url' => strpos($this->thisScript, '?') === false ? $this->thisScript . '?' : $this->thisScript . '&',
588 'data-url-parameters' => json_encode($this->getUrlParameters()),
589 'data-parameters' => json_encode($this->parameters),
590 'data-add-on-params' => $addPassOnParams,
591 'data-link-attribute-fields' => json_encode($this->linkAttributeFields)
592 ]
593 );
594 }
595
596 /**
597 * Return the ID of current page
598 *
599 * @return int
600 */
601 abstract protected function getCurrentPageId();
602
603 /**
604 * @return array
605 */
606 public function getParameters()
607 {
608 return $this->parameters;
609 }
610
611 /**
612 * Retrieve the configuration
613 *
614 * @return array
615 */
616 public function getConfiguration()
617 {
618 return [];
619 }
620
621 /**
622 * @return string
623 */
624 public function getDisplayedLinkHandlerId()
625 {
626 return $this->displayedLinkHandlerId;
627 }
628
629 /**
630 * @return string
631 */
632 public function getScriptUrl()
633 {
634 return $this->thisScript;
635 }
636
637 /**
638 * @return LanguageService
639 */
640 protected function getLanguageService()
641 {
642 return $GLOBALS['LANG'];
643 }
644
645 /**
646 * @return BackendUserAuthentication
647 */
648 protected function getBackendUser()
649 {
650 return $GLOBALS['BE_USER'];
651 }
652 }