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