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