[BUGFIX] Refer to correct naming of link handler TSconfig option
[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 * @return void
127 */
128 protected function init()
129 {
130 $this->getLanguageService()->includeLLFile('EXT:lang/locallang_browse_links.xlf');
131 }
132
133 /**
134 * Initialize hook objects implementing the interface
135 *
136 * @throws \UnexpectedValueException
137 * @return void
138 */
139 protected function initHookObjects()
140 {
141 if (
142 isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['LinkBrowser']['hooks'])
143 && is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['LinkBrowser']['hooks'])
144 ) {
145 $hooks = GeneralUtility::makeInstance(DependencyOrderingService::class)->orderByDependencies(
146 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['LinkBrowser']['hooks']
147 );
148 foreach ($hooks as $key => $hook) {
149 $this->hookObjects[] = GeneralUtility::makeInstance($hook['handler']);
150 }
151 }
152 }
153
154 /**
155 * Injects the request object for the current request or subrequest
156 * As this controller goes only through the main() method, it is rather simple for now
157 *
158 * @param ServerRequestInterface $request the current request
159 * @param ResponseInterface $response the prepared response object
160 * @return ResponseInterface the response with the content
161 */
162 public function mainAction(ServerRequestInterface $request, ResponseInterface $response)
163 {
164 $this->determineScriptUrl($request);
165 $this->initVariables($request);
166 $this->loadLinkHandlers();
167 $this->initCurrentUrl();
168
169 $menuData = $this->buildMenuArray();
170 $renderLinkAttributeFields = $this->renderLinkAttributeFields();
171 $browserContent = $this->displayedLinkHandler->render($request);
172
173 $this->initDocumentTemplate();
174 $content = $this->doc->startPage('Link Browser');
175 $content .= $this->doc->getFlashMessages();
176
177 if (!empty($this->currentLinkParts)) {
178 $content .= $this->renderCurrentUrl();
179 }
180 $content .= $this->doc->getTabMenuRaw($menuData);
181 $content .= $renderLinkAttributeFields;
182
183 $content .= '<div class="linkBrowser-tabContent">' . $browserContent . '</div>';
184 $content .= $this->doc->endPage();
185
186 $response->getBody()->write($this->doc->insertStylesAndJS($content));
187 return $response;
188 }
189
190 /**
191 * Sets the script url depending on being a module or script request
192 *
193 * @param ServerRequestInterface $request
194 *
195 * @throws \TYPO3\CMS\Backend\Routing\Exception\ResourceNotFoundException
196 * @throws \TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException
197 */
198 protected function determineScriptUrl(ServerRequestInterface $request)
199 {
200 if ($routePath = $request->getQueryParams()['route']) {
201 $router = GeneralUtility::makeInstance(Router::class);
202 $route = $router->match($routePath);
203 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
204 $this->thisScript = (string)$uriBuilder->buildUriFromRoute($route->getOption('_identifier'));
205 } elseif ($moduleName = $request->getQueryParams()['M']) {
206 $this->thisScript = BackendUtility::getModuleUrl($moduleName);
207 } else {
208 $this->thisScript = GeneralUtility::getIndpEnv('SCRIPT_NAME');
209 }
210 }
211
212 /**
213 * @param ServerRequestInterface $request
214 */
215 protected function initVariables(ServerRequestInterface $request)
216 {
217 $queryParams = $request->getQueryParams();
218 $act = isset($queryParams['act']) ? $queryParams['act'] : '';
219 // @deprecated since CMS 7, remove with CMS 8
220 if (strpos($act, '|')) {
221 GeneralUtility::deprecationLog('Using multiple values for the "act" parameter in the link wizard is deprecated. Only a single value is allowed. Values were: ' . $act);
222 $act = array_shift(explode('|', $act));
223 }
224 $this->displayedLinkHandlerId = $act;
225 $this->parameters = isset($queryParams['P']) ? $queryParams['P'] : [];
226 $this->linkAttributeValues = isset($queryParams['linkAttributes']) ? $queryParams['linkAttributes'] : [];
227 }
228
229 /**
230 * @return void
231 * @throws \UnexpectedValueException
232 */
233 protected function loadLinkHandlers()
234 {
235 $linkHandlers = $this->getLinkHandlers();
236 if (empty($linkHandlers)) {
237 throw new \UnexpectedValueException('No link handlers are configured. Check page TSconfig TCEMAIN.linkHandler.', 1442787911);
238 }
239
240 $lang = $this->getLanguageService();
241 foreach ($linkHandlers as $identifier => $configuration) {
242 $identifier = rtrim($identifier, '.');
243 /** @var LinkHandlerInterface $handler */
244 $handler = GeneralUtility::makeInstance($configuration['handler']);
245 $handler->initialize(
246 $this,
247 $identifier,
248 isset($configuration['configuration.']) ? $configuration['configuration.'] : []
249 );
250
251 $this->linkHandlers[$identifier] = [
252 'handlerInstance' => $handler,
253 'label' => $lang->sL($configuration['label'], true),
254 'displayBefore' => isset($configuration['displayBefore']) ? GeneralUtility::trimExplode(',', $configuration['displayBefore']) : [],
255 'displayAfter' => isset($configuration['displayAfter']) ? GeneralUtility::trimExplode(',', $configuration['displayAfter']) : [],
256 'scanBefore' => isset($configuration['scanBefore']) ? GeneralUtility::trimExplode(',', $configuration['scanBefore']) : [],
257 'scanAfter' => isset($configuration['scanAfter']) ? GeneralUtility::trimExplode(',', $configuration['scanAfter']) : [],
258 'addParams' => isset($configuration['addParams']) ? $configuration['addParams'] : '',
259 ];
260 }
261 }
262
263 /**
264 * Reads the configured link handlers from page TSconfig
265 *
266 * @return array
267 */
268 protected function getLinkHandlers()
269 {
270 $pageTSconfig = BackendUtility::getPagesTSconfig($this->getCurrentPageId());
271 $pageTSconfig = $this->getBackendUser()->getTSConfig('TCEMAIN.linkHandler.', $pageTSconfig);
272 $linkHandlers = (array)$pageTSconfig['properties'];
273
274 foreach ($this->hookObjects as $hookObject) {
275 if (method_exists($hookObject, 'modifyLinkHandlers')) {
276 $linkHandlers = $hookObject->modifyLinkHandlers($linkHandlers, $this->currentLinkParts);
277 }
278 }
279
280 return $linkHandlers;
281 }
282
283 /**
284 * Initialize $this->currentLinkParts and $this->currentLinkHandler
285 *
286 * @return void
287 */
288 protected function initCurrentUrl()
289 {
290 if (empty($this->currentLinkParts)) {
291 return;
292 }
293
294 $orderedHandlers = GeneralUtility::makeInstance(DependencyOrderingService::class)->orderByDependencies($this->linkHandlers, 'scanBefore', 'scanAfter');
295
296 // find responsible handler for current link
297 foreach ($orderedHandlers as $key => $configuration) {
298 /** @var LinkHandlerInterface $handler */
299 $handler = $configuration['handlerInstance'];
300 if ($handler->canHandleLink($this->currentLinkParts)) {
301 $this->currentLinkHandler = $handler;
302 $this->currentLinkHandlerId = $key;
303 break;
304 }
305 }
306 // reset the link if we have no handler for it
307 if (!$this->currentLinkHandler) {
308 $this->currentLinkParts = [];
309 }
310
311 // overwrite any preexisting
312 foreach ($this->currentLinkParts as $key => $part) {
313 if ($key !== 'url') {
314 $this->linkAttributeValues[$key] = $part;
315 }
316 }
317 }
318
319 /**
320 * Initialize document template object
321 *
322 * @return void
323 */
324 protected function initDocumentTemplate()
325 {
326 $this->doc = GeneralUtility::makeInstance(DocumentTemplate::class);
327 $this->doc->bodyTagId = 'typo3-browse-links-php';
328
329 foreach ($this->getBodyTagAttributes() as $attributeName => $value) {
330 $this->doc->bodyTagAdditions .= ' ' . $attributeName . '="' . htmlspecialchars($value) . '"';
331 }
332
333 // Finally, add the accumulated JavaScript to the template object:
334 // also unset the default jumpToUrl() function before
335 unset($this->doc->JScodeArray['jumpToUrl']);
336 }
337
338 /**
339 * Render the currently set URL
340 *
341 * @return string
342 */
343 protected function renderCurrentUrl()
344 {
345 return '<!-- Print current URL -->
346 <table border="0" cellpadding="0" cellspacing="0" id="typo3-curUrl">
347 <tr>
348 <td>' . $this->getLanguageService()->getLL('currentLink', true) . ': ' . htmlspecialchars($this->currentLinkHandler->formatCurrentUrl()) . '</td>
349 </tr>
350 </table>';
351 }
352
353 /**
354 * Returns an array definition of the top menu
355 *
356 * @return mixed[][]
357 */
358 protected function buildMenuArray()
359 {
360 $allowedItems = $this->getAllowedItems();
361 if ($this->displayedLinkHandlerId && !in_array($this->displayedLinkHandlerId, $allowedItems, true)) {
362 $this->displayedLinkHandlerId = '';
363 }
364
365 $allowedHandlers = array_flip($allowedItems);
366 $menuDef = array();
367 foreach ($this->linkHandlers as $identifier => $configuration) {
368 if (!isset($allowedHandlers[$identifier])) {
369 continue;
370 }
371
372 /** @var LinkHandlerInterface $handlerInstance */
373 $handlerInstance = $configuration['handlerInstance'];
374 $isActive = $this->displayedLinkHandlerId === $identifier || !$this->displayedLinkHandlerId && $handlerInstance === $this->currentLinkHandler;
375 if ($isActive) {
376 $this->displayedLinkHandler = $handlerInstance;
377 if (!$this->displayedLinkHandlerId) {
378 $this->displayedLinkHandlerId = $this->currentLinkHandlerId;
379 }
380 }
381
382 if ($configuration['addParams']) {
383 $addParams = $configuration['addParams'];
384 } else {
385 $parameters = GeneralUtility::implodeArrayForUrl('', $this->getUrlParameters(['act' => $identifier]));
386 $addParams = 'onclick="jumpToUrl(' . GeneralUtility::quoteJSvalue('?' . ltrim($parameters, '&')) . ');return false;"';
387 }
388 $menuDef[$identifier] = [
389 'isActive' => $isActive,
390 'label' => $configuration['label'],
391 'url' => '#',
392 'addParams' => $addParams,
393 'before' => $configuration['displayBefore'],
394 'after' => $configuration['displayAfter']
395 ];
396 }
397
398 $menuDef = GeneralUtility::makeInstance(DependencyOrderingService::class)->orderByDependencies($menuDef);
399
400 // if there is no active tab
401 if (!$this->displayedLinkHandler) {
402 // empty the current link
403 $this->currentLinkParts = [];
404 $this->currentLinkHandler = null;
405 $this->currentLinkHandler = '';
406 // select first tab
407 reset($menuDef);
408 $this->displayedLinkHandlerId = key($menuDef);
409 $this->displayedLinkHandler = $this->linkHandlers[$this->displayedLinkHandlerId]['handlerInstance'];
410 $menuDef[$this->displayedLinkHandlerId]['isActive'] = true;
411 }
412
413 return $menuDef;
414 }
415
416 /**
417 * Get the allowed items or tabs
418 *
419 * @return string[]
420 */
421 protected function getAllowedItems()
422 {
423 $allowedItems = array_keys($this->linkHandlers);
424
425 foreach ($this->hookObjects as $hookObject) {
426 if (method_exists($hookObject, 'modifyAllowedItems')) {
427 $allowedItems = $hookObject->modifyAllowedItems($allowedItems, $this->currentLinkParts);
428 }
429 }
430
431 // Initializing the action value, possibly removing blinded values etc:
432 $blindLinkOptions = isset($this->parameters['params']['blindLinkOptions'])
433 ? GeneralUtility::trimExplode(',', $this->parameters['params']['blindLinkOptions'])
434 : [];
435 $allowedItems = array_diff($allowedItems, $blindLinkOptions);
436
437 return $allowedItems;
438 }
439
440 /**
441 * Get the allowed link attributes
442 *
443 * @return string[]
444 */
445 protected function getAllowedLinkAttributes()
446 {
447 $allowedLinkAttributes = $this->displayedLinkHandler->getLinkAttributes();
448
449 // Removing link fields if configured
450 $blindLinkFields = isset($this->parameters['params']['blindLinkFields'])
451 ? GeneralUtility::trimExplode(',', $this->parameters['params']['blindLinkFields'], true)
452 : [];
453 $allowedLinkAttributes = array_diff($allowedLinkAttributes, $blindLinkFields);
454
455 return $allowedLinkAttributes;
456 }
457
458 /**
459 * Renders the link attributes for the selected link handler
460 *
461 * @return string
462 */
463 public function renderLinkAttributeFields()
464 {
465 $fieldRenderingDefinitions = $this->getLinkAttributeFieldDefinitions();
466
467 $fieldRenderingDefinitions = $this->displayedLinkHandler->modifyLinkAttributes($fieldRenderingDefinitions);
468
469 $this->linkAttributeFields = $this->getAllowedLinkAttributes();
470
471 $content = '';
472 foreach ($this->linkAttributeFields as $attribute) {
473 $content .= $fieldRenderingDefinitions[$attribute];
474 }
475
476 // add update button if appropriate
477 if (!empty($this->currentLinkParts) && $this->displayedLinkHandler === $this->currentLinkHandler && $this->currentLinkHandler->isUpdateSupported()) {
478 $content .= '
479 <form action="" name="lparamsform" id="lparamsform">
480 <table border="0" cellpadding="2" cellspacing="1" id="typo3-linkParams">
481 <tr><td>
482 <input class="btn btn-default t3js-linkCurrent" type="submit" value="' . $this->getLanguageService()->getLL('update', true) . '" />
483 </td></tr>
484 </table>
485 </form><br /><br />';
486 }
487
488 return $content;
489 }
490
491 /**
492 * Create an array of link attribute field rendering definitions
493 *
494 * @return string[]
495 */
496 protected function getLinkAttributeFieldDefinitions()
497 {
498 $lang = $this->getLanguageService();
499
500 $fieldRenderingDefinitions = [];
501 $fieldRenderingDefinitions['target'] = '
502 <!--
503 Selecting target for link:
504 -->
505 <form action="" name="ltargetform" id="ltargetform" class="t3js-dummyform">
506 <table border="0" cellpadding="2" cellspacing="1" id="typo3-linkTarget">
507 <tr>
508 <td style="width: 96px;">' . $lang->getLL('target', true) . ':</td>
509 <td>
510 <input type="text" name="ltarget" class="t3js-linkTarget" value="' . htmlspecialchars($this->linkAttributeValues['target']) . '" />
511 <select name="ltarget_type" class="t3js-targetPreselect">
512 <option value=""></option>
513 <option value="_top">' . $lang->getLL('top', true) . '</option>
514 <option value="_blank">' . $lang->getLL('newWindow', true) . '</option>
515 </select>
516 </td>
517 </tr>
518 </table>
519 </form>';
520
521 $fieldRenderingDefinitions['title'] = '
522 <!--
523 Selecting title for link:
524 -->
525 <form action="" name="ltitleform" id="ltitleform" class="t3js-dummyform">
526 <table border="0" cellpadding="2" cellspacing="1" id="typo3-linkTitle">
527 <tr>
528 <td style="width: 96px;">' . $lang->getLL('title', true) . '</td>
529 <td><input type="text" name="ltitle" class="typo3-link-input" value="' . htmlspecialchars($this->linkAttributeValues['title']) . '" /></td>
530 </tr>
531 </table>
532 </form>
533 ';
534
535 $fieldRenderingDefinitions['class'] = '
536 <!--
537 Selecting class for link:
538 -->
539 <form action="" name="lclassform" id="lclassform" class="t3js-dummyform">
540 <table border="0" cellpadding="2" cellspacing="1" id="typo3-linkClass">
541 <tr>
542 <td style="width: 96px;">' . $lang->getLL('class', true) . '</td>
543 <td><input type="text" name="lclass" class="typo3-link-input" value="' . htmlspecialchars($this->linkAttributeValues['class']) . '" /></td>
544 </tr>
545 </table>
546 </form>
547 ';
548
549 $fieldRenderingDefinitions['params'] = '
550 <!--
551 Selecting params for link:
552 -->
553 <form action="" name="lparamsform" id="lparamsform" class="t3js-dummyform">
554 <table border="0" cellpadding="2" cellspacing="1" id="typo3-linkParams">
555 <tr>
556 <td style="width: 96px;">' . $lang->getLL('params', true) . '</td>
557 <td><input type="text" name="lparams" class="typo3-link-input" value="' . htmlspecialchars($this->linkAttributeValues['params']) . '" /></td>
558 </tr>
559 </table>
560 </form>
561 ';
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 }