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