[FEATURE] Integration of a generic record link handler 26/51526/32
authorGeorg Ringer <georg.ringer@gmail.com>
Fri, 3 Feb 2017 20:40:35 +0000 (21:40 +0100)
committerAndreas Fernandez <typo3@scripting-base.de>
Tue, 7 Feb 2017 21:34:16 +0000 (22:34 +0100)
Enable linking to any record by migrating the code of
EXT:linkhandler into the core.

Resolves: #66373
Resolves: #66374
Releases: master
Change-Id: I749103e201d387ae826575c6acb3cdcdf639e966
Reviewed-on: https://review.typo3.org/51526
Reviewed-by: Georg Ringer <georg.ringer@gmail.com>
Tested-by: Georg Ringer <georg.ringer@gmail.com>
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Andreas Fernandez <typo3@scripting-base.de>
Tested-by: Andreas Fernandez <typo3@scripting-base.de>
14 files changed:
typo3/sysext/backend/Classes/Form/Element/InputLinkElement.php
typo3/sysext/core/Classes/Database/SoftReferenceIndex.php
typo3/sysext/core/Classes/LinkHandling/LegacyLinkNotationConverter.php
typo3/sysext/core/Classes/LinkHandling/LinkService.php
typo3/sysext/core/Classes/LinkHandling/RecordLinkHandler.php [new file with mode: 0644]
typo3/sysext/core/Configuration/DefaultConfiguration.php
typo3/sysext/core/Documentation/Changelog/master/Feature-79626-IntegrateRecordLinkHandler.rst [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/LinkHandling/RecordLinkHandlerTest.php [new file with mode: 0644]
typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php
typo3/sysext/recordlist/Classes/Browser/RecordBrowser.php [new file with mode: 0644]
typo3/sysext/recordlist/Classes/LinkHandler/RecordLinkHandler.php [new file with mode: 0644]
typo3/sysext/recordlist/Classes/Tree/View/RecordBrowserPageTreeView.php [new file with mode: 0644]
typo3/sysext/recordlist/Resources/Private/Templates/LinkBrowser/Record.html [new file with mode: 0644]
typo3/sysext/recordlist/Resources/Public/JavaScript/RecordLinkHandler.js [new file with mode: 0644]

index f67b5b7..754d771 100644 (file)
@@ -201,14 +201,15 @@ class InputLinkElement extends AbstractFormElement
         $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
 
         $linkExplanation = $this->getLinkExplanation($itemValue);
+        $explanation = htmlspecialchars($linkExplanation['text']);
 
         $expansionHtml = [];
         $expansionHtml[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
         $expansionHtml[] =  '<div class="form-wizards-wrap">';
         $expansionHtml[] =      '<div class="form-wizards-element">';
-        $expansionHtml[] =          '<div class="input-group t3js-form-field-inputlink">';
+        $expansionHtml[] =          '<div class="input-group t3js-form-field-inputlink" data-toggle="tooltip" data-title="' . $explanation . '">';
         $expansionHtml[] =              '<span class="input-group-addon">' . $linkExplanation['icon'] . '</span>';
-        $expansionHtml[] =              '<input class="form-control t3js-form-field-inputlink-explanation" disabled value="' . htmlspecialchars($linkExplanation['text']) . '">';
+        $expansionHtml[] =              '<input class="form-control t3js-form-field-inputlink-explanation" disabled value="' . $explanation . '">';
         $expansionHtml[] =              '<input type="text"' . GeneralUtility::implodeAttributes($attributes, true) . ' />';
         $expansionHtml[] =              '<span class="input-group-btn">';
         $expansionHtml[] =                  '<button class="btn btn-default t3js-form-field-inputlink-explanation-toggle" type="button">';
@@ -315,20 +316,20 @@ class InputLinkElement extends AbstractFormElement
                 // Is this a real page
                 if ($pageRecord['uid']) {
                     $data = [
-                        'text' => htmlspecialchars($pageRecord['_thePathFull']) . '[' . $pageRecord['uid'] . ']',
+                        'text' => $pageRecord['_thePathFull'] . '[' . $pageRecord['uid'] . ']',
                         'icon' => $this->iconFactory->getIconForRecord('pages', $pageRecord, Icon::SIZE_SMALL)->render()
                     ];
                 }
                 break;
             case LinkService::TYPE_EMAIL:
                 $data = [
-                    'text' => htmlspecialchars($linkData['email']),
+                    'text' => $linkData['email'],
                     'icon' => $this->iconFactory->getIcon('content-elements-mailform', Icon::SIZE_SMALL)->render()
                 ];
                 break;
             case LinkService::TYPE_URL:
                 $data = [
-                    'text' => htmlspecialchars($linkData['url']),
+                    'text' => $linkData['url'],
                     'icon' => $this->iconFactory->getIcon('apps-pagetree-page-shortcut-external', Icon::SIZE_SMALL)->render()
 
                 ];
@@ -338,7 +339,7 @@ class InputLinkElement extends AbstractFormElement
                 $file = $linkData['file'];
                 if ($file) {
                     $data = [
-                        'text' => htmlspecialchars($file->getPublicUrl()),
+                        'text' => $file->getPublicUrl(),
                         'icon' => $this->iconFactory->getIconForFileExtension($file->getExtension(), Icon::SIZE_SMALL)->render()
                     ];
                 }
@@ -348,16 +349,27 @@ class InputLinkElement extends AbstractFormElement
                 $folder = $linkData['folder'];
                 if ($folder) {
                     $data = [
-                        'text' => htmlspecialchars($folder->getPublicUrl()),
+                        'text' => $folder->getPublicUrl(),
                         'icon' => $this->iconFactory->getIcon('apps-filetree-folder-default', Icon::SIZE_SMALL)->render()
                     ];
                 }
                 break;
+            case LinkService::TYPE_RECORD:
+                $table = $this->data['pageTsConfig']['TCEMAIN.']['linkHandler.'][$linkData['identifier'] . '.']['configuration.']['table'];
+                $record = BackendUtility::getRecord($table, $linkData['uid']);
+                $recordTitle = BackendUtility::getRecordTitle($table, $record);
+                $tableTitle = $this->getLanguageService()->sL($GLOBALS['TCA'][$table]['ctrl']['title']);
+                $data = [
+                    'text' => sprintf('%s [%s:%d]', $recordTitle, $tableTitle, $linkData['uid']),
+                    'icon' => $this->iconFactory->getIconForRecord($table, $record, Icon::SIZE_SMALL)->render()
+                ];
+                break;
             default:
                 $data = [
-                    'text' => htmlspecialchars('not implemented type ' . $linkData['type']),
+                    'text' => 'not implemented type ' . $linkData['type'],
                     'icon' => ''
                 ];
+                // @todo this needs a hook for being extensible for other link types. forge #79647
         }
 
         $additionalAttributes = [];
index 6dadd6d..b289128 100644 (file)
@@ -14,6 +14,7 @@ namespace TYPO3\CMS\Core\Database;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Core\LinkHandling\LinkService;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Frontend\Service\TypoLinkCodecService;
 
@@ -413,6 +414,11 @@ class SoftReferenceIndex
             return $finalTagParts;
         }
 
+        if ($pU['scheme'] === 't3' && $pU['host'] === LinkService::TYPE_RECORD) {
+            $finalTagParts['LINK_TYPE'] = LinkService::TYPE_RECORD;
+            $finalTagParts['url'] = $link_param;
+        }
+
         list($linkHandlerKeyword, $linkHandlerValue) = explode(':', trim($link_param), 2);
 
         // Dispatch available signal slots.
@@ -578,6 +584,16 @@ class SoftReferenceIndex
                     }
                 }
                 break;
+            case LinkService::TYPE_RECORD:
+                $elements[$tokenID . ':' . $idx]['subst'] = [
+                    'type' => 'db',
+                    'recordRef' => $tLP['table'] . ':' . $tLP['uid'],
+                    'tokenID' => $tokenID,
+                    'tokenValue' => $content,
+                ];
+
+                $content = '{softref:' . $tokenID . '}';
+                break;
             default:
                 $linkHandlerFound = false;
                 list($linkHandlerFound, $tLP, $content, $newElements) = $this->emitSetTypoLinkPartsElement($linkHandlerFound, $tLP, $content, $elements, $idx, $tokenID);
index b947373..6d3bfae 100644 (file)
@@ -70,6 +70,12 @@ class LegacyLinkNotationConverter
             list($linkHandlerKeyword, $linkHandlerValue) = explode(':', $linkParameter, 2);
             $result['type'] = strtolower(trim($linkHandlerKeyword));
             $result['url'] = $linkHandlerValue;
+            if ($result['type'] === LinkService::TYPE_RECORD) {
+                list($a['identifier'], $a['table'], $a['uid']) = explode(':', $linkHandlerValue);
+                $result['url'] = $a;
+            } else {
+                // @TODO add a hook for old typolinkLinkHandler to convert their values properly, forge #79647
+            }
         } else {
             // special handling without a scheme
             $isLocalFile = 0;
index 1c22117..29ec07b 100644 (file)
@@ -31,8 +31,11 @@ class LinkService implements SingletonInterface
     const TYPE_EMAIL = 'email';
     const TYPE_FILE = 'file';
     const TYPE_FOLDER = 'folder';
+    const TYPE_RECORD = 'record';
     const TYPE_UNKNOWN = 'unknown';
 
+    // @TODO There needs to be an API to make these types extensible as the former 'typolinkLinkHandler' does not work anymore! forge #79647
+
     /**
      * All registered LinkHandlers
      *
@@ -95,7 +98,7 @@ class LinkService implements SingletonInterface
             $urnParsed = parse_url($urn);
             $type = $urnParsed['host'];
             if (isset($urnParsed['query'])) {
-                parse_str($urnParsed['query'], $data);
+                parse_str(htmlspecialchars_decode($urnParsed['query']), $data);
             } else {
                 $data = [];
             }
diff --git a/typo3/sysext/core/Classes/LinkHandling/RecordLinkHandler.php b/typo3/sysext/core/Classes/LinkHandling/RecordLinkHandler.php
new file mode 100644 (file)
index 0000000..2b73825
--- /dev/null
@@ -0,0 +1,63 @@
+<?php
+declare(strict_types=1);
+namespace TYPO3\CMS\Core\LinkHandling;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Resolves links to records and the parameters given
+ */
+class RecordLinkHandler implements LinkHandlingInterface
+{
+    /**
+     * The Base URN for this link handling to act on
+     *
+     * @var string
+     */
+    protected $baseUrn = 't3://record';
+
+    /**
+     * Returns all valid parameters for linking to a TYPO3 page as a string
+     *
+     * @param array $parameters
+     * @return string
+     * @throws \InvalidArgumentException
+     */
+    public function asString(array $parameters): string
+    {
+        if (empty($parameters['identifier']) || empty($parameters['uid'])) {
+            throw new \InvalidArgumentException('The RecordLinkHandler expects identifier and uid as $parameter configuration.', 1486155150);
+        }
+        $urn = $this->baseUrn;
+        $urn .= sprintf('?identifier=%s&uid=%s', $parameters['identifier'], $parameters['uid']);
+
+        return $urn;
+    }
+
+    /**
+     * Returns all relevant information built in the link to a page (see asString())
+     *
+     * @param array $data
+     * @return array
+     * @throws \InvalidArgumentException
+     */
+    public function resolveHandlerData(array $data): array
+    {
+        if (empty($data['identifier']) || empty($data['uid'])) {
+            throw new \InvalidArgumentException('The RecordLinkHandler expects identifier, uid as $data configuration', 1486155151);
+        }
+
+        return $data;
+    }
+}
index 64b9256..3b6837d 100644 (file)
@@ -302,6 +302,7 @@ return [
             'folder' => \TYPO3\CMS\Core\LinkHandling\FolderLinkHandler::class,
             'url'    => \TYPO3\CMS\Core\LinkHandling\UrlLinkHandler::class,
             'email'  => \TYPO3\CMS\Core\LinkHandling\EmailLinkHandler::class,
+            'record' => \TYPO3\CMS\Core\LinkHandling\RecordLinkHandler::class,
         ],
         'livesearch' => [],    // Array: keywords used for commands to search for specific tables
         'isInitialInstallationInProgress' => false,        // Boolean: If TRUE, the installation is 'in progress'. This value is handled within the install tool step installer internally.
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-79626-IntegrateRecordLinkHandler.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-79626-IntegrateRecordLinkHandler.rst
new file mode 100644 (file)
index 0000000..851008d
--- /dev/null
@@ -0,0 +1,52 @@
+.. include:: ../../Includes.txt
+
+===============================================
+Feature: #79626 - Integrate record link handler
+===============================================
+
+See :issue:`79626`
+
+Description
+===========
+
+The functionality of the extension `linkhandler` has been integrated into the core. It enables editors to link to single records.
+
+The configuration consists of the following parts:
+
+*PageTsConfig* is used to create a new tab in the LinkBrowser to be able to select records:
+
+.. code-block:: typoscript
+
+    TCEMAIN.linkHandler.anIdentifier {
+        handler = TYPO3\CMS\Recordlist\LinkHandler\RecordLinkHandler
+        label = LLL:EXT:extension/Resources/Private/Language/locallang.xlf:link.customTab
+        configuration {
+            table = tx_example_domain_model_item
+        }
+        scanBefore = page
+    }
+
+The following optional configuration is available:
+
+- :typoscript:`configuration.hidePageTree = 1`: Hide the page tree in the link browser
+- :typoscript:`configuration.storagePid = 1`: Let the link browser start with the given page
+- :typoscript:`configuration.pageTreeMountPoints = 123,456`: Mount the given pages instead of the regular page tree
+
+
+*TypoScript* is used to generate the actual link in the frontend
+
+.. code-block:: typoscript
+
+    config.recordLinks.anIdentifier {
+        // Do not force link generation when the record is hidden
+        forceLink = 0
+
+        typolink {
+            parameter = 123
+            additionalParams.data = field:uid
+            additionalParams.wrap = &tx_example_pi1[item]=|&tx_example_pi1[controller]=Item&tx_example_pi1[action]=show
+            useCacheHash = 1
+        }
+    }
+
+.. index:: Backend, Frontend, PHP-API, TSConfig, TypoScript
diff --git a/typo3/sysext/core/Tests/Unit/LinkHandling/RecordLinkHandlerTest.php b/typo3/sysext/core/Tests/Unit/LinkHandling/RecordLinkHandlerTest.php
new file mode 100644 (file)
index 0000000..8fc660e
--- /dev/null
@@ -0,0 +1,86 @@
+<?php
+
+namespace TYPO3\CMS\Core\Tests\Unit\LinkHandling;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\LinkHandling\RecordLinkHandler;
+use TYPO3\Components\TestingFramework\Core\Unit\UnitTestCase;
+
+class RecordLinkHandlerTest extends UnitTestCase
+{
+    /**
+     * @test
+     */
+    public function asStringReturnsUrl()
+    {
+        $subject = new RecordLinkHandler();
+        $parameters = [
+            'identifier' => 'tx_identifier',
+            'uid' => 123
+        ];
+        $url = sprintf('t3://record?identifier=%s&uid=%s',
+            $parameters['identifier'],
+            $parameters['uid']);
+
+        $this->assertEquals($url, $subject->asString($parameters));
+    }
+
+    /**
+     * @return array
+     */
+    public function missingParameterDataProvider(): array
+    {
+        return [
+            'identifier is missing' => [
+                [
+                    'uid' => 123
+                ]
+            ],
+            'uid is missing' => [
+                [
+                    'identifier' => 'identifier',
+                ]
+            ]
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider missingParameterDataProvider
+     * @param array $parameters
+     */
+    public function resolveHandlerDataThrowsExceptionIfParameterIsMissing(array $parameters)
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionCode(1486155151);
+
+        $subject = new RecordLinkHandler();
+        $subject->resolveHandlerData($parameters);
+    }
+
+    /**
+     * @test
+     * @dataProvider missingParameterDataProvider
+     * @param array $parameters
+     */
+    public function asStringThrowsExceptionIfParameterIsMissing(array $parameters)
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionCode(1486155150);
+
+        $subject = new RecordLinkHandler();
+        $subject->asString($parameters);
+    }
+}
index be62268..5052d48 100644 (file)
@@ -4090,8 +4090,8 @@ class ContentObjectRenderer
                         $urlPrefix = $tsfe->absRefPrefix;
                     }
                     $icon = '<img src="' . htmlspecialchars($urlPrefix . $icon) . '"' .
-                            ' width="' . (int)$sizeParts[0] . '" height="' . (int)$sizeParts[1] . '" ' .
-                            $this->getBorderAttr(' border="0"') . '' . $this->getAltParam($conf) . ' />';
+                        ' width="' . (int)$sizeParts[0] . '" height="' . (int)$sizeParts[1] . '" ' .
+                        $this->getBorderAttr(' border="0"') . '' . $this->getAltParam($conf) . ' />';
                 }
             } else {
                 $conf['icon.']['widthAttribute'] = isset($conf['icon.']['widthAttribute.'])
@@ -5577,8 +5577,9 @@ class ContentObjectRenderer
      * @param string $linkText The string (text) to link
      * @param string $mixedLinkParameter destination data like "15,13 _blank myclass &more=1" used to create the link
      * @param array $configuration TypoScript configuration
-     * @return array | string
+     * @return array|string
      * @see typoLink()
+     * @todo the whole thing does not work like this anymore. remove the whole function. forge #79647
      */
     protected function resolveMixedLinkParameter($linkText, $mixedLinkParameter, &$configuration = [])
     {
@@ -6004,6 +6005,39 @@ class ContentObjectRenderer
                     return $linkText;
                 }
             break;
+            case LinkService::TYPE_RECORD:
+                $tsfe = $this->getTypoScriptFrontendController();
+                $configurationKey = $linkDetails['identifier'] . '.';
+                $configuration = $tsfe->tmpl->setup['config.']['recordLinks.'];
+                $linkHandlerConfiguration = $tsfe->pagesTSconfig['TCEMAIN.']['linkHandler.'];
+
+                if (!isset($configuration[$configurationKey]) || !isset($linkHandlerConfiguration[$configurationKey])) {
+                    return $linkText;
+                }
+                $typoScriptConfiguration = $configuration[$configurationKey]['typolink.'];
+                $linkHandlerConfiguration = $linkHandlerConfiguration[$configurationKey]['configuration.'];
+
+                if ($configuration[$configurationKey]['forceLink']) {
+                    $record = $tsfe->sys_page->getRawRecord($linkHandlerConfiguration['table'], $linkDetails['uid']);
+                } else {
+                    $record = $tsfe->sys_page->checkRecord($linkHandlerConfiguration['table'], $linkDetails['uid']);
+                }
+                if ($record === 0) {
+                    return $linkText;
+                }
+
+                // Build the full link to the record
+                $localContentObjectRenderer = GeneralUtility::makeInstance(self::class);
+                $localContentObjectRenderer->start($record, $linkHandlerConfiguration['table']);
+                $localContentObjectRenderer->parameters = $this->parameters;
+                $link = $localContentObjectRenderer->typoLink($linkText, $typoScriptConfiguration);
+
+                $this->lastTypoLinkLD = $localContentObjectRenderer->lastTypoLinkLD;
+                $this->lastTypoLinkUrl = $localContentObjectRenderer->lastTypoLinkUrl;
+                $this->lastTypoLinkTarget = $localContentObjectRenderer->lastTypoLinkTarget;
+
+                return $link;
+                break;
 
             // Legacy files or something else
             case LinkService::TYPE_UNKNOWN:
@@ -6043,7 +6077,7 @@ class ContentObjectRenderer
                     $finalTagParts['targetParams'] = $target ? ' target="' . $target . '"' : '';
                     $finalTagParts['aTagParams'] .= $this->extLinkATagParams($finalTagParts['url'], LinkService::TYPE_URL);
                 }
-            break;
+                break;
         }
 
         $finalTagParts['TYPE'] = $linkDetails['type'];
@@ -7577,10 +7611,10 @@ class ContentObjectRenderer
             $knownAliases[$tableReference] = true;
 
             $fromClauses[$tableReference] = $tableSql . $this->getQueryArrayJoinHelper(
-                $tableReference,
-                $queryBuilder->getQueryPart('join'),
-                $knownAliases
-            );
+                    $tableReference,
+                    $queryBuilder->getQueryPart('join'),
+                    $knownAliases
+                );
         }
 
         $queryParts['SELECT'] = implode(', ', $queryBuilder->getQueryPart('select'));
diff --git a/typo3/sysext/recordlist/Classes/Browser/RecordBrowser.php b/typo3/sysext/recordlist/Classes/Browser/RecordBrowser.php
new file mode 100644 (file)
index 0000000..ab13055
--- /dev/null
@@ -0,0 +1,71 @@
+<?php
+declare(strict_types=1);
+namespace TYPO3\CMS\Recordlist\Browser;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Extends the DatabaseBrowser for the specific needs of the LinkBrowser.
+ *
+ * Mostly this is about being able to set to some parameters that cannot
+ * be set from outside the DatabaseBrowser.
+ */
+class RecordBrowser extends DatabaseBrowser
+{
+    /**
+     * @var array
+     */
+    protected $urlParameters = [];
+
+    /**
+     * Main initialization
+     */
+    protected function initialize()
+    {
+        $this->determineScriptUrl();
+        $this->initVariables();
+    }
+
+    /**
+     * Avoid any initialization
+     */
+    protected function initVariables()
+    {
+    }
+
+    /**
+     * @param int $selectedPage Id of page
+     * @param string $tables Comma separated list of tables
+     * @param array $urlParameters url parameters
+     *
+     * @return string
+     */
+    public function displayRecordsForPage(int $selectedPage, string $tables, array $urlParameters): string
+    {
+        $this->urlParameters = $urlParameters;
+        $this->urlParameters['mode'] = 'db';
+        $this->expandPage = $selectedPage;
+
+        return $this->renderTableRecords($tables);
+    }
+
+    /**
+     * @param array $values Array of values to include into the parameters
+     * @return string[] Array of parameters which have to be added to URLs
+     */
+    public function getUrlParameters(array $values): array
+    {
+        return array_merge($this->urlParameters, $values);
+    }
+}
diff --git a/typo3/sysext/recordlist/Classes/LinkHandler/RecordLinkHandler.php b/typo3/sysext/recordlist/Classes/LinkHandler/RecordLinkHandler.php
new file mode 100644 (file)
index 0000000..bc5a35f
--- /dev/null
@@ -0,0 +1,245 @@
+<?php
+declare(strict_types=1);
+namespace TYPO3\CMS\Recordlist\LinkHandler;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\Http\Message\ServerRequestInterface;
+use TYPO3\CMS\Backend\Utility\BackendUtility;
+use TYPO3\CMS\Core\LinkHandling\LinkService;
+use TYPO3\CMS\Core\Page\PageRenderer;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Fluid\View\StandaloneView;
+use TYPO3\CMS\Recordlist\Browser\RecordBrowser;
+use TYPO3\CMS\Recordlist\Controller\AbstractLinkBrowserController;
+use TYPO3\CMS\Recordlist\Tree\View\LinkParameterProviderInterface;
+use TYPO3\CMS\Recordlist\Tree\View\RecordBrowserPageTreeView;
+
+/**
+ * Link handler for arbitrary database records
+ */
+class RecordLinkHandler extends AbstractLinkHandler implements LinkHandlerInterface, LinkParameterProviderInterface
+{
+    /**
+     * Configuration key in TSconfig TCEMAIN.linkHandler.record
+     *
+     * @var string
+     */
+    protected $identifier;
+
+    /**
+     * Specific TSconfig for the current instance (corresponds to TCEMAIN.linkHandler.record.identifier.configuration)
+     *
+     * @var array
+     */
+    protected $configuration = [];
+
+    /**
+     * Parts of the current link
+     *
+     * @var array
+     */
+    protected $linkParts = [];
+
+    /**
+     * @var int
+     */
+    protected $expandPage = 0;
+
+    /**
+     * Initializes the handler.
+     *
+     * @param AbstractLinkBrowserController $linkBrowser
+     * @param string $identifier
+     * @param array $configuration Page TSconfig
+     *
+     * @return void
+     */
+    public function initialize(AbstractLinkBrowserController $linkBrowser, $identifier, array $configuration)
+    {
+        parent::initialize($linkBrowser, $identifier, $configuration);
+        $this->identifier = $identifier;
+        $this->configuration = $configuration;
+    }
+
+    /**
+     * Checks if this is the right handler for the given link.
+     *
+     * Also stores information locally about currently linked record.
+     *
+     * @param array $linkParts Link parts as returned from TypoLinkCodecService
+     * @return bool
+     */
+    public function canHandleLink(array $linkParts): bool
+    {
+        if (!$linkParts['url'] || !isset($linkParts['url']['identifier'])) {
+            return false;
+        }
+
+        $data = $linkParts['url'];
+
+        // Get the related record
+        $table = $this->configuration['table'];
+        $record = BackendUtility::getRecord($table, $data['uid']);
+        if ($record === null) {
+            $linkParts['title'] = $this->getLanguageService()->getLL('recordNotFound');
+        } else {
+            $linkParts['tableName'] = $this->getLanguageService()->sL($GLOBALS['TCA'][$table]['ctrl']['title']);
+            $linkParts['pid'] = (int)$record['pid'];
+            $linkParts['title'] = $linkParts['title'] ?: BackendUtility::getRecordTitle($table, $record);
+        }
+        $linkParts['url']['type'] = $linkParts['type'];
+        $this->linkParts = $linkParts;
+
+        return true;
+    }
+
+    /**
+     * Formats information for the current record for HTML output.
+     *
+     * @return string
+     */
+    public function formatCurrentUrl(): string
+    {
+        return sprintf(
+            '%s: %s [uid: %d]',
+            $this->linkParts['tableName'],
+            $this->linkParts['title'],
+            $this->linkParts['url']['uid']
+        );
+    }
+
+    /**
+     * Renders the link handler.
+     *
+     * @param ServerRequestInterface $request
+     * @return string
+     */
+    public function render(ServerRequestInterface $request): string
+    {
+        // Declare JS module
+        GeneralUtility::makeInstance(PageRenderer::class)->loadRequireJsModule('TYPO3/CMS/Recordlist/RecordLinkHandler');
+
+        // Define the current page
+        if (isset($request->getQueryParams()['expandPage'])) {
+            $this->expandPage = (int)$request->getQueryParams()['expandPage'];
+        } elseif (isset($this->configuration['storagePid'])) {
+            $this->expandPage = (int)$this->configuration['storagePid'];
+        } elseif (isset($this->linkParts['pid'])) {
+            $this->expandPage = (int)$this->linkParts['pid'];
+        }
+
+        $databaseBrowser = GeneralUtility::makeInstance(RecordBrowser::class);
+
+        $recordList = $databaseBrowser->displayRecordsForPage(
+            $this->expandPage,
+            $this->configuration['table'],
+            $this->getUrlParameters([])
+        );
+
+        $path = GeneralUtility::getFileAbsFileName('EXT:recordlist/Resources/Private/Templates/LinkBrowser/Record.html');
+        $view = GeneralUtility::makeInstance(StandaloneView::class);
+        $view->setTemplatePathAndFilename($path);
+        $view->assignMultiple([
+            'tree' => $this->configuration['hidePageTree'] ? '' : $this->renderPageTree(),
+            'recordList' => $recordList,
+        ]);
+
+        return $view->render();
+    }
+
+    /**
+     * Renders the page tree.
+     *
+     * @return string
+     */
+    protected function renderPageTree(): string
+    {
+        $backendUser = $this->getBackendUser();
+
+        /** @var RecordBrowserPageTreeView $pageTree */
+        $pageTree = GeneralUtility::makeInstance(RecordBrowserPageTreeView::class);
+        $pageTree->setLinkParameterProvider($this);
+        $pageTree->ext_showPageId = (bool)$backendUser->getTSConfigVal('options.pageTree.showPageIdWithTitle');
+        $pageTree->ext_showNavTitle = (bool)$backendUser->getTSConfigVal('options.pageTree.showNavTitle');
+        $pageTree->ext_showPathAboveMounts = (bool)$backendUser->getTSConfigVal('options.pageTree.showPathAboveMounts');
+        $pageTree->addField('nav_title');
+
+        // Load the mount points, if any
+        // NOTE: mount points actually override the page tree
+        if (!empty($this->configuration['pageTreeMountPoints'])) {
+            $pageTree->MOUNTS = GeneralUtility::intExplode(',', $this->configuration['pageTreeMountPoints'], true);
+        }
+
+        return $pageTree->getBrowsableTree();
+    }
+
+    /**
+     * Returns attributes for the body tag.
+     *
+     * @return string[] Array of body-tag attributes
+     */
+    public function getBodyTagAttributes(): array
+    {
+        $attributes = [
+            'data-identifier' => 't3://record?identifier=' . $this->identifier . '&uid=',
+        ];
+        if (!empty($this->linkParts)) {
+            $attributes['data-current-link'] = GeneralUtility::makeInstance(LinkService::class)->asString($this->linkParts['url']);
+        }
+
+        return $attributes;
+    }
+
+    /**
+     * Returns all parameters needed to build a URL with all the necessary information.
+     *
+     * @param array $values Array of values to include into the parameters or which might influence the parameters
+     * @return string[] Array of parameters which have to be added to URLs
+     */
+    public function getUrlParameters(array $values): array
+    {
+        $pid = isset($values['pid']) ? (int)$values['pid'] : $this->expandPage;
+        $parameters = [
+            'expandPage' => $pid,
+        ];
+
+        return array_merge(
+            $this->linkBrowser->getUrlParameters($values),
+            ['P' => $this->linkBrowser->getParameters()],
+            $parameters
+        );
+    }
+
+    /**
+     * Checks if the submitted page matches the current page.
+     *
+     * @param array $values Values to be checked
+     * @return bool Returns TRUE if the given values match the currently selected item
+     */
+    public function isCurrentlySelectedItem(array $values): bool
+    {
+        return !empty($this->linkParts) && (int)$this->linkParts['pid'] === (int)$values['pid'];
+    }
+
+    /**
+     * Returns the URL of the current script
+     *
+     * @return string
+     */
+    public function getScriptUrl(): string
+    {
+        return $this->linkBrowser->getScriptUrl();
+    }
+}
diff --git a/typo3/sysext/recordlist/Classes/Tree/View/RecordBrowserPageTreeView.php b/typo3/sysext/recordlist/Classes/Tree/View/RecordBrowserPageTreeView.php
new file mode 100644 (file)
index 0000000..73445f5
--- /dev/null
@@ -0,0 +1,122 @@
+<?php
+declare(strict_types=1);
+namespace TYPO3\CMS\Recordlist\Tree\View;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Backend\Tree\View\ElementBrowserPageTreeView;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Specific page tree for the record link handler.
+ */
+class RecordBrowserPageTreeView extends ElementBrowserPageTreeView
+{
+    /**
+     * Creates the page navigation tree in HTML.
+     *
+     * This variant does not show the right-pointing caret next to pages and puts the "action" link
+     * directly on each page title.
+     *
+     * @param array|string $treeArr Tree array
+     * @return string HTML output.
+     */
+    public function printTree($treeArr = '')
+    {
+        $titleLen = (int)$GLOBALS['BE_USER']->uc['titleLen'];
+        if (!is_array($treeArr)) {
+            $treeArr = $this->tree;
+        }
+        $out = '';
+        // We need to count the opened <ul>'s every time we dig into another level,
+        // so we know how many we have to close when all children are done rendering
+        $closeDepth = [];
+        foreach ($treeArr as $treeItem) {
+            $classAttr = $treeItem['row']['_CSSCLASS'];
+            if ($treeItem['isFirst']) {
+                $out .= '<ul class="list-tree">';
+            }
+
+            // Add CSS classes to the list item
+            if ($treeItem['hasSub']) {
+                $classAttr .= ' list-tree-control-open';
+            }
+
+            $selected = '';
+            if ($this->linkParameterProvider->isCurrentlySelectedItem(['pid' => (int)$treeItem['row']['uid']])) {
+                $selected = ' bg-success';
+                $classAttr .= ' active';
+            }
+            $out .= '
+                               <li' . ($classAttr ? ' class="' . trim($classAttr) . '"' : '') . '>
+                                       <span class="list-tree-group' . $selected . '">
+                                               ' . $treeItem['HTML']
+                    . $this->wrapTitle($this->getTitleStr($treeItem['row'], $titleLen), $treeItem['row'], $this->ext_pArrPages)
+                    . '</span>
+                               ';
+            if (!$treeItem['hasSub']) {
+                $out .= '</li>';
+            }
+
+            // We have to remember if this is the last one
+            // on level X so the last child on level X+1 closes the <ul>-tag
+            if ($treeItem['isLast']) {
+                $closeDepth[$treeItem['invertedDepth']] = 1;
+            }
+            // If this is the last one and does not have subitems, we need to close
+            // the tree as long as the upper levels have last items too
+            if ($treeItem['isLast'] && !$treeItem['hasSub']) {
+                for ($i = $treeItem['invertedDepth']; $closeDepth[$i] == 1; $i++) {
+                    $closeDepth[$i] = 0;
+                    $out .= '</ul></li>';
+                }
+            }
+        }
+        $out = '<ul class="list-tree list-tree-root">' . $out . '</ul>';
+
+        return $out;
+    }
+
+    /**
+     * Wrapping the title in a link, if applicable.
+     *
+     * @param string $title Title, (must be ready for output, that means it must be htmlspecialchars()'ed).
+     * @param array $record The record
+     * @param bool $ext_pArrPages (ignored)
+     * @return string Wrapping title string.
+     */
+    public function wrapTitle($title, $record, $ext_pArrPages = false)
+    {
+        $urlParameters = $this->linkParameterProvider->getUrlParameters(['pid' => (int)$record['uid']]);
+        $url = $this->getThisScript() . ltrim(GeneralUtility::implodeArrayForUrl('', $urlParameters), '&');
+        $aOnClick = 'return jumpToUrl(' . GeneralUtility::quoteJSvalue($url) . ');';
+
+        return '<span class="list-tree-title"><a href="#" onclick="' . htmlspecialchars($aOnClick) . '">'
+            . $title . '</a></span>';
+    }
+
+    /**
+     * Returns TRUE if a doktype can be linked.
+     *
+     * In this case, all pages can be linked.
+     *
+     * @param int $doktype Doktype value to test
+     * @param int $uid uid to test.
+     * @return bool
+     */
+    public function ext_isLinkable($doktype, $uid)
+    {
+        return true;
+    }
+}
diff --git a/typo3/sysext/recordlist/Resources/Private/Templates/LinkBrowser/Record.html b/typo3/sysext/recordlist/Resources/Private/Templates/LinkBrowser/Record.html
new file mode 100644 (file)
index 0000000..f9c118c
--- /dev/null
@@ -0,0 +1,16 @@
+<div class="link-browser-section link-browser-pagetree">
+    <f:if condition="{tree}">
+        <f:then>
+            <div class="col-xs-4">
+                <h3>{f:translate(key:'LLL:EXT:lang/Resources/Private/Language/locallang_browse_links.xlf:pageTree')}</h3>
+                {tree -> f:format.raw()}
+            </div>
+            <div class="col-xs-8">
+                {recordList -> f:format.raw()}
+            </div>
+        </f:then>
+        <f:else>
+            {recordList -> f:format.raw()}
+        </f:else>
+    </f:if>
+</div>
\ No newline at end of file
diff --git a/typo3/sysext/recordlist/Resources/Public/JavaScript/RecordLinkHandler.js b/typo3/sysext/recordlist/Resources/Public/JavaScript/RecordLinkHandler.js
new file mode 100644 (file)
index 0000000..16a2fa6
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Module: TYPO3/CMS/Recordlist/RecordLinkHandler
+ * record link interaction
+ */
+define(['jquery', 'TYPO3/CMS/Recordlist/LinkBrowser'], function ($, LinkBrowser) {
+       'use strict';
+
+       /**
+        * @type {{currentLink: string, identifier: string, linkRecord: function, linkCurrent: function}}
+        */
+       var RecordLinkHandler = {
+               currentLink: '',
+               identifier: '',
+
+               /**
+                * @param {Event} event
+                */
+               linkRecord: function (event) {
+                       event.preventDefault();
+
+                       var data = $(this).parents('span').data();
+                       LinkBrowser.finalizeFunction(RecordLinkHandler.identifier + data.uid);
+               },
+
+               /**
+                * @param {Event} event
+                */
+               linkCurrent: function (event) {
+                       event.preventDefault();
+
+                       LinkBrowser.finalizeFunction(RecordLinkHandler.currentLink);
+               }
+       };
+
+       $(function () {
+               var body = $('body');
+               RecordLinkHandler.currentLink = body.data('currentLink');
+               RecordLinkHandler.identifier = body.data('identifier');
+
+               // adjust searchbox layout
+               var searchbox = document.getElementById('db_list-searchbox-toolbar');
+               searchbox.style.display = 'block';
+               searchbox.style.position = 'relative';
+
+               $('[data-close]').on('click', RecordLinkHandler.linkRecord);
+               $('input.t3js-linkCurrent').on('click', RecordLinkHandler.linkCurrent);
+       });
+
+       return RecordLinkHandler;
+});