[!!!][TASK] Replace jumpurl functionality with hooks 11/36211/20
authorAlexander Stehlik <alexander.stehlik@gmail.com>
Tue, 15 Sep 2015 09:22:13 +0000 (11:22 +0200)
committerChristian Kuhn <lolli@schwarzbu.ch>
Tue, 15 Sep 2015 12:33:21 +0000 (14:33 +0200)
The jumpurl feature is moved from the core to a new extension "jumpurl".

This patch removes all jumpurl functionality and replaces it with
hooks that can be used to modify the link generation and to handle
any submitted jumpurl.

Resolves: #52156
Releases: master
Change-Id: I7ec2b8d2da0ce42b227a5150d9c84bce340b16a7
Reviewed-on: http://review.typo3.org/36211
Reviewed-by: Benni Mack <benni@typo3.org>
Tested-by: Benni Mack <benni@typo3.org>
Reviewed-by: Mathias Schreiber <mathias.schreiber@wmdb.de>
Tested-by: Mathias Schreiber <mathias.schreiber@wmdb.de>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
29 files changed:
composer.json
typo3/sysext/core/Classes/Resource/ResourceStorage.php
typo3/sysext/core/Documentation/Changelog/master/Breaking-52156-ReplaceJumpUrlWithHooks.rst [new file with mode: 0644]
typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php
typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
typo3/sysext/frontend/Classes/Http/RequestHandler.php
typo3/sysext/frontend/Classes/Http/UrlHandlerInterface.php [new file with mode: 0644]
typo3/sysext/frontend/Classes/Http/UrlProcessorInterface.php [new file with mode: 0644]
typo3/sysext/frontend/Classes/Page/ExternalPageUrlHandler.php [new file with mode: 0644]
typo3/sysext/frontend/Tests/Unit/ContentObject/ContentObjectRendererTest.php
typo3/sysext/frontend/ext_localconf.php
typo3/sysext/jumpurl/Classes/JumpUrlHandler.php [new file with mode: 0644]
typo3/sysext/jumpurl/Classes/JumpUrlProcessor.php [new file with mode: 0644]
typo3/sysext/jumpurl/Classes/JumpUrlUtility.php [new file with mode: 0644]
typo3/sysext/jumpurl/Documentation/.gitignore [new file with mode: 0644]
typo3/sysext/jumpurl/Documentation/Configuration/Index.rst [new file with mode: 0644]
typo3/sysext/jumpurl/Documentation/Includes.txt [new file with mode: 0644]
typo3/sysext/jumpurl/Documentation/Index.rst [new file with mode: 0644]
typo3/sysext/jumpurl/Documentation/Introduction/Index.rst [new file with mode: 0644]
typo3/sysext/jumpurl/Documentation/Settings.yml [new file with mode: 0644]
typo3/sysext/jumpurl/Documentation/Targets.rst [new file with mode: 0644]
typo3/sysext/jumpurl/Tests/Unit/ContentObjectRendererTest.php [new file with mode: 0644]
typo3/sysext/jumpurl/Tests/Unit/JumpUrlHandlerTest.php [new file with mode: 0644]
typo3/sysext/jumpurl/Tests/Unit/JumpUrlProcessorMock.php [new file with mode: 0644]
typo3/sysext/jumpurl/Tests/Unit/JumpUrlProcessorTest.php [new file with mode: 0644]
typo3/sysext/jumpurl/composer.json [new file with mode: 0644]
typo3/sysext/jumpurl/ext_emconf.php [new file with mode: 0644]
typo3/sysext/jumpurl/ext_icon.png [new file with mode: 0644]
typo3/sysext/jumpurl/ext_localconf.php [new file with mode: 0644]

index 85b81db..cbd1c79 100644 (file)
@@ -92,6 +92,7 @@
                "typo3/cms-info": "self.version",
                "typo3/cms-info-pagetsconfig": "self.version",
                "typo3/cms-install": "self.version",
+               "typo3/cms-jumpurl": "self.version",
                "typo3/cms-lang": "self.version",
                "typo3/cms-linkvalidator": "self.version",
                "typo3/cms-lowlevel": "self.version",
                        "TYPO3\\CMS\\Info\\": "typo3/sysext/info/Classes/",
                        "TYPO3\\CMS\\InfoPagetsconfig\\": "typo3/sysext/info_pagetsconfig/Classes/",
                        "TYPO3\\CMS\\Install\\": "typo3/sysext/install/Classes/",
+                       "TYPO3\\CMS\\Jumpurl\\": "typo3/sysext/jumpurl/Classes/",
                        "TYPO3\\CMS\\Lang\\": "typo3/sysext/lang/Classes/",
                        "TYPO3\\CMS\\Linkvalidator\\": "typo3/sysext/linkvalidator/Classes/",
                        "TYPO3\\CMS\\Lowlevel\\": "typo3/sysext/lowlevel/Classes/",
                        "TYPO3\\CMS\\Impexp\\Tests\\": "typo3/sysext/impexp/Tests/",
                        "TYPO3\\CMS\\IndexedSearch\\Tests\\": "typo3/sysext/indexed_search/Tests/",
                        "TYPO3\\CMS\\Install\\Tests\\": "typo3/sysext/install/Tests/",
+                       "TYPO3\\CMS\\Jumpurl\\Tests\\": "typo3/sysext/jumpurl/Tests/",
                        "TYPO3\\CMS\\Lang\\Tests\\": "typo3/sysext/lang/Tests/",
                        "TYPO3\\CMS\\Lowlevel\\Tests\\": "typo3/sysext/lowlevel/Tests/",
                        "TYPO3\\CMS\\Mediace\\": "typo3/sysext/mediace/Classes/",
index 1de3240..c9db178 100644 (file)
@@ -1551,13 +1551,14 @@ class ResourceStorage implements ResourceStorageInterface {
         * @param FileInterface $file
         * @param bool $asDownload If set Content-Disposition attachment is sent, inline otherwise
         * @param string $alternativeFilename the filename for the download (if $asDownload is set)
+        * @param string $overrideMimeType If set this will be used as Content-Type header instead of the automatically detected mime type.
         * @return void
         */
-       public function dumpFileContents(FileInterface $file, $asDownload = FALSE, $alternativeFilename = NULL) {
+       public function dumpFileContents(FileInterface $file, $asDownload = FALSE, $alternativeFilename = NULL, $overrideMimeType = NULL) {
                $downloadName = $alternativeFilename ?: $file->getName();
                $contentDisposition = $asDownload ? 'attachment' : 'inline';
                header('Content-Disposition: ' . $contentDisposition . '; filename="' . $downloadName . '"');
-               header('Content-Type: ' . $file->getMimeType());
+               header('Content-Type: ' . $overrideMimeType ?: $file->getMimeType());
                header('Content-Length: ' . $file->getSize());
 
                // Cache-Control header is needed here to solve an issue with browser IE8 and lower
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Breaking-52156-ReplaceJumpUrlWithHooks.rst b/typo3/sysext/core/Documentation/Changelog/master/Breaking-52156-ReplaceJumpUrlWithHooks.rst
new file mode 100644 (file)
index 0000000..31db7d1
--- /dev/null
@@ -0,0 +1,109 @@
+=======================================================
+Breaking: #52156 - Replaced JumpURL features with hooks
+=======================================================
+
+Description
+===========
+
+JumpURL handling
+^^^^^^^^^^^^^^^^
+
+The generation and handling of JumpURLs has been removed from the frontend extension and
+is moved to a new core extension called "jumpurl".
+
+URL handler hooks
+^^^^^^^^^^^^^^^^^
+
+New hooks were introduced in :code:`TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer`
+and :code:`\TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController` that allow
+custom URL generation and handling.
+
+This is how you can register a hook for manipulating URLs during link generation:
+
+.. code-block:: php
+
+       // Place this in your ext_localconf.php file
+       $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['urlProcessing']['urlHandlers']['myext_myidentifier']['handler'] =
+               \Company\MyExt\MyUrlHandler::class;
+
+       // The class needs to implement the UrlProcessorInterface:
+       class MyUrlHandler implements \TYPO3\CMS\Frontend\Http\UrlProcessorInterface {}
+
+This is how you can handle URLs in a custom way:
+
+.. code-block:: php
+
+       // Place this in your ext_localconf.php file
+       $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['urlProcessing']['urlProcessors']['myext_myidentifier']['processor']
+               = \Company\MyExt\MyUrlProcessor::class;
+
+       // The class needs to implement the UrlHandlerInterface:
+       class MyUrlProcessor implements \TYPO3\CMS\Frontend\Http\UrlHandlerInterface {}
+
+
+External URL page handling
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The core functionality for redirecting the user to an external URL when he hits a page with doktype "external"
+is moved from the :code:`\TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController` to the
+:code:`\TYPO3\CMS\Frontend\Page\ExternalPageUrlHandler` class.
+
+
+ResourceStorage adjustment
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The method :code:`\TYPO3\CMS\Core\Resource\ResourceStorage::dumpFileContents()` accepts an additional
+parameter for overriding the mime type that is sent in the ``Content-Type`` header when delivering a file.
+
+Impact
+======
+
+Unless the jumpurl extension is installed, no JumpURL related feature will work anymore.
+
+If an extension tightly integrates into the JumpURL process it might break, because some of the related
+methods were removed, disabled or changed.
+
+These methods are removed and their functionality is moved to the new jumpurl extension:
+
+:code:`\TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::locDataJU()`
+
+:code:`\TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::locDataCheck()`
+
+The :code:`$initP` parameter of the method  :code:`\TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::getMailTo()` is removed.
+
+The method :code:`\TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::setExternalJumpUrl()` is deprecated
+and is an alias for the new :code:`initializeRedirectUrlHandlers()` method that does no jumpurl handling any more. The
+new method only checks if the current page is a link to an external URL and sets the :code:`redirectUrl` property.
+
+The method :code:`\TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::jumpUrl()` is also deprecated
+and is an alias for the new :code:`redirectToExternalUrl()` method. The jumpurl handling was removed from
+this method. It loops over all registered URL handlers and handles the redirection to the :code:`redirectUrl`.
+
+
+Affected installations
+======================
+
+All CMS 7.4 installations that use the JumpURL features or that use Extensions that rely on these features
+or one of the removed methods.
+
+
+Migration
+=========
+
+If you  want to use the JumpURL features you need to install the jumpurl extension. Your configuration should
+then work as before.
+
+Please note that the configuration of the :ref:`filelink <t3tsref:filelink>` TypoScript function has changed.
+Passing the :code:`jumpurl` parameter in the configuration is deprecated and will be removed in future versions.
+
+You can now pass arbitrary configuration options for the typolink call that is used to generate
+the file link in the :code:`typolinkConfiguration` parameter:
+
+.. code-block:: typoscript
+
+       lib.myfilelink = TEXT
+       lib.myfilelink.value = fileadmin/myfile.txt
+       lib.myfilelink.filelink {
+               typolinkConfiguration.jumpurl = 1
+               typolinkConfiguration.jumpurl.secure = 1
+       }
index bb9f787..391511b 100755 (executable)
@@ -29,6 +29,7 @@ use TYPO3\CMS\Core\Resource\FileReference;
 use TYPO3\CMS\Core\Resource\Folder;
 use TYPO3\CMS\Core\Resource\ProcessedFile;
 use TYPO3\CMS\Core\Resource\ResourceFactory;
+use TYPO3\CMS\Core\Service\DependencyOrderingService;
 use TYPO3\CMS\Core\Service\MarkerBasedTemplateService;
 use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
 use TYPO3\CMS\Core\TypoScript\TemplateService;
@@ -43,6 +44,7 @@ use TYPO3\CMS\Core\Versioning\VersionState;
 use TYPO3\CMS\Frontend\ContentObject\Exception\ContentRenderingException;
 use TYPO3\CMS\Frontend\ContentObject\Exception\ProductionExceptionHandler;
 use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
+use TYPO3\CMS\Frontend\Http\UrlProcessorInterface;
 use TYPO3\CMS\Frontend\Imaging\GifBuilder;
 use TYPO3\CMS\Frontend\ContentObject\Exception\ExceptionHandlerInterface;
 use TYPO3\CMS\Frontend\Page\CacheHashCalculator;
@@ -4494,36 +4496,33 @@ class ContentObjectRenderer {
                if (isset($conf['target.'])) {
                        $target = $this->stdWrap($target, $conf['target.']);
                }
-               // The jumpURL feature will be taken care of by typoLink, only "jumpurl.secure = 1" is applyable needed for special link creation
                $tsfe = $this->getTypoScriptFrontendController();
-               if ($conf['jumpurl.']['secure']) {
-                       $alternativeJumpUrlParameter = isset($conf['jumpurl.']['parameter.']) ? $this->stdWrap($conf['jumpurl.']['parameter'], $conf['jumpurl.']['parameter.']) : $conf['jumpurl.']['parameter'];
-                       $typoLinkConf = array(
-                               'parameter' => $alternativeJumpUrlParameter ? $alternativeJumpUrlParameter : $tsfe->id . ',' . $tsfe->type,
-                               'fileTarget' => $target,
-                               'title' => $title,
-                               'ATagParams' => $this->getATagParams($conf),
-                               'additionalParams' => '&jumpurl=' . rawurlencode($theFileEnc) . $this->locDataJU($theFileEnc, $conf['jumpurl.']['secure.']) . $tsfe->getMethodUrlIdToken
-                       );
-               } else {
-                       $typoLinkConf = array(
-                               'parameter' => $theFileEnc,
-                               'fileTarget' => $target,
-                               'title' => $title,
-                               'ATagParams' => $this->getATagParams($conf)
-                       );
+
+               $typoLinkConf = array(
+                       'parameter' => $theFileEnc,
+                       'fileTarget' => $target,
+                       'title' => $title,
+                       'ATagParams' => $this->getATagParams($conf)
+               );
+
+               if (isset($conf['typolinkConfiguration.'])) {
+                       $additionalTypoLinkConfiguration = $conf['typolinkConfiguration.'];
+                       // We only allow additional configuration. This is why the generated conf overwrites the additional conf.
+                       ArrayUtility::mergeRecursiveWithOverrule($additionalTypoLinkConfiguration, $typoLinkConf);
+                       $typoLinkConf = $additionalTypoLinkConfiguration;
                }
-               // If the global jumpURL feature is activated, but is disabled for this
-               // filelink, the global parameter needs to be disabled as well for this link creation
-               $globalJumpUrlEnabled = $tsfe->config['config']['jumpurl_enable'];
-               if ($globalJumpUrlEnabled && isset($conf['jumpurl']) && $conf['jumpurl'] == 0) {
-                       $tsfe->config['config']['jumpurl_enable'] = 0;
-               } elseif (!$globalJumpUrlEnabled && $conf['jumpurl']) {
-                       $tsfe->config['config']['jumpurl_enable'] = 1;
+
+               if (isset($conf['jumpurl']) || isset($conf['jumpurl.'])) {
+                       GeneralUtility::deprecationLog('The TypoScript jumpurl configuration is deprecated for file links since TYPO3 CMS 7 and will be removed in TYPO3 CMS 8. Pass this configuration in the typolinkConfiguration property instead.');
+                       if (isset($conf['jumpurl'])) {
+                               $typoLinkConf['jumpurl'] = $conf['jumpurl'];
+                       }
+                       if (isset($conf['jumpurl.'])) {
+                               $typoLinkConf['jumpurl.'] = $conf['jumpurl.'];
+                       }
                }
+
                $theLinkWrap = $this->typoLink('|', $typoLinkConf);
-               // Now the original value is set again
-               $tsfe->config['config']['jumpurl_enable'] = $globalJumpUrlEnabled;
                $theSize = filesize($theFile);
                $fI = GeneralUtility::split_fileref($theFile);
                $icon = '';
@@ -4624,42 +4623,6 @@ class ContentObjectRenderer {
        }
 
        /**
-        * Returns a URL parameter string setting parameters for secure downloads by "jumpurl".
-        * Helper function for filelink()
-        *
-        * @param string $jumpUrl The URL to jump to, basically the filepath
-        * @param array $conf TypoScript properties for the "jumpurl.secure" property of "filelink
-        * @return string URL parameters like "&juSecure=1.....
-        * @access private
-        * @see filelink()
-        */
-       public function locDataJU($jumpUrl, $conf) {
-               $fI = pathinfo($jumpUrl);
-               $mimetype = '';
-               $mimetypeValue = '';
-               if ($fI['extension']) {
-                       $mimeTypes = GeneralUtility::trimExplode(',', $conf['mimeTypes'], TRUE);
-                       foreach ($mimeTypes as $v) {
-                               $parts = explode('=', $v, 2);
-                               if (strtolower($fI['extension']) == strtolower(trim($parts[0]))) {
-                                       $mimetypeValue = trim($parts[1]);
-                                       $mimetype = '&mimeType=' . rawurlencode($mimetypeValue);
-                                       break;
-                               }
-                       }
-               }
-               $locationData = $this->getTypoScriptFrontendController()->id . ':' . $this->currentRecord;
-               $rec = '&locationData=' . rawurlencode($locationData);
-               $hArr = array(
-                       $jumpUrl,
-                       $locationData,
-                       $mimetypeValue
-               );
-               $juHash = '&juHash=' . GeneralUtility::hmac(serialize($hArr));
-               return '&juSecure=1' . $mimetype . $rec . $juHash;
-       }
-
-       /**
         * Performs basic mathematical evaluation of the input string. Does NOT take parathesis and operator precedence into account! (for that, see \TYPO3\CMS\Core\Utility\MathUtility::calculateWithPriorityToAdditionAndSubtraction())
         *
         * @param string $val The string to evaluate. Example: "3+4*10/5" will generate "35". Only integer numbers can be used.
@@ -5369,7 +5332,6 @@ class ContentObjectRenderer {
                        $textpieces = explode($scheme, $data);
                        $pieces = count($textpieces);
                        $textstr = $textpieces[0];
-                       $initP = '?id=' . $this->getTypoScriptFrontendController()->id . '&type=' . $this->getTypoScriptFrontendController()->type;
                        for ($i = 1; $i < $pieces; $i++) {
                                $len = strcspn($textpieces[$i], chr(32) . TAB . CRLF);
                                if (trim(substr($textstr, -1)) == '' && $len) {
@@ -5405,13 +5367,12 @@ class ContentObjectRenderer {
                                        } else {
                                                $target = $this->getTypoScriptFrontendController()->extTarget;
                                        }
-                                       if ($this->getTypoScriptFrontendController()->config['config']['jumpurl_enable']) {
-                                               $jumpurl = 'http://' . $parts[0];
-                                               $juHash = GeneralUtility::hmac($jumpurl, 'jumpurl');
-                                               $res = '<a' . ' href="' . htmlspecialchars(($this->getTypoScriptFrontendController()->absRefPrefix . $this->getTypoScriptFrontendController()->config['mainScript'] . $initP . '&jumpurl=' . rawurlencode($jumpurl))) . '&juHash=' . $juHash . $this->getTypoScriptFrontendController()->getMethodUrlIdToken . '"' . ($target ? ' target="' . $target . '"' : '') . $aTagParams . $this->extLinkATagParams(('http://' . $parts[0]), 'url') . '>';
-                                       } else {
-                                               $res = '<a' . ' href="' . $scheme . htmlspecialchars($parts[0]) . '"' . ($target ? ' target="' . $target . '"' : '') . $aTagParams . $this->extLinkATagParams(('http://' . $parts[0]), 'url') . '>';
-                                       }
+
+                                       // check for jump URLs or similar
+                                       $linkUrl = $this->processUrl(UrlProcessorInterface::CONTEXT_COMMON, $scheme . $parts[0], $conf);
+
+                                       $res = '<a href="' . htmlspecialchars($linkUrl) . '"' . ($target ? ' target="' . $target . '"' : '') . $aTagParams . $this->extLinkATagParams(('http://' . $parts[0]), 'url') . '>';
+
                                        $wrap = isset($conf['wrap.']) ? $this->stdWrap($conf['wrap'], $conf['wrap.']) : $conf['wrap'];
                                        if ($conf['ATagBeforeWrap']) {
                                                $res = $res . $this->wrap($linktxt, $wrap) . '</a>';
@@ -5444,7 +5405,6 @@ class ContentObjectRenderer {
                $pieces = count($textpieces);
                $textstr = $textpieces[0];
                $tsfe = $this->getTypoScriptFrontendController();
-               $initP = '?id=' . $tsfe->id . '&type=' . $tsfe->type;
                for ($i = 1; $i < $pieces; $i++) {
                        $len = strcspn($textpieces[$i], chr(32) . TAB . CRLF);
                        if (trim(substr($textstr, -1)) == '' && $len) {
@@ -5455,7 +5415,7 @@ class ContentObjectRenderer {
                                $parts[0] = substr($textpieces[$i], 0, $len);
                                $parts[1] = substr($textpieces[$i], $len);
                                $linktxt = preg_replace('/\\?.*/', '', $parts[0]);
-                               list($mailToUrl, $linktxt) = $this->getMailTo($parts[0], $linktxt, $initP);
+                               list($mailToUrl, $linktxt) = $this->getMailTo($parts[0], $linktxt);
                                $mailToUrl = $tsfe->spamProtectEmailAddresses === 'ascii' ? $mailToUrl : htmlspecialchars($mailToUrl);
                                $res = '<a href="' . $mailToUrl . '"' . $aTagParams . '>';
                                $wrap = isset($conf['wrap.']) ? $this->stdWrap($conf['wrap'], $conf['wrap.']) : $conf['wrap'];
@@ -6171,7 +6131,6 @@ class ContentObjectRenderer {
                $finalTagParts = array();
                $finalTagParts['aTagParams'] = $this->getATagParams($conf);
                $linkParameter = isset($conf['parameter.']) ? trim($this->stdWrap($conf['parameter'], $conf['parameter.'])) : trim($conf['parameter']);
-               $initP = '?id=' . $tsfe->id . '&type=' . $tsfe->type;
                $this->lastTypoLinkUrl = '';
                $this->lastTypoLinkTarget = '';
 
@@ -6233,7 +6192,7 @@ class ContentObjectRenderer {
                        // If it's a mail address
                        case 'mailto':
                                $linkParameter = preg_replace('/^mailto:/i', '', $linkParameter);
-                               list($this->lastTypoLinkUrl, $linktxt) = $this->getMailTo($linkParameter, $linktxt, $initP);
+                               list($this->lastTypoLinkUrl, $linktxt) = $this->getMailTo($linkParameter, $linktxt);
                                $finalTagParts['url'] = $this->lastTypoLinkUrl;
                        break;
 
@@ -6261,14 +6220,8 @@ class ContentObjectRenderer {
                                        $scheme = '';
                                }
 
-                               if ($tsfe->config['config']['jumpurl_enable']) {
-                                       $url = $tsfe->absRefPrefix . $tsfe->config['mainScript'] . $initP;
-                                       $jumpurl = $scheme . $linkParameter;
-                                       $juHash = GeneralUtility::hmac($jumpurl, 'jumpurl');
-                                       $this->lastTypoLinkUrl = $url . '&jumpurl=' . rawurlencode($jumpurl) . '&juHash=' . $juHash . $tsfe->getMethodUrlIdToken;
-                               } else {
-                                       $this->lastTypoLinkUrl = $scheme . $linkParameter;
-                               }
+                               $this->lastTypoLinkUrl = $this->processUrl(UrlProcessorInterface::CONTEXT_EXTERNAL, $scheme . $linkParameter, $conf);
+
                                $this->lastTypoLinkTarget = $target;
                                $finalTagParts['url'] = $this->lastTypoLinkUrl;
                                $finalTagParts['targetParams'] = $target ? ' target="' . $target . '"' : '';
@@ -6285,18 +6238,7 @@ class ContentObjectRenderer {
                                        if ($linktxt == '') {
                                                $linktxt = $this->parseFunc(rawurldecode($linkParameter), array('makelinks' => 0), '< lib.parseFunc');
                                        }
-                                       if ($tsfe->config['config']['jumpurl_enable'] || $conf['jumpurl']) {
-                                               $theFileEnc = str_replace('%2F', '/', rawurlencode(rawurldecode($linkParameter)));
-                                               $url = $tsfe->absRefPrefix . $tsfe->config['mainScript'] . $initP . '&jumpurl=' . rawurlencode($linkParameter);
-                                               if ($conf['jumpurl.']['secure']) {
-                                                       $url .= $this->locDataJU($theFileEnc, $conf['jumpurl.']['secure.']);
-                                               } else {
-                                                       $url .= '&juHash=' . GeneralUtility::hmac($linkParameter, 'jumpurl');
-                                               }
-                                               $this->lastTypoLinkUrl =  $url . $tsfe->getMethodUrlIdToken;
-                                       } else {
-                                               $this->lastTypoLinkUrl = $tsfe->absRefPrefix . $linkParameter;
-                                       }
+                                       $this->lastTypoLinkUrl = $this->processUrl(UrlProcessorInterface::CONTEXT_FILE, $GLOBALS['TSFE']->absRefPrefix . $linkParameter, $conf);
                                        $this->lastTypoLinkUrl = $this->forceAbsoluteUrl($this->lastTypoLinkUrl, $conf);
                                        $target = isset($conf['fileTarget']) ? $conf['fileTarget'] : $tsfe->fileTarget;
                                        if ($conf['fileTarget.']) {
@@ -6753,6 +6695,48 @@ class ContentObjectRenderer {
        }
 
        /**
+        * Loops over all configured URL modifier hooks (if available) and returns the generated URL or NULL if no URL was generated.
+        *
+        * @param string $context The context in which the method is called (e.g. typoLink).
+        * @param string $url The URL that should be processed.
+        * @param array $typolinkConfiguration The current link configuration array.
+        * @return string|NULL Returns NULL if URL was not processed or the processed URL as a string.
+        * @throws \RuntimeException if a hook was registered but did not fulfill the correct parameters.
+        */
+       protected function processUrl($context, $url, $typolinkConfiguration = array()) {
+               if (
+                       empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['urlProcessing']['urlProcessors'])
+                       || !is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['urlProcessing']['urlProcessors'])
+               ) {
+                       return $url;
+               }
+
+               $urlProcessors = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['urlProcessing']['urlProcessors'];
+               foreach ($urlProcessors as $identifier => $configuration) {
+                       if (empty($configuration) || !is_array($configuration)) {
+                               throw new \RuntimeException('Missing configuration for URI processor "' . $identifier . '".', 1442050529);
+                       }
+                       if (!is_string($configuration['processor']) || empty($configuration['processor']) || !class_exists($configuration['processor']) || !is_subclass_of($configuration['processor'], UrlProcessorInterface::class)) {
+                               throw new \RuntimeException('The URI processor "' . $identifier . '" defines an invalid provider. Ensure the class exists and implements the "' . UrlProcessorInterface::class . '".', 1442050579);
+                       }
+               }
+
+               $orderedProcessors = GeneralUtility::makeInstance(DependencyOrderingService::class)->orderByDependencies($urlProcessors);
+               $keepProcessing = TRUE;
+
+               foreach ($orderedProcessors as $configuration) {
+                       /** @var UrlProcessorInterface $urlProcessor */
+                       $urlProcessor = GeneralUtility::makeInstance($configuration['processor']);
+                       $url = $urlProcessor->process($context, $url, $typolinkConfiguration, $this, $keepProcessing);
+                       if (!$keepProcessing) {
+                               break;
+                       }
+               }
+
+               return $url;
+       }
+
+       /**
         * Returns the &MP variable value for a page id.
         * The function will do its best to find a MP value that will keep the page id inside the current Mount Point rootline if any.
         *
@@ -6810,21 +6794,25 @@ class ContentObjectRenderer {
 
        /**
         * Creates a href attibute for given $mailAddress.
-        * The function uses spamProtectEmailAddresses and Jumpurl functionality for encoding the mailto statement.
+        * The function uses spamProtectEmailAddresses for encoding the mailto statement.
         * If spamProtectEmailAddresses is disabled, it'll just return a string like "mailto:user@example.tld".
         *
         * @param string $mailAddress Email address
         * @param string $linktxt Link text, default will be the email address.
-        * @param string $initP Initial link parameters, only used if Jumpurl functionality is enabled. Example: ?id=5&type=0
         * @return string Returns a numerical array with two elements: 1) $mailToUrl, string ready to be inserted into the href attribute of the <a> tag, b) $linktxt: The string between starting and ending <a> tag.
         */
-       public function getMailTo($mailAddress, $linktxt, $initP = '?') {
+       public function getMailTo($mailAddress, $linktxt) {
+
                if ((string)$linktxt === '') {
                        $linktxt = $mailAddress;
                }
-               $mailToUrl = 'mailto:' . $mailAddress;
+
+               $originalMailToUrl = 'mailto:' . $mailAddress;
+               $mailToUrl = $this->processUrl(UrlProcessorInterface::CONTEXT_MAIL, $originalMailToUrl);
+
                $tsfe = $this->getTypoScriptFrontendController();
-               if (!$tsfe->config['config']['jumpurl_enable'] || $tsfe->config['config']['jumpurl_mailto_disable']) {
+               // no processing happened, therefore
+               if ($mailToUrl === $originalMailToUrl) {
                        if ($tsfe->spamProtectEmailAddresses) {
                                if ($tsfe->spamProtectEmailAddresses === 'ascii') {
                                        $mailToUrl = $tsfe->encryptEmail($mailToUrl);
@@ -6843,14 +6831,9 @@ class ContentObjectRenderer {
                                }
                                $linktxt = str_ireplace($mailAddress, $spamProtectedMailAddress, $linktxt);
                        }
-               } else {
-                       $juHash = GeneralUtility::hmac($mailToUrl, 'jumpurl');
-                       $mailToUrl = $tsfe->absRefPrefix . $tsfe->config['mainScript'] . $initP . '&jumpurl=' . rawurlencode($mailToUrl) . '&juHash=' . $juHash . $tsfe->getMethodUrlIdToken;
                }
-               return array(
-                       $mailToUrl,
-                       $linktxt
-               );
+
+               return array($mailToUrl, $linktxt);
        }
 
        /**
index 959df61..deb8c55 100644 (file)
@@ -27,6 +27,7 @@ use TYPO3\CMS\Core\Locking\Locker;
 use TYPO3\CMS\Core\Messaging\ErrorpageMessage;
 use TYPO3\CMS\Core\Page\PageRenderer;
 use TYPO3\CMS\Core\Resource\StorageRepository;
+use TYPO3\CMS\Core\Service\DependencyOrderingService;
 use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
 use TYPO3\CMS\Core\TypoScript\TemplateService;
 use TYPO3\CMS\Core\Utility\ArrayUtility;
@@ -39,6 +40,7 @@ use TYPO3\CMS\Core\Utility\MathUtility;
 use TYPO3\CMS\Core\Utility\StringUtility;
 use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;
 use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
+use TYPO3\CMS\Frontend\Http\UrlHandlerInterface;
 use TYPO3\CMS\Frontend\Page\CacheHashCalculator;
 use TYPO3\CMS\Frontend\Page\PageGenerator;
 use TYPO3\CMS\Frontend\Page\PageRepository;
@@ -133,10 +135,21 @@ class TypoScriptFrontendController {
 
        /**
         * @var string
+        * @deprecated since TYPO3 CMS 7, will be removed in TYPO3 CMS 8. JumpURL handling is moved to extensions.
         */
        public $jumpurl = '';
 
        /**
+        * Contains all URL handler instances that are active for the current request.
+        *
+        * The methods isGeneratePage(), isOutputting() and isINTincScript() depend on this property.
+        *
+        * @var \TYPO3\CMS\Frontend\Http\UrlHandlerInterface[]
+        * @see initializeRedirectUrlHandlers()
+        */
+       protected $activeUrlHandlers = [];
+
+       /**
         * Is set to 1 if a pageNotFound handler could have been called.
         * @var int
         */
@@ -881,7 +894,7 @@ class TypoScriptFrontendController {
         * @param int $type The value of GeneralUtility::_GP('type')
         * @param bool|string $no_cache The value of GeneralUtility::_GP('no_cache'), evaluated to 1/0
         * @param string $cHash The value of GeneralUtility::_GP('cHash')
-        * @param string $jumpurl The value of GeneralUtility::_GP('jumpurl')
+        * @param string $jumpurl The value of GeneralUtility::_GP('jumpurl'), unused since TYPO3 CMS 7. Will have no effect in TYPO3 CMS 8 anymore
         * @param string $MP The value of GeneralUtility::_GP('MP')
         * @param string $RDCT The value of GeneralUtility::_GP('RDCT')
         * @see index_ts.php
@@ -2193,6 +2206,7 @@ class TypoScriptFrontendController {
                        if (isset($GET_VARS['cHash'])) {
                                $this->cHash = $GET_VARS['cHash'];
                        }
+                       // @deprecated since TYPO3 7, remove in TYPO3 8 together with jumpurl property.
                        if (isset($GET_VARS['jumpurl'])) {
                                $this->jumpurl = $GET_VARS['jumpurl'];
                        }
@@ -2807,133 +2821,82 @@ class TypoScriptFrontendController {
        }
 
        /**
-        * Checks if a formmail submission can be sent as email, also used for JumpURLs
-        * should be removed once JumpURL is handled outside TypoScriptFrontendController
+        * Loops over all configured URL handlers and registers all active handlers in the redirect URL handler array.
         *
-        * @param string|NULL $locationData The input from $_POST['locationData']
-        * @return void|int
+        * @see $activeRedirectUrlHandlers
         */
-       protected function locDataCheck($locationData) {
-               $locData = explode(':', $locationData);
-               if (!$locData[1] || $this->sys_page->checkRecord($locData[1], $locData[2], 1)) {
-                       // $locData[1] -check means that a record is checked only if the locationData has a value for a record else than the page.
-                       if (!empty($this->sys_page->getPage($locData[0]))) {
-                               return 1;
-                       } else {
-                               $this->getTimeTracker()->setTSlogMessage('LocationData Error: The page pointed to by location data (' . $locationData . ') was not accessible.', 2);
+       public function initializeRedirectUrlHandlers() {
+               if (
+                       empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['urlProcessing']['urlHandlers'])
+                       || !is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['urlProcessing']['urlHandlers'])
+               ) {
+                       return;
+               }
+
+               $urlHandlers = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['urlProcessing']['urlHandlers'];
+               foreach ($urlHandlers as $identifier => $configuration) {
+                       if (empty($configuration) || !is_array($configuration)) {
+                               throw new \RuntimeException('Missing configuration for URL handler "' . $identifier . '".', 1442052263);
+                       }
+                       if (!is_string($configuration['handler']) || empty($configuration['handler']) || !class_exists($configuration['handler']) || !is_subclass_of($configuration['handler'], UrlHandlerInterface::class)) {
+                               throw new \RuntimeException('The URL handler "' . $identifier . '" defines an invalid provider. Ensure the class exists and implements the "' . UrlHandlerInterface::class . '".', 1442052249);
+                       }
+               }
+
+               $orderedHandlers = GeneralUtility::makeInstance(DependencyOrderingService::class)->orderByDependencies($urlHandlers);
+
+               foreach ($orderedHandlers as $configuration) {
+                       /** @var UrlHandlerInterface $urlHandler */
+                       $urlHandler = GeneralUtility::makeInstance($configuration['handler']);
+                       if ($urlHandler->canHandleCurrentUrl()) {
+                               $this->activeUrlHandlers[] = $urlHandler;
                        }
-               } else {
-                       $this->getTimeTracker()->setTSlogMessage('LocationData Error: Location data (' . $locationData . ') record pointed to was not accessible.', 2);
                }
-               return NULL;
        }
 
        /**
-        * Sets the jumpurl for page type "External URL"
+        * Checks if the current page points to an external URL and stores this value in the redirectUrl variable.
+        * The redirection will then be handled by the redirectToExternalUrl() method.
         *
         * @return void
+        * @deprecated since TYPO3 CMS 7, will be removed in TYPO3 CMS 8. See handleExternalUrlPage()
         */
        public function setExternalJumpUrl() {
-               if ((bool)$this->config['config']['disablePageExternalUrl'] === FALSE && $extUrl = $this->sys_page->getExtURL($this->page)) {
-                       $this->jumpurl = $extUrl;
-                       GeneralUtility::_GETset(GeneralUtility::hmac($this->jumpurl, 'jumpurl'), 'juHash');
-               }
+               GeneralUtility::logDeprecatedFunction();
+               $this->initializeRedirectUrlHandlers();
        }
 
        /**
         * Sends a header "Location" to jumpUrl, if jumpurl is set.
         * Will exit if a location header is sent (for instance if jumpUrl was triggered)
         *
-        * "jumpUrl" is a concept where external links are redirected from the index_ts.php script, which first logs the URL.
+        * "jumpUrl" is a concept where external links are redirected from the TYPO3 Frontend, but first logs the URL.
         *
         * @throws \Exception
         * @return void
+        * @deprecated since TYPO3 CMS 7, will be removed in TYPO3 CMS 8. JumpURL handling is moved to extensions.
         */
        public function jumpUrl() {
-               if ($this->jumpurl) {
-                       if (GeneralUtility::_GP('juSecure')) {
-                               $locationData = (string)GeneralUtility::_GP('locationData');
-                               // Need a type cast here because mimeType is optional!
-                               $mimeType = (string)GeneralUtility::_GP('mimeType');
-                               $hArr = array(
-                                       $this->jumpurl,
-                                       $locationData,
-                                       $mimeType
-                               );
-                               $calcJuHash = GeneralUtility::hmac(serialize($hArr));
-                               $juHash = (string)GeneralUtility::_GP('juHash');
-                               if ($juHash === $calcJuHash) {
-                                       if ($this->locDataCheck($locationData)) {
-                                               // 211002 - goes with cObj->filelink() rawurlencode() of filenames so spaces can be allowed.
-                                               $this->jumpurl = rawurldecode($this->jumpurl);
-                                               // Deny access to files that match TYPO3_CONF_VARS[SYS][fileDenyPattern] and whose parent directory is typo3conf/ (there could be a backup file in typo3conf/ which does not match against the fileDenyPattern)
-                                               $absoluteFileName = GeneralUtility::getFileAbsFileName(GeneralUtility::resolveBackPath($this->jumpurl), FALSE);
-                                               if (GeneralUtility::isAllowedAbsPath($absoluteFileName) && GeneralUtility::verifyFilenameAgainstDenyPattern($absoluteFileName) && !GeneralUtility::isFirstPartOfStr($absoluteFileName, (PATH_site . 'typo3conf'))) {
-                                                       if (@is_file($absoluteFileName)) {
-                                                               $mimeType = $mimeType ?: 'application/octet-stream';
-                                                               header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
-                                                               header('Content-Type: ' . $mimeType);
-                                                               header('Content-Disposition: attachment; filename="' . basename($absoluteFileName) . '"');
-                                                               header('Content-Length: ' . filesize($absoluteFileName));
-                                                               GeneralUtility::flushOutputBuffers();
-                                                               readfile($absoluteFileName);
-                                                               die;
-                                                       } else {
-                                                               throw new \Exception('jumpurl Secure: "' . $this->jumpurl . '" was not a valid file!', 1294585193);
-                                                       }
-                                               } else {
-                                                       throw new \Exception('jumpurl Secure: The requested file was not allowed to be accessed through jumpUrl (path or file not allowed)!', 1294585194);
-                                               }
-                                       } else {
-                                               throw new \Exception('jumpurl Secure: locationData, ' . $locationData . ', was not accessible.', 1294585195);
-                                       }
-                               } else {
-                                       throw new \Exception('jumpurl Secure: Calculated juHash did not match the submitted juHash.', 1294585196);
-                               }
-                       } else {
-                               $allowRedirect = FALSE;
-                               if (GeneralUtility::hmac($this->jumpurl, 'jumpurl') === (string)GeneralUtility::_GP('juHash')) {
-                                       $allowRedirect = TRUE;
-                               } elseif (is_array($this->TYPO3_CONF_VARS['SC_OPTIONS']['tslib/class.tslib_fe.php']['jumpurlRedirectHandler'])) {
-                                       foreach ($this->TYPO3_CONF_VARS['SC_OPTIONS']['tslib/class.tslib_fe.php']['jumpurlRedirectHandler'] as $classReference) {
-                                               $hookObject = GeneralUtility::getUserObj($classReference);
-                                               $allowRedirectFromHook = FALSE;
-                                               if (method_exists($hookObject, 'jumpurlRedirectHandler')) {
-                                                       $allowRedirectFromHook = $hookObject->jumpurlRedirectHandler($this->jumpurl, $this);
-                                               }
-                                               if ($allowRedirectFromHook === TRUE) {
-                                                       $allowRedirect = TRUE;
-                                                       break;
-                                               }
-                                       }
-                               }
-                               if ($allowRedirect) {
-                                       $TSConf = $this->getPagesTSconfig();
-                                       if ($TSConf['TSFE.']['jumpUrl_transferSession']) {
-                                               $uParts = parse_url($this->jumpurl);
-                                               $params = '&FE_SESSION_KEY=' . rawurlencode(($this->fe_user->id . '-' . md5(($this->fe_user->id . '/' . $this->TYPO3_CONF_VARS['SYS']['encryptionKey']))));
-                                               // Add the session parameter ...
-                                               $this->jumpurl .= ($uParts['query'] ? '' : '?') . $params;
-                                       }
-                                       $statusCode = HttpUtility::HTTP_STATUS_303;
-                                       if ($TSConf['TSFE.']['jumpURL_HTTPStatusCode']) {
-                                               switch ((int)$TSConf['TSFE.']['jumpURL_HTTPStatusCode']) {
-                                                       case 301:
-                                                               $statusCode = HttpUtility::HTTP_STATUS_301;
-                                                               break;
-                                                       case 302:
-                                                               $statusCode = HttpUtility::HTTP_STATUS_302;
-                                                               break;
-                                                       case 307:
-                                                               $statusCode = HttpUtility::HTTP_STATUS_307;
-                                                               break;
-                                               }
-                                       }
-                                       HttpUtility::redirect($this->jumpurl, $statusCode);
-                               } else {
-                                       throw new \Exception('jumpurl: Calculated juHash did not match the submitted juHash.', 1359987599);
-                               }
-                       }
+               GeneralUtility::logDeprecatedFunction();
+               $this->redirectToExternalUrl();
+       }
+
+       /**
+        * Loops over all registered URL handlers and lets them process the current URL.
+        *
+        * If no handler has stopped the current process (e.g. by redirecting) and a
+        * the redirectUrl propert is not empty, the user will be redirected to this URL.
+        *
+        * @internal Should be called by the FrontendRequestHandler only.
+        */
+       public function redirectToExternalUrl() {
+
+               foreach ($this->activeUrlHandlers as $redirectHandler) {
+                       $redirectHandler->handle();
+               }
+
+               if (!empty($this->activeUrlHandlers)) {
+                       throw new \RuntimeException('A URL handler is active but did not process the URL.', 1442305505);
                }
        }
 
@@ -3051,13 +3014,13 @@ class TypoScriptFrontendController {
         *
         *******************************************/
        /**
-        * Returns TRUE if the page should be generated
-        * That is if jumpurl is not set and the cacheContentFlag is not set.
+        * Returns TRUE if the page should be generated.
+        * That is if no URL handler is active and the cacheContentFlag is not set.
         *
         * @return bool
         */
        public function isGeneratePage() {
-               return !$this->cacheContentFlag && !$this->jumpurl;
+               return !$this->cacheContentFlag && empty($this->activeUrlHandlers);
        }
 
        /**
@@ -3575,12 +3538,12 @@ class TypoScriptFrontendController {
        }
 
        /**
-        * Determines if there are any INTincScripts to include
+        * Determines if there are any INTincScripts to include.
         *
-        * @return bool Returns TRUE if scripts are found (and not jumpurl)
+        * @return bool Returns TRUE if scripts are found and no URL handler is active.
         */
        public function isINTincScript() {
-               return is_array($this->config['INTincScript']) && !$this->jumpurl;
+               return is_array($this->config['INTincScript']) && empty($this->activeUrlHandlers);
        }
 
        /**
@@ -3612,13 +3575,13 @@ class TypoScriptFrontendController {
         *******************************************/
        /**
         * Determines if content should be outputted.
-        * Outputting content is done only if jumpUrl is NOT set.
+        * Outputting content is done only if no URL handler is active and no hook disables the output.
         *
-        * @return bool Returns TRUE if $this->jumpurl is not set.
+        * @return bool Returns TRUE if no redirect URL is set and no hook disables the output.
         */
        public function isOutputting() {
-               // Initialize by status of jumpUrl:
-               $enableOutput = !$this->jumpurl;
+               // Initialize by status if there is a Redirect URL
+               $enableOutput = empty($this->activeUrlHandlers);
                // Call hook for possible disabling of output:
                if (isset($this->TYPO3_CONF_VARS['SC_OPTIONS']['tslib/class.tslib_fe.php']['isOutputting']) && is_array($this->TYPO3_CONF_VARS['SC_OPTIONS']['tslib/class.tslib_fe.php']['isOutputting'])) {
                        $_params = array('pObj' => &$this, 'enableOutput' => &$enableOutput);
index 9df5032..9e678a8 100644 (file)
@@ -189,8 +189,7 @@ class RequestHandler implements RequestHandlerInterface {
                // Convert POST data to internal "renderCharset" if different from the metaCharset
                $this->controller->convPOSTCharset();
 
-               // Check JumpUrl
-               $this->controller->setExternalJumpUrl();
+               $this->controller->initializeRedirectUrlHandlers();
 
                $this->controller->handleDataSubmission();
 
@@ -253,8 +252,7 @@ class RequestHandler implements RequestHandlerInterface {
                if ($this->controller->isOutputting() && $debugParseTime) {
                        $this->controller->content .= LF . '<!-- Parsetime: ' . $this->controller->scriptParseTime . 'ms -->';
                }
-               // Check JumpUrl
-               $this->controller->jumpurl();
+               $this->controller->redirectToExternalUrl();
                // Preview info
                $this->controller->previewInfo();
                // Hook for end-of-frontend
diff --git a/typo3/sysext/frontend/Classes/Http/UrlHandlerInterface.php b/typo3/sysext/frontend/Classes/Http/UrlHandlerInterface.php
new file mode 100644 (file)
index 0000000..c49e5e8
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+namespace TYPO3\CMS\Frontend\Http;
+
+/*
+ * 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!
+ */
+
+/**
+ * This interface needs to be implemented by all classes that register for the hook in:
+ * $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['urlProcessing']['urlHandlers']
+ *
+ * It can be used to do custom URL processing during a Frontend request.
+ */
+interface UrlHandlerInterface {
+
+       /**
+        * Return TRUE if this hook handles the current URL.
+        * Warning! If TRUE is returned content rendering will be disabled!
+        * This method will be called in the constructor of the TypoScriptFrontendController
+        *
+        * @see \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::__construct()
+        * @see \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::initializeRedirectUrlHandlers()
+        * @return bool
+        */
+       public function canHandleCurrentUrl();
+
+       /**
+        * Custom processing of the current URL.
+        *
+        * If canHandle() has returned TRUE this method needs to take care of redirecting the user or generating custom output.
+        * This hook will be called BEFORE the user is redirected to an external URL configured in the page properties.
+        *
+        * @see \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::redirectToExternalUrl()
+        * @return void
+        */
+       public function handle();
+}
diff --git a/typo3/sysext/frontend/Classes/Http/UrlProcessorInterface.php b/typo3/sysext/frontend/Classes/Http/UrlProcessorInterface.php
new file mode 100644 (file)
index 0000000..0725829
--- /dev/null
@@ -0,0 +1,42 @@
+<?php
+namespace TYPO3\CMS\Frontend\Http;
+
+/*
+ * 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!
+ */
+
+/**
+ * This interface needs to be implemented by all classes that register for the hook in:
+ * $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['urlProcessing']['urlProcessors']
+ *
+ * It can be used manipulate URLs that are generated by the ContentObjectRenderer.
+ */
+interface UrlProcessorInterface {
+
+       const CONTEXT_COMMON = 'common';
+       const CONTEXT_FILE = 'file';
+       const CONTEXT_MAIL = 'mail';
+       const CONTEXT_EXTERNAL = 'external';
+
+       /**
+        * Generates the JumpURL for the given parameters.
+        *
+        * @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::processUrl()
+        * @param string $context The context in which the URL is generated (e.g. "typolink" or one of the constants above).
+        * @param string $url The URL that should be processed.
+        * @param array $configuration The link configuration.
+        * @param \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer $contentObjectRenderer The calling content object renderer.
+        * @param bool $keepProcessing If this is set to FALSE no further hooks will be processed after the current one.
+        * @return string|NULL
+        */
+       public function process($context, $url, array $configuration, \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer $contentObjectRenderer, &$keepProcessing);
+}
diff --git a/typo3/sysext/frontend/Classes/Page/ExternalPageUrlHandler.php b/typo3/sysext/frontend/Classes/Page/ExternalPageUrlHandler.php
new file mode 100644 (file)
index 0000000..ca9fdd0
--- /dev/null
@@ -0,0 +1,65 @@
+<?php
+namespace TYPO3\CMS\Frontend\Page;
+
+/*
+ * 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\Utility\HttpUtility;
+
+/**
+ * Handles the redirection for external URL pages.
+ */
+class ExternalPageUrlHandler implements \TYPO3\CMS\Frontend\Http\UrlHandlerInterface {
+
+       /**
+        * @var string
+        */
+       protected $externalUrl = '';
+
+       /**
+        * Checks if external URLs are enabled and if the current page points to an external URL.
+        *
+        * @return bool
+        */
+       public function canHandleCurrentUrl() {
+               $tsfe = $this->getTypoScriptFrontendController();
+
+               if (!empty($tsfe->config['config']['disablePageExternalUrl'])) {
+                       return FALSE;
+               }
+
+               $this->externalUrl = $tsfe->sys_page->getExtURL($tsfe->page);
+               if (empty($this->externalUrl)) {
+                       return FALSE;
+               } else {
+                       return TRUE;
+               }
+       }
+
+       /**
+        * Redirects the user to the detected external URL.
+        *
+        * @return void
+        */
+       public function handle() {
+               HttpUtility::redirect($this->externalUrl, HttpUtility::HTTP_STATUS_303);
+       }
+
+       /**
+        * @return \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController
+        */
+       protected function getTypoScriptFrontendController() {
+               return $GLOBALS['TSFE'];
+       }
+}
\ No newline at end of file
index 2463eb1..d2f85dd 100644 (file)
@@ -3663,4 +3663,24 @@ class ContentObjectRendererTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
                $this->assertEquals($expectedResult, $contentObjectRenderer->getWhere($table, $configuration));
        }
 
+       /////////////////////////////////////
+       // Test concerning link generation //
+       /////////////////////////////////////
+
+       /**
+        * @test
+        */
+       public function filelinkCreatesCorrectUrlForFileWithUrlEncodedSpecialChars() {
+
+               $fileNameAndPath = PATH_site . 'typo3temp/phpunitJumpUrlTestFile with spaces & amps.txt';
+               file_put_contents($fileNameAndPath, 'Some test data');
+               $relativeFileNameAndPath = substr($fileNameAndPath, strlen(PATH_site));
+               $fileName = substr($fileNameAndPath, strlen(PATH_site . 'typo3temp/'));
+
+               $expectedLink = str_replace('%2F', '/', rawurlencode($relativeFileNameAndPath));
+               $result = $this->subject->filelink($fileName, array('path' => 'typo3temp/'));
+               $this->assertEquals('<a href="' . $expectedLink . '">' . $fileName . '</a>', $result);
+
+               \TYPO3\CMS\Core\Utility\GeneralUtility::unlink_tempfile($fileNameAndPath);
+       }
 }
index be2c83b..c6ceb63 100644 (file)
@@ -61,3 +61,8 @@ $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['hook_prev
 
 // Register search key shortcuts
 $GLOBALS['TYPO3_CONF_VARS']['SYS']['livesearch']['content'] = 'tt_content';
+
+// Register URL handler for external pages.
+$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['urlProcessing']['urlHandlers']['frontendExternalUrl'] = [
+       'handler' => \TYPO3\CMS\Frontend\Page\ExternalPageUrlHandler::class,
+];
\ No newline at end of file
diff --git a/typo3/sysext/jumpurl/Classes/JumpUrlHandler.php b/typo3/sysext/jumpurl/Classes/JumpUrlHandler.php
new file mode 100644 (file)
index 0000000..5c1d93b
--- /dev/null
@@ -0,0 +1,331 @@
+<?php
+namespace TYPO3\CMS\Jumpurl;
+
+/*
+ * 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\Resource\FileInterface;
+use TYPO3\CMS\Core\Resource\ResourceFactory;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
+use TYPO3\CMS\Frontend\Http\UrlHandlerInterface;
+
+/**
+ * This class implements the hooks for the JumpURL functionality when accessing a page
+ * which has a GET parameter "jumpurl".
+ * It then validates the referrer
+ */
+class JumpUrlHandler implements UrlHandlerInterface {
+
+       /**
+        * @var string The current JumpURL value submitted in the GET parameters.
+        */
+       protected $url;
+
+       /**
+        * Return TRUE if this hook handles the current URL.
+        * Warning! If TRUE is returned content rendering will be disabled!
+        * This method will be called in the constructor of the TypoScriptFrontendController
+        *
+        * @see \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::__construct()
+        * @see \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::initializeCustomUrlHandlers()
+        * @return bool
+        */
+       public function canHandleCurrentUrl() {
+               $this->url = (string)GeneralUtility::_GP('jumpurl');
+               return ($this->url !== '');
+       }
+
+       /**
+        * Custom processing of the current URL.
+        *
+        * If a valid hash was submitted the user will either be redirected
+        * to the given jumpUrl or if it is a secure jumpUrl the file data
+        * will be passed to the user.
+        *
+        * If canHandle() has returned TRUE this method needs to take care of redirecting the user or generating custom output.
+        * This hook will be called BEFORE the user is redirected to an external URL configured in the page properties.
+        *
+        * @see \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::processCustomUrlHandlers()
+        * @throws \RuntimeException if Jump URL was triggered by an illegal referrer.
+        * @return void
+        */
+       public function handle() {
+               if (!$this->referrerIsValid()) {
+                       throw new \RuntimeException('The jumpUrl request was triggered by an illegal referrer.');
+               }
+
+               if ((bool)GeneralUtility::_GP('juSecure')) {
+                       $this->forwardJumpUrlSecureFileData($this->url);
+               } else {
+                       $this->redirectToJumpUrl($this->url);
+               }
+       }
+
+       /**
+        * Returns TRUE if the current referrer allows Jump URL handling.
+        * This is the case then the referrer check is disabled or when the referrer matches the current TYPO3 host.
+        *
+        * @return bool if the referer is valid.
+        */
+       protected function referrerIsValid() {
+               if (!empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['doNotCheckReferer'])) {
+                       return TRUE;
+               }
+
+               $referrer = parse_url(GeneralUtility::getIndpEnv('HTTP_REFERER'));
+               // everything is fine if no host is set, or the host matches the TYPO3_HOST
+               return (!isset($referrer['host']) || $referrer['host'] === GeneralUtility::getIndpEnv('TYPO3_HOST_ONLY'));
+       }
+
+
+       /**
+        * Redirects the user to the given jump URL if all submitted values
+        * are valid
+        *
+        * @param string $jumpUrl The URL to which the user should be redirected
+        * @throws \Exception
+        */
+       protected function redirectToJumpUrl($jumpUrl) {
+               $this->validateIfJumpUrlRedirectIsAllowed($jumpUrl);
+
+               $pageTSconfig = $this->getTypoScriptFrontendController()->getPagesTSconfig();
+               if (is_array($pageTSconfig['TSFE.'])) {
+                       $pageTSconfig = $pageTSconfig['TSFE.'];
+               } else {
+                       $pageTSconfig = array();
+               }
+
+               $jumpUrl = $this->addParametersToTransferSession($jumpUrl, $pageTSconfig);
+               $statusCode = $this->getRedirectStatusCode($pageTSconfig);
+               $this->redirect($jumpUrl, $statusCode);
+       }
+
+       /**
+        * If the submitted hash is correct and the user has access to the
+        * related content element the contents of the submitted file will
+        * be output to the user.
+        *
+        * @param string $jumpUrl The URL to the file that should be output to the user
+        * @throws \Exception
+        */
+       protected function forwardJumpUrlSecureFileData($jumpUrl) {
+               // Set the parameters required for handling a secure jumpUrl link
+               // The locationData GET parameter, containing information about the record that created the URL
+               $locationData = (string)GeneralUtility::_GP('locationData');
+               // The optional mimeType GET parameter
+               $mimeType = (string)GeneralUtility::_GP('mimeType');
+               // The jump Url Hash GET parameter
+               $juHash = (string)GeneralUtility::_GP('juHash');
+
+               // validate the hash GET parameter against the other parameters
+               if ($juHash !== JumpUrlUtility::calculateHashSecure($jumpUrl, $locationData, $mimeType)) {
+                       throw new \Exception('The calculated Jump URL secure hash ("juHash") did not match the submitted "juHash" query parameter.', 1294585196);
+               }
+
+               if (!$this->isLocationDataValid($locationData)) {
+                       throw new \Exception('The calculated secure location data "' . $locationData . '" is not accessible.', 1294585195);
+               }
+
+               // Allow spaces / special chars in filenames.
+               $jumpUrl = rawurldecode($jumpUrl);
+
+               // Deny access to files that match TYPO3_CONF_VARS[SYS][fileDenyPattern] and whose parent directory
+               // is typo3conf/ (there could be a backup file in typo3conf/ which does not match against the fileDenyPattern)
+               $absoluteFileName = GeneralUtility::getFileAbsFileName(GeneralUtility::resolveBackPath($jumpUrl), FALSE);
+
+               if (
+                       !GeneralUtility::isAllowedAbsPath($absoluteFileName)
+                       || !GeneralUtility::verifyFilenameAgainstDenyPattern($absoluteFileName)
+                       || GeneralUtility::isFirstPartOfStr($absoluteFileName, (PATH_site . 'typo3conf'))
+               ) {
+                       throw new \Exception('The requested file was not allowed to be accessed through Jump URL. The path or file is not allowed.', 1294585194);
+               }
+
+               try {
+                       $resourceFactory = $this->getResourceFactory();
+                       $file = $resourceFactory->retrieveFileOrFolderObject($absoluteFileName);
+                       $this->readFileAndExit($file, $mimeType);
+               } catch (\Exception $e) {
+                       throw new \Exception('The requested file "' . $jumpUrl . '" for Jump URL was not found..', 1294585193);
+               }
+       }
+
+       /**
+        * Checks if the given location data is valid and the connected record is accessible by the current user.
+        *
+        * @param string $locationData
+        * @return bool
+        */
+       protected function isLocationDataValid($locationData) {
+               $isValidLocationData = FALSE;
+               list($pageUid, $table, $recordUid) = explode(':', $locationData);
+               $pageRepository = $this->getTypoScriptFrontendController()->sys_page;
+               $timeTracker = $this->getTimeTracker();
+               if (empty($table) || $pageRepository->checkRecord($table, $recordUid, TRUE)) {
+                       // This check means that a record is checked only if the locationData has a value for a
+                       // record else than the page.
+                       if (!empty($pageRepository->getPage($pageUid))) {
+                               $isValidLocationData = TRUE;
+                       } else {
+                               $timeTracker->setTSlogMessage('LocationData Error: The page pointed to by location data "' . $locationData . '" was not accessible.', 2);
+                       }
+               } else {
+                       $timeTracker->setTSlogMessage('LocationData Error: Location data "' . $locationData . '" record pointed to was not accessible.', 2);
+               }
+               return $isValidLocationData;
+       }
+
+       /**
+        * This implements a hook, e.g. for direct mail to allow the redirects but only if the handler says it's alright
+        * But also checks against the common juHash parameter first
+        *
+        * @param string $jumpUrl the URL to check
+        * @throws \Exception thrown if no redirect is allowed
+        */
+       protected function validateIfJumpUrlRedirectIsAllowed($jumpUrl) {
+               $allowRedirect = FALSE;
+               if ($this->isJumpUrlHashValid($jumpUrl)) {
+                       $allowRedirect = TRUE;
+               } elseif (
+                       isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['jumpurlRedirectHandler'])
+                       && is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['jumpurlRedirectHandler'])
+               ) {
+                       foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['jumpurlRedirectHandler'] as $className) {
+                               $hookObject = GeneralUtility::getUserObj($className);
+                               if (method_exists($hookObject, 'jumpurlRedirectHandler')) {
+                                       $allowRedirect = $hookObject->jumpurlRedirectHandler($jumpUrl, $GLOBALS['TSFE']);
+                               }
+                               if ($allowRedirect) {
+                                       break;
+                               }
+                       }
+               }
+
+               if (!$allowRedirect) {
+                       throw new \Exception('The calculated Jump URL hash ("juHash") did not match the submitted "juHash" query parameter.', 1359987599);
+               }
+       }
+
+       /**
+        * Validate the jumpUrl hash against the GET/POST parameter "juHash".
+        *
+        * @param string $jumpUrl The URL to check against.
+        * @return bool
+        */
+       protected function isJumpUrlHashValid($jumpUrl) {
+               return GeneralUtility::_GP('juHash') === JumpUrlUtility::calculateHash($jumpUrl);
+       }
+
+       /**
+        * Calls the PHP readfile function and exits.
+        *
+        * @param FileInterface $file The file that should be read.
+        * @param string $mimeType Optional mime type override. If empty the automatically detected mime type will be used.
+        */
+       protected function readFileAndExit($file, $mimeType) {
+               $file->getStorage()->dumpFileContents($file, TRUE, NULL, $mimeType);
+               exit;
+       }
+
+       /**
+        * Simply calls the redirect method in the HttpUtility.
+        *
+        * @param string $jumpUrl
+        * @param int $statusCode
+        */
+       protected function redirect($jumpUrl, $statusCode) {
+               HttpUtility::redirect($jumpUrl, $statusCode);
+       }
+
+       /**
+        * Modified the URL to go to by adding the session key information to it
+        * but only if TSFE.jumpUrl_transferSession = 1 is set via pageTSconfig.
+        *
+        * @param string $jumpUrl the URL to go to
+        * @param array $pageTSconfig the TSFE. part of the TS configuration
+        *
+        * @return string the modified URL
+        */
+       protected function addParametersToTransferSession($jumpUrl, $pageTSconfig) {
+               // allow to send the current fe_user with the jump URL
+               if (!empty($pageTSconfig['jumpUrl_transferSession'])) {
+                       $uParts = parse_url($jumpUrl);
+                       /** @noinspection PhpInternalEntityUsedInspection We need access to the current frontend user ID. */
+                       $params = '&FE_SESSION_KEY=' .
+                               rawurlencode(
+                                       $this->getTypoScriptFrontendController()->fe_user->id . '-' .
+                                       md5(
+                                               $this->getTypoScriptFrontendController()->fe_user->id . '/' .
+                                               $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey']
+                                       )
+                               );
+                       // Add the session parameter ...
+                       $jumpUrl .= ($uParts['query'] ? '' : '?') . $params;
+               }
+               return $jumpUrl;
+       }
+
+       /**
+        * Returns one of the HTTP_STATUS_* constants of the HttpUtility that matches
+        * the configured HTTP status code in TSFE.jumpURL_HTTPStatusCode Page TSconfig.
+        *
+        * @param array $pageTSconfig
+        * @return string
+        * @throws \InvalidArgumentException If the configured status code is not valid.
+        */
+       protected function getRedirectStatusCode($pageTSconfig) {
+               $statusCode = HttpUtility::HTTP_STATUS_303;
+
+               if (!empty($pageTSconfig['jumpURL_HTTPStatusCode'])) {
+                       switch ((int)$pageTSconfig['jumpURL_HTTPStatusCode']) {
+                               case 301:
+                                       $statusCode = HttpUtility::HTTP_STATUS_301;
+                                       break;
+                               case 302:
+                                       $statusCode = HttpUtility::HTTP_STATUS_302;
+                                       break;
+                               case 307:
+                                       $statusCode = HttpUtility::HTTP_STATUS_307;
+                                       break;
+                               default:
+                                       throw new \InvalidArgumentException('The configured jumpURL_HTTPStatusCode option is invalid. Allowed codes are 301, 302 and 307.', 1381768833);
+                       }
+               }
+
+               return $statusCode;
+       }
+
+       /**
+        * @return \TYPO3\CMS\Core\TimeTracker\TimeTracker
+        */
+       protected function getTimeTracker() {
+               return $GLOBALS['TT'];
+       }
+
+       /**
+        * Returns an instance of the ResourceFactory.
+        *
+        * @return ResourceFactory
+        */
+       protected function getResourceFactory() {
+               return ResourceFactory::getInstance();
+       }
+
+       /**
+        * @return \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController
+        */
+       protected function getTypoScriptFrontendController() {
+               return $GLOBALS['TSFE'];
+       }
+}
diff --git a/typo3/sysext/jumpurl/Classes/JumpUrlProcessor.php b/typo3/sysext/jumpurl/Classes/JumpUrlProcessor.php
new file mode 100644 (file)
index 0000000..075fc3b
--- /dev/null
@@ -0,0 +1,204 @@
+<?php
+namespace TYPO3\CMS\Jumpurl;
+
+/*
+ * 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\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\StringUtility;
+use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
+use TYPO3\CMS\Frontend\Http\UrlProcessorInterface;
+
+/**
+ * This class implements the hooks for creating jump URLs when links (typolink, mailtoLink) are built
+ */
+class JumpUrlProcessor implements UrlProcessorInterface {
+
+       /**
+        * @var ContentObjectRenderer
+        */
+       protected $contentObjectRenderer;
+
+       /**
+        * Generates the JumpURL for the given parameters.
+        *
+        * @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::processUrlModifiers()
+        * @param string $context The context in which the URL is generated (e.g. "typolink").
+        * @param string $url The URL that should be processed.
+        * @param array $configuration The link configuration.
+        * @param \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer $contentObjectRenderer The calling content object renderer.
+        * @param bool $keepProcessing If this is set to FALSE no further hooks will be processed after the current one.
+        * @return string
+        */
+       public function process($context, $url, array $configuration, ContentObjectRenderer $contentObjectRenderer, &$keepProcessing) {
+               if (!$this->isEnabled($context, $configuration)) {
+                       return $url;
+               }
+
+               $this->contentObjectRenderer = $contentObjectRenderer;
+
+               // Strip the absRefPrefix from the URLs.
+               $urlPrefix = (string)$this->getTypoScriptFrontendController()->absRefPrefix;
+               if ($urlPrefix !== '' && StringUtility::beginsWith($url, $urlPrefix)) {
+                       $url = substr($url, strlen($urlPrefix));
+               }
+
+               // Make sure the slashes in the file URL are not encoded.
+               if ($context === UrlProcessorInterface::CONTEXT_FILE) {
+                       $url = str_replace('%2F', '/', rawurlencode(rawurldecode($url)));
+               }
+
+               $url = $this->build($url, isset($configuration['jumpurl.']) ? $configuration['jumpurl.'] : array());
+
+               // Now add the prefix again if it was not added by a typolink call already.
+               if ($urlPrefix !== '' && !StringUtility::beginsWith($url, $urlPrefix)) {
+                       $url = $urlPrefix . $url;
+               }
+
+               return $url;
+       }
+
+       /**
+        * Returns TRUE if jumpurl was enabled in the global configuration
+        * or in the given configuration
+        *
+        * @param string $context separate check for the MAIL context needed
+        * @param array $configuration Optional jump URL configuration
+        * @return bool TRUE if enabled, FALSE if disabled
+        */
+       protected function isEnabled($context, array $configuration = array()) {
+               if (!empty($configuration['jumpurl.']['forceDisable'])) {
+                       return FALSE;
+               }
+
+               $enabled = !empty($configuration['jumpurl']);
+
+               // if jumpurl is explicitly set to 0 we override the global configuration
+               if (!$enabled && $this->getTypoScriptFrontendController()->config['config']['jumpurl_enable']) {
+                       $enabled = !isset($configuration['jumpurl']) || $configuration['jumpurl'];
+               }
+
+               // If we have a mailto link and jumpurl is not explicitly enabled
+               // but globally disabled for mailto links we disable it
+               if (
+                       empty($configuration['jumpurl']) && $context === UrlProcessorInterface::CONTEXT_MAIL
+                       && $this->getTypoScriptFrontendController()->config['config']['jumpurl_mailto_disable']
+               ) {
+                       $enabled = FALSE;
+               }
+
+               return $enabled;
+       }
+
+
+       /**
+        * Builds a jump URL for the given URL
+        *
+        * @param string $url The URL to which will be jumped
+        * @param array $configuration Optional TypoLink configuration
+        * @return string The generated URL
+        */
+       protected function build($url, array $configuration) {
+               $urlParameters = array('jumpurl' => $url);
+
+               // see if a secure File URL should be built
+               if (!empty($configuration['secure'])) {
+                       $secureParameters = $this->getParametersForSecureFile(
+                               $url,
+                               isset($configuration['secure.']) ? $configuration['secure.'] : array()
+                       );
+                       $urlParameters = array_merge($urlParameters, $secureParameters);
+               } else {
+                       $urlParameters['juHash'] = JumpUrlUtility::calculateHash($url);
+               }
+
+               $typoLinkConfiguration = array(
+                       'parameter' => $this->getTypoLinkParameter($configuration),
+                       'additionalParams' => GeneralUtility::implodeArrayForUrl('', $urlParameters),
+                       // make sure jump URL is not called again
+                       'jumpurl.' => array('forceDisable' => '1')
+               );
+
+               return $this->getContentObjectRenderer()->typoLink_URL($typoLinkConfiguration);
+       }
+
+       /**
+        * Returns a URL parameter array containing parameters for secure downloads by "jumpurl".
+        * Helper function for filelink()
+        *
+        * The array returned has the following structure:
+        * juSecure => is always 1,
+        * locationData => information about the record that created the jumpUrl,
+        * juHash => the hash that will be checked before the file is downloadable
+        * [mimeType => the mime type of the file]
+        *
+        * @param string $jumpUrl The URL to jump to, basically the filepath
+        * @param array $configuration TypoScript properties for the "jumpurl.secure" property of "filelink"
+        * @return array URL parameters required for jumpUrl secure
+        *
+        */
+       protected function getParametersForSecureFile($jumpUrl, array $configuration) {
+               $parameters = array(
+                       'juSecure' => 1,
+                       'locationData' => $this->getTypoScriptFrontendController()->id . ':' . $this->getContentObjectRenderer()->currentRecord
+               );
+
+               $pathInfo = pathinfo($jumpUrl);
+               if (!empty($pathInfo['extension'])) {
+                       $mimeTypes = GeneralUtility::trimExplode(',', $configuration['mimeTypes'], TRUE);
+                       foreach ($mimeTypes as $mimeType) {
+                               list($fileExtension, $mimeType) = GeneralUtility::trimExplode('=', $mimeType, FALSE, 2);
+                               if (strtolower($pathInfo['extension']) === strtolower($fileExtension)) {
+                                       $parameters['mimeType'] = $mimeType;
+                                       break;
+                               }
+                       }
+               }
+               $parameters['juHash'] = JumpUrlUtility::calculateHashSecure($jumpUrl, $parameters['locationData'], $parameters['mimeType']);
+               return $parameters;
+       }
+
+       /**
+        * Checks if an alternative link parameter was configured and if not
+        * a default parameter will be generated based on the current page
+        * ID and type.
+        * When linking to a file this method is needed
+        *
+        * @param array $configuration Data from the TypoLink jumpurl configuration
+        * @return string The parameter for the jump URL TypoLink
+        */
+       protected function getTypoLinkParameter(array $configuration) {
+               $linkParameter = $this->getContentObjectRenderer()->stdWrapValue('parameter', $configuration);
+
+               if (empty($linkParameter)) {
+                       $frontendController = $this->getTypoScriptFrontendController();
+                       $linkParameter = $frontendController->id . ',' . $frontendController->type;
+               }
+
+               return $linkParameter;
+       }
+
+       /**
+        * @return \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController
+        */
+       protected function getTypoScriptFrontendController() {
+               return $GLOBALS['TSFE'];
+       }
+
+       /**
+        * @return ContentObjectRenderer
+        */
+       protected function getContentObjectRenderer() {
+               return $this->contentObjectRenderer ?: $this->getTypoScriptFrontendController()->cObj;
+       }
+}
diff --git a/typo3/sysext/jumpurl/Classes/JumpUrlUtility.php b/typo3/sysext/jumpurl/Classes/JumpUrlUtility.php
new file mode 100644 (file)
index 0000000..7e0a2c1
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+namespace TYPO3\CMS\Jumpurl;
+
+/*
+ * 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\Utility\GeneralUtility;
+
+/**
+ * This class contains functions for generating and validating jump URLs
+ */
+class JumpUrlUtility {
+
+       /**
+        * Calculates the hash for the given jump URL
+        *
+        * @param string $jumpUrl The target URL
+        * @return string The calculated hash
+        */
+       public static function calculateHash($jumpUrl) {
+               return GeneralUtility::hmac($jumpUrl, 'jumpurl');
+       }
+
+       /**
+        * Calculates the hash for the given jump URL secure data.
+        *
+        * @param string $jumpUrl The URL to the file
+        * @param string $locationData Information about the record that rendered the jump URL, format is [pid]:[table]:[uid]
+        * @param string $mimeType Mime type of the file or an empty string
+        * @return string The calculated hash
+        */
+       public static function calculateHashSecure($jumpUrl, $locationData, $mimeType) {
+               $data = array((string)$jumpUrl, (string)$locationData, (string)$mimeType);
+               return GeneralUtility::hmac(serialize($data));
+       }
+}
diff --git a/typo3/sysext/jumpurl/Documentation/.gitignore b/typo3/sysext/jumpurl/Documentation/.gitignore
new file mode 100644 (file)
index 0000000..6cd159f
--- /dev/null
@@ -0,0 +1,7 @@
+# this is file .gitignore
+
+# ignore everything in this directory
+_make/*
+
+# but do not ignore this file
+!_not_versioned/.gitignore
diff --git a/typo3/sysext/jumpurl/Documentation/Configuration/Index.rst b/typo3/sysext/jumpurl/Documentation/Configuration/Index.rst
new file mode 100644 (file)
index 0000000..5e817a3
--- /dev/null
@@ -0,0 +1,208 @@
+.. ==================================================
+.. FOR YOUR INFORMATION
+.. --------------------------------------------------
+.. -*- coding: utf-8 -*- with BOM.
+
+.. include:: ../Includes.txt
+
+
+.. _configuration:
+
+Configuration
+-------------
+
+This Extension is configured by global :ref:`TypoScript config <configuration-global>`,
+:ref:`typolink options <configuration-global-jumpurl-enable>` and
+:ref:`configuration-typo3-conf-vars`.
+
+
+.. _configuration-global:
+
+Global TypoScript configuration
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+JumpURL can be configured globally in the ``config`` TypoScript namespace.
+
+
+.. ### BEGIN~OF~TABLE ###
+
+
+.. _configuration-global-jumpurl-enable:
+
+jumpurl\_enable
+"""""""""""""""
+
+.. container:: table-row
+
+   Property
+         jumpurl\_enable
+
+   Data type
+         boolean
+
+   Description
+         Enables JumpURL for all supported contexts for all generated links.
+
+
+.. _configuration-global-jumpurl-mailto-disable:
+
+jumpurl\_mailto\_disable
+""""""""""""""""""""""""
+
+.. container:: table-row
+
+   Property
+         jumpurl\_mailto\_disable
+
+   Data type
+         boolean
+
+   Description
+         Disables the use of JumpURL when linking to email-adresses.
+
+
+.. ###### END~OF~TABLE ######
+
+
+.. _configuration-typolink:
+
+typolink settings
+^^^^^^^^^^^^^^^^^
+
+JumpURL options can be provided in the :ref:`typolink <t3tsref:typolink>` and the :ref:`filelink <t3tsref:filelink>`
+configuration.
+
+The ``typolink`` options can be provided directly in the ``typolink`` namespace, e.g.:
+
+.. code-block:: typoscript
+
+   mylink = TEXT
+   mylink.value = typo3.org
+   mylink.typolink.parameter = http://www.typo3.org
+   mylink.typolink.jumpurl = 1
+
+
+The :code:`filelink` options can be provided in the :code:`typolinkConfiguration` property:
+
+.. code-block:: typoscript
+
+   mylink = TEXT
+   mylink.value = text.txt
+   mylink.filelink.path = fileadmin/
+   mylink.filelink.typolinkConfiguration.jumpurl = 1
+
+
+The following options are available for JumpURLs.
+
+
+.. ### BEGIN~OF~TABLE ###
+
+
+.. _configuration-typolink-jumpurl:
+
+jumpurl
+"""""""
+
+.. container:: table-row
+
+   Property
+         jumpurl
+
+   Data type
+         boolean
+
+   Description
+         Enables JumpURL for the current link if it points to an external URL or a file.
+
+         Please note that this does not work for internal links or for email links.
+
+         To enable JumpURL for email links the global setting needs to be used.
+
+
+.. _configuration-typolink-jumpurl-force-disable:
+
+jumpurl.forceDisable
+""""""""""""""""""""
+
+.. container:: table-row
+
+   Property
+         jumpurl.forceDisable
+
+   Data type
+         boolean
+
+   Description
+         Disables JumpURL.
+
+         This will override the global setting config.jumpurl_enable for the current link.
+
+
+.. _configuration-typolink-jumpurl-secure:
+
+jumpurl.secure
+""""""""""""""
+
+.. container:: table-row
+
+   Property
+         jumpurl.secure
+
+   Data type
+         boolean
+
+   Description
+         Enables JumpURL secure. This option is only available for file links.
+
+         If set, then the file pointed to by jumpurl is **not** redirected to, but rather it's read
+         from the file and returned with a correct header.
+
+         This option adds a hash and locationData to the URL and there MUST be access to the record
+         in order to download the file.
+
+         If the file  position on the server is furthermore secured by a .htaccess file preventing ANY
+         access, you've got secure download here!
+
+
+.. _configuration-typolink-jumpurl-secure-mime-types:
+
+jumpurl.secure.mimeTypes
+""""""""""""""""""""""""
+
+.. container:: table-row
+
+   Property
+         jumpurl.secure.mimeTypes
+
+   Data type
+         string
+
+   Description
+         With this option you can specify an alternative mime type that is sent in the HTTP Content-Type
+         header when the file is delivered to the user. By default the automatically detected mime type
+         will be used.
+
+         Syntax: [ext] = [MIME type1], [ext2] = [MIME type2], ...
+
+         **Example:**
+
+         .. code-block:: typoscript
+
+            jumpurl.secure = 1
+            jumpurl.secure.mimeTypes = pdf=application/pdf, doc=application/msword
+
+
+.. ###### END~OF~TABLE ######
+
+
+.. _configuration-typo3-conf-vars:
+
+TYPO3_CONF_VARS
+^^^^^^^^^^^^^^^
+
+The setting :php:`$GLOBALS['TYPO3_CONF_VARS']['SYS']['doNotCheckReferer']` can be used to disable
+the referer check during jumpurl handling. By default the referring host must match the current
+host, otherwise processing is stopped.
+
+The setting :php:`$GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey']` is used for generating
+the hashes submitted in the URLs.
\ No newline at end of file
diff --git a/typo3/sysext/jumpurl/Documentation/Includes.txt b/typo3/sysext/jumpurl/Documentation/Includes.txt
new file mode 100644 (file)
index 0000000..38aa398
--- /dev/null
@@ -0,0 +1,21 @@
+.. ==================================================
+.. FOR YOUR INFORMATION
+.. --------------------------------------------------
+.. -*- coding: utf-8 -*- with BOM.
+
+.. This is 'Includes.txt'. It is included at the very top of each and
+   every ReST source file in this documentation project (= manual).
+
+
+.. ==================================================
+.. DEFINE SOME TEXTROLES
+.. --------------------------------------------------
+
+.. role::   typoscript(code)
+
+.. role::   ts(typoscript)
+   :class:  typoscript
+
+.. role::   php(code)
+
+.. highlight:: php
diff --git a/typo3/sysext/jumpurl/Documentation/Index.rst b/typo3/sysext/jumpurl/Documentation/Index.rst
new file mode 100644 (file)
index 0000000..16ba3ce
--- /dev/null
@@ -0,0 +1,60 @@
+.. ==================================================
+.. FOR YOUR INFORMATION
+.. --------------------------------------------------
+.. -*- coding: utf-8 -*- with BOM.
+
+.. include:: Includes.txt
+
+
+.. _start:
+
+========
+JumpURL
+========
+
+:Extension key:
+      jumpurl
+
+:Version:
+      |release|
+
+:Language:
+      en
+
+:Description:
+      Allows to modify links to create Jump URLs created in the frontend of the TYPO3 Core.
+
+:Keywords:
+      jumpurl, sysext
+
+:Copyright:
+      2015
+
+:Author:
+      TYPO3 CMS Core Development Team
+
+:License:
+      Open Content License available from `www.opencontent.org/opl.shtml
+      <http://www.opencontent.org/opl.shtml>`_
+
+:Rendered:
+      |today|
+
+The content of this document is related to TYPO3,
+
+a GNU/GPL CMS/Framework available from `www.typo3.org
+<http://www.typo3.org/>`_
+
+
+
+
+**Table of Contents**
+
+.. toctree::
+   :maxdepth: 5
+   :titlesonly:
+   :glob:
+
+   Introduction/Index
+   Configuration/Index
+   Targets
diff --git a/typo3/sysext/jumpurl/Documentation/Introduction/Index.rst b/typo3/sysext/jumpurl/Documentation/Introduction/Index.rst
new file mode 100644 (file)
index 0000000..64cb348
--- /dev/null
@@ -0,0 +1,57 @@
+.. ==================================================
+.. FOR YOUR INFORMATION
+.. --------------------------------------------------
+.. -*- coding: utf-8 -*- with BOM.
+
+.. include:: ../Includes.txt
+
+
+
+.. _introduction:
+
+Introduction
+------------
+
+**Hint!** The JumpURL functionality is deprecated and might be removed in future versions.
+
+
+.. _what-does-it-do:
+
+What does it do?
+^^^^^^^^^^^^^^^^
+
+This Extension bundles the JumpURL functionality of TYPO3. JumpURL consists of
+two components: link tracking and secure file access.
+
+
+.. _introduction-link-tracking:
+
+Link tracking
+^^^^^^^^^^^^^
+
+The redirection to external URLs will be handled by a request to TYPO3. This allows the tracking
+of clicks on links to external pages. Such a URL might look like this:
+
+http://mytypo3.tld/index.php?id=1&jumpurl=http%3A%2F%2Fwww.typo3.org&juHash=XXX
+
+When this URL is processed by TYPO3 the user will be redirected to http://www.typo3.org if
+the sumitted juHash is valid.
+
+The same functionality can be used for file and email links.
+
+
+.. _introduction-secure-file-access:
+
+Secure file access
+^^^^^^^^^^^^^^^^^^
+
+JumpURL can also make files downloadable that are not directly accessible by the web server.
+This feature is called "JumpURL secure".
+
+A secure JumpURL link will deliver a file if the submitted hash matches. The record that
+references the file needs to be accessible by the current user. When the referencing
+record is removed or hidden the file will not be delivered to the user any more.
+
+
+
+
diff --git a/typo3/sysext/jumpurl/Documentation/Settings.yml b/typo3/sysext/jumpurl/Documentation/Settings.yml
new file mode 100644 (file)
index 0000000..89d21cc
--- /dev/null
@@ -0,0 +1,15 @@
+# This is the project specific Settings.yml file.
+# Place Sphinx specific build information here.
+# Settings given here will replace the settings of 'conf.py'.
+
+---
+conf.py:
+  copyright: 2015
+  project: JumpURL
+  version: 1
+  release: 1
+  intersphinx_mapping:
+    t3tsref:
+    - http://docs.typo3.org/typo3cms/TyposcriptReference/
+    - null
+...
diff --git a/typo3/sysext/jumpurl/Documentation/Targets.rst b/typo3/sysext/jumpurl/Documentation/Targets.rst
new file mode 100644 (file)
index 0000000..c7c43c0
--- /dev/null
@@ -0,0 +1,13 @@
+.. ==================================================
+.. FOR YOUR INFORMATION
+.. --------------------------------------------------
+.. -*- coding: utf-8 -*- with BOM.
+
+.. include:: Includes.txt
+
+.. _index-labels-for-crossreferencing:
+
+Index: Labels for Crossreferencing
+==================================
+
+.. ref-targets-list::
diff --git a/typo3/sysext/jumpurl/Tests/Unit/ContentObjectRendererTest.php b/typo3/sysext/jumpurl/Tests/Unit/ContentObjectRendererTest.php
new file mode 100644 (file)
index 0000000..29e2719
--- /dev/null
@@ -0,0 +1,398 @@
+<?php
+namespace TYPO3\CMS\Jumpurl\Tests\Unit;
+
+/*
+ * 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\Log\LoggerInterface;
+use TYPO3\CMS\Core\Charset\CharsetConverter;
+use TYPO3\CMS\Core\Database\DatabaseConnection;
+use TYPO3\CMS\Core\Log\LogManager;
+use TYPO3\CMS\Core\TimeTracker\TimeTracker;
+use TYPO3\CMS\Core\TypoScript\TemplateService;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
+use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
+use TYPO3\CMS\Frontend\Page\PageRepository;
+use TYPO3\CMS\Jumpurl\JumpUrlHandler;
+use TYPO3\CMS\Jumpurl\JumpUrlProcessor;
+
+/**
+ * Testcase for the jumpurl processing in TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer.
+ */
+class ContentObjectRendererTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
+
+       /**
+        * @var array A backup of registered singleton instances
+        */
+       protected $singletonInstances = array();
+
+       /**
+        * @var \PHPUnit_Framework_MockObject_MockObject|\TYPO3\CMS\Core\Tests\AccessibleObjectInterface|\TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer
+        */
+       protected $subject = NULL;
+
+       /**
+        * @var \PHPUnit_Framework_MockObject_MockObject|\TYPO3\CMS\Core\TypoScript\TemplateService
+        */
+       protected $templateServiceMock = NULL;
+
+       /**
+        * @var \PHPUnit_Framework_MockObject_MockObject|\TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController|\TYPO3\CMS\Core\Tests\AccessibleObjectInterface
+        */
+       protected $typoScriptFrontendControllerMock = NULL;
+
+       /**
+        * Set up
+        */
+       public function setUp() {
+               $this->singletonInstances = GeneralUtility::getSingletonInstances();
+               $this->createMockedLoggerAndLogManager();
+
+               $this->templateServiceMock = $this->getMock(TemplateService::class, array('getFileName'));
+               $pageRepositoryMock = $this->getMock(PageRepository::class, array('getPage'));
+
+               $this->typoScriptFrontendControllerMock = $this->getAccessibleMock(TypoScriptFrontendController::class, array('dummy'), array(), '', FALSE);
+               $this->typoScriptFrontendControllerMock->tmpl = $this->templateServiceMock;
+               $this->typoScriptFrontendControllerMock->config = array();
+               $this->typoScriptFrontendControllerMock->page = array();
+               $this->typoScriptFrontendControllerMock->sys_page = $pageRepositoryMock;
+               $this->typoScriptFrontendControllerMock->csConvObj = new CharsetConverter();
+               $this->typoScriptFrontendControllerMock->renderCharset = 'utf-8';
+               $GLOBALS['TSFE'] = $this->typoScriptFrontendControllerMock;
+
+               $GLOBALS['TT'] = $this->getMock(TimeTracker::class, array('dummy'));
+
+               $GLOBALS['TYPO3_DB'] = $this->getMock(DatabaseConnection::class, array());
+               $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] = '12345';
+
+               $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['urlProcessing']['urlProcessors']['jumpurl']['processor'] = JumpUrlProcessor::class;
+               $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['urlProcessing']['urlHandlers']['jumpurl']['handler'] = JumpUrlHandler::class;
+
+               $this->subject = $this->getAccessibleMock(
+                       ContentObjectRenderer::class,
+                       array('getResourceFactory', 'getEnvironmentVariable'),
+                       array($this->typoScriptFrontendControllerMock)
+               );
+               $this->subject->start(array(), 'tt_content');
+       }
+
+       public function tearDown() {
+               GeneralUtility::resetSingletonInstances($this->singletonInstances);
+               parent::tearDown();
+       }
+
+       /**
+        * @test
+        */
+       public function filelinkCreatesCorrectJumpUrlSecureForFileWithUrlEncodedSpecialChars() {
+
+               $testData = $this->initializeJumpUrlTestEnvironment($this->once());
+
+               $fileNameAndPath = PATH_site . 'typo3temp/phpunitJumpUrlTestFile with spaces & amps.txt';
+               file_put_contents($fileNameAndPath, 'Some test data');
+               $relativeFileNameAndPath = substr($fileNameAndPath, strlen(PATH_site));
+               $fileName = substr($fileNameAndPath, strlen(PATH_site . 'typo3temp/'));
+
+               $expectedHash = '304b8c8e022e92e6f4d34e97395da77705830818';
+               $expectedLink = htmlspecialchars($testData['absRefPrefix'] . $testData['mainScript'] . '?id=' . $testData['pageId'] . '&type=' . $testData['pageType'] . '&jumpurl=' . rawurlencode(str_replace('%2F', '/', rawurlencode($relativeFileNameAndPath))) . '&juSecure=1&locationData=' . rawurlencode($testData['locationData']) . '&juHash=' . $expectedHash);
+
+               $result = $this->subject->filelink($fileName, array('path' => 'typo3temp/', 'jumpurl' => '1', 'jumpurl.' => array('secure' => 1)));
+               $this->assertEquals('<a href="' . $expectedLink . '">' . $fileName . '</a>', $result);
+
+               GeneralUtility::unlink_tempfile($fileNameAndPath);
+       }
+
+       /**
+        * @test
+        */
+       public function filelinkCreatesCorrectSecureJumpUrlIfConfigured() {
+
+               $testData = $this->initializeJumpUrlTestEnvironment($this->once());
+
+               $fileNameAndPath = PATH_site . 'typo3temp/phpunitJumpUrlTestFile.txt';
+               file_put_contents($fileNameAndPath, 'Some test data');
+               $relativeFileNameAndPath = substr($fileNameAndPath, strlen(PATH_site));
+               $fileName = substr($fileNameAndPath, strlen(PATH_site . 'typo3temp/'));
+
+               $expectedHash = '1933f3c181db8940acfcd4d16c74643947179948';
+               $expectedLink = htmlspecialchars($testData['absRefPrefix'] . $testData['mainScript'] . '?id=' . $testData['pageId'] . '&type=' . $testData['pageType'] . '&jumpurl=' . rawurlencode($relativeFileNameAndPath) . '&juSecure=1&locationData=' . rawurlencode($testData['locationData']) . '&juHash=' . $expectedHash);
+
+               $result = $this->subject->filelink($fileName, array('path' => 'typo3temp/', 'jumpurl' => '1', 'jumpurl.' => array('secure' => 1)));
+               $this->assertEquals('<a href="' . $expectedLink . '">' . $fileName . '</a>', $result);
+
+               GeneralUtility::unlink_tempfile($fileNameAndPath);
+       }
+
+       /**
+        * @test
+        */
+       public function filelinkDisablesGlobalJumpUrlIfConfigured() {
+
+               $testData = $this->initializeJumpUrlTestEnvironment($this->never());
+
+               $fileName = 'phpunitJumpUrlTestFile.txt';
+               $fileNameAndPath = 'typo3temp/' . $fileName;
+               file_put_contents(PATH_site . $fileNameAndPath, 'Some test data');
+
+               $expectedLink = $testData['absRefPrefix'] . $fileNameAndPath;
+               $expectedLink = '<a href="' . $expectedLink . '">' . $fileName . '</a>';
+
+               // Test with deprecated configuration, TODO: remove when deprecated code is removed!
+               $result = $this->subject->filelink($fileName, array('path' => 'typo3temp/', 'jumpurl' => 0));
+               $this->assertEquals($expectedLink, $result);
+
+               GeneralUtility::unlink_tempfile($fileNameAndPath);
+       }
+
+       /**
+        * @test
+        */
+       public function filelinkDisablesGlobalJumpUrlWithDeprecatedOptionIfConfigured() {
+
+               $testData = $this->initializeJumpUrlTestEnvironment($this->never());
+
+               $fileName = 'phpunitJumpUrlTestFile.txt';
+               $fileNameAndPath = 'typo3temp/' . $fileName;
+               file_put_contents(PATH_site . $fileNameAndPath, 'Some test data');
+
+               $expectedLink = $testData['absRefPrefix'] . $fileNameAndPath;
+               $expectedLink = '<a href="' . $expectedLink . '">' . $fileName . '</a>';
+
+               // Test with deprecated configuration
+               $result = $this->subject->filelink($fileName, array('path' => 'typo3temp/', 'jumpurl' => 0));
+               $this->assertEquals($expectedLink, $result);
+
+               GeneralUtility::unlink_tempfile($fileNameAndPath);
+       }
+
+       /**
+        * @test
+        */
+       public function makeHttpLinksCreatesCorrectJumpUrlIfConfigured() {
+
+               $testData = $this->initializeJumpUrlTestEnvironment();
+
+               $testUrl = 'http://www.mytesturl.tld';
+               $expectedHash = '7d2261b12682a4b73402ae67415e09f294b29a55';
+
+               $expectedLinkFirstPart = $testData['absRefPrefix'] . $testData['mainScript'] . '?id=' . $testData['pageId'] . '&type=' . $testData['pageType'] . '&jumpurl=' . rawurlencode($testUrl);
+               $expectedLinkSecondPart = '&juHash=' . $expectedHash;
+
+               // due to a bug in the jump URL generation in the old version only
+               // the first part of the link is encoded which does not make much sense.
+               $expectedLink = htmlspecialchars($expectedLinkFirstPart . $expectedLinkSecondPart);
+
+               $result = $this->subject->http_makelinks('teststring ' . $testUrl . ' anotherstring', array('keep' => 'scheme'));
+               $this->assertEquals('teststring <a href="' . $expectedLink . '">' . $testUrl . '</a> anotherstring', $result);
+       }
+
+       /**
+        * @test
+        */
+       public function makeMailtoLinksCreatesCorrectJumpUrlIfConfigured() {
+
+               $testData = $this->initializeJumpUrlTestEnvironment();
+
+               $testMail = 'mail@ddress.tld';
+               $testMailto = 'mailto:' . $testMail;
+               $expectedHash = 'bd82328dc40755f5d0411e2e16e7c0cbf33b51b7';
+               $expectedLink = htmlspecialchars($testData['absRefPrefix'] . $testData['mainScript'] . '?id=' . $testData['pageId'] . '&type=' . $testData['pageType'] . '&jumpurl=' . rawurlencode($testMailto) . '&juHash=' . $expectedHash);
+               $result = $this->subject->mailto_makelinks('teststring ' . $testMailto . ' anotherstring', array());
+
+               $this->assertEquals('teststring <a href="' . $expectedLink . '">' . $testMail . '</a> anotherstring', $result);
+       }
+
+       /**
+        * @test
+        */
+       public function typoLinkCreatesCorrectJumpUrlForExternalUrl() {
+
+               $testData = $this->initializeJumpUrlTestEnvironment();
+
+               $testAddress = 'http://external.domain.tld';
+               $expectedHash = '8591c573601d17f37e06aff4ac14c78f107dd49e';
+               $expectedUrl = $testData['absRefPrefix'] . $testData['mainScript'] . '?id=' . $testData['pageId'] . '&type=' . $testData['pageType'] . '&jumpurl=' . rawurlencode($testAddress) . '&juHash=' . $expectedHash;
+               $generatedUrl = $this->subject->typoLink_URL(array('parameter' => $testAddress));
+
+               $this->assertEquals($expectedUrl, $generatedUrl);
+       }
+
+       /**
+        * @test
+        */
+       public function typoLinkCreatesCorrectJumpUrlForExternalUrlWithUrlEncodedParameters() {
+
+               $testData = $this->initializeJumpUrlTestEnvironment();
+
+               $testAddress = 'http://external.domain.tld?parameter1=' . rawurlencode('parameter[data]with&a lot-of-special/chars');
+               $expectedHash = 'cfc95f583da7689238e98bbc8930ebd820f0d20f';
+               $expectedUrl = $testData['absRefPrefix'] . $testData['mainScript'] . '?id=' . $testData['pageId'] . '&type=' . $testData['pageType'] . '&jumpurl=' . rawurlencode($testAddress) . '&juHash=' . $expectedHash;
+               $generatedUrl = $this->subject->typoLink_URL(array('parameter' => $testAddress));
+
+               $this->assertEquals($expectedUrl, $generatedUrl);
+       }
+
+       /**
+        * @test
+        */
+       public function typoLinkCreatesCorrectJumpUrlForFile() {
+
+               $testData = $this->initializeJumpUrlTestEnvironment();
+
+               $fileNameAndPath = PATH_site . 'typo3temp/phpunitJumpUrlTestFile.txt';
+               file_put_contents($fileNameAndPath, 'Some test data');
+               $relativeFileNameAndPath = substr($fileNameAndPath, strlen(PATH_site));
+
+               $testAddress = $relativeFileNameAndPath;
+               $expectedHash = 'e36be153c32f4d4d0db1414e47a05cf3149923ae';
+               $expectedUrl = $testData['absRefPrefix'] . $testData['mainScript'] . '?id=' . $testData['pageId'] . '&type=' . $testData['pageType'] . '&jumpurl=' . rawurlencode($testAddress) . '&juHash=' . $expectedHash;
+               $generatedUrl = $this->subject->typoLink_URL(array('parameter' => $testAddress));
+
+               $this->assertEquals($expectedUrl, $generatedUrl);
+
+               GeneralUtility::unlink_tempfile($fileNameAndPath);
+       }
+
+       /**
+        * @test
+        */
+       public function typoLinkCreatesCorrectJumpUrlForFileWithSpecialUrlEncodedSpecialChars() {
+
+               $testData = $this->initializeJumpUrlTestEnvironment();
+
+               $fileNameAndPath = PATH_site . 'typo3temp/phpunitJumpUrlTestFile with spaces & amps.txt';
+               file_put_contents($fileNameAndPath, 'Some test data');
+               $relativeFileNameAndPath = substr($fileNameAndPath, strlen(PATH_site));
+
+               $testFileLink = $relativeFileNameAndPath;
+               $expectedHash = '691dbf63a21181e2d69bf78e61f1c9fd023aef2c';
+               $expectedUrl = $testData['absRefPrefix'] . $testData['mainScript'] . '?id=' . $testData['pageId'] . '&type=' . $testData['pageType'] . '&jumpurl=' . rawurlencode(str_replace('%2F', '/', rawurlencode($testFileLink))) . '&juHash=' . $expectedHash;
+               $generatedUrl = $this->subject->typoLink_URL(array('parameter' => str_replace('%2F', '/', rawurlencode($testFileLink))));
+
+               $this->assertEquals($expectedUrl, $generatedUrl);
+
+               GeneralUtility::unlink_tempfile($fileNameAndPath);
+       }
+
+       /**
+        * @test
+        */
+       public function typoLinkCreatesCorrectJumpUrlForFileWithUrlEncodedSpecialChars() {
+
+               $testData = $this->initializeJumpUrlTestEnvironment();
+
+               $fileNameAndPath = PATH_site . 'typo3temp/phpunitJumpUrlTestFile with spaces & amps.txt';
+               file_put_contents($fileNameAndPath, 'Some test data');
+               $relativeFileNameAndPath = substr($fileNameAndPath, strlen(PATH_site));
+
+               $testFileLink = $relativeFileNameAndPath;
+               $expectedHash = '691dbf63a21181e2d69bf78e61f1c9fd023aef2c';
+               $expectedUrl = $testData['absRefPrefix'] . $testData['mainScript'] . '?id=' . $testData['pageId'] . '&type=' . $testData['pageType'] . '&jumpurl=' . rawurlencode(str_replace('%2F', '/', rawurlencode($testFileLink))) . '&juHash=' . $expectedHash;
+               $generatedUrl = $this->subject->typoLink_URL(array('parameter' => rawurlencode($testFileLink)));
+
+               $this->assertEquals($expectedUrl, $generatedUrl);
+
+               GeneralUtility::unlink_tempfile($fileNameAndPath);
+       }
+
+       /**
+        * @test
+        */
+       public function typoLinkCreatesCorrectJumpUrlForMail() {
+
+               $testData = $this->initializeJumpUrlTestEnvironment();
+
+               $testAddress = 'mail@ddress.tld';
+               $expectedHash = 'bd82328dc40755f5d0411e2e16e7c0cbf33b51b7';
+               $expectedUrl = $testData['absRefPrefix'] . $testData['mainScript'] . '?id=' . $testData['pageId'] . '&type=' . $testData['pageType'] . '&jumpurl=' . rawurlencode('mailto:' . $testAddress) . '&juHash=' . $expectedHash;
+               $generatedUrl = $this->subject->typoLink_URL(array('parameter' => $testAddress));
+
+               $this->assertEquals($expectedUrl, $generatedUrl);
+       }
+
+       /**
+        * @test
+        */
+       public function typoLinkCreatesCorrectSecureJumpUrlForFile() {
+
+               $testData = $this->initializeJumpUrlTestEnvironment();
+
+               $fileNameAndPath = PATH_site . 'typo3temp/phpunitJumpUrlTestFile.txt';
+               file_put_contents($fileNameAndPath, 'Some test data');
+               $relativeFileNameAndPath = substr($fileNameAndPath, strlen(PATH_site));
+
+               $testAddress = $relativeFileNameAndPath;
+               $expectedHash = '1933f3c181db8940acfcd4d16c74643947179948';
+               $expectedUrl = $testData['absRefPrefix'] . $testData['mainScript'] . '?id=' . $testData['pageId'] . '&type=' . $testData['pageType'] . '&jumpurl=' . rawurlencode($testAddress) . '&juSecure=1&locationData=' . rawurlencode($testData['locationData']) . '&juHash=' . $expectedHash;
+               $generatedUrl = $this->subject->typoLink_URL(array('parameter' => $testAddress, 'jumpurl.' => array('secure' => 1)));
+
+               $this->assertEquals($expectedUrl, $generatedUrl);
+
+               GeneralUtility::unlink_tempfile($fileNameAndPath);
+       }
+
+       /**
+        * Avoid logging to the file system (file writer is currently the only configured writer)
+        */
+       protected function createMockedLoggerAndLogManager() {
+               /** @var \TYPO3\CMS\Core\SingletonInterface $logManagerMock */
+               $logManagerMock = $this->getMock(LogManager::class);
+               $loggerMock = $this->getMock(LoggerInterface::class);
+               $logManagerMock->expects($this->any())
+                       ->method('getLogger')
+                       ->willReturn($loggerMock);
+               GeneralUtility::setSingletonInstance(LogManager::class, $logManagerMock);
+       }
+
+       /**
+        * Initializes all required settings in $GLOBALS['TSFE'] and the current
+        * content object renderer for testing jump URL functionality.
+        *
+        * @param \PHPUnit_Framework_MockObject_Matcher_InvokedCount $expectedGetPageCalls
+        * @return array
+        */
+       protected function initializeJumpUrlTestEnvironment($expectedGetPageCalls = NULL) {
+
+               if (!isset($expectedGetPageCalls)) {
+                       $expectedGetPageCalls = $this->once();
+               }
+
+               $testData = array();
+
+               $this->typoScriptFrontendControllerMock->config['config']['jumpurl_enable'] = TRUE;
+
+               $testData['pageId'] = $this->typoScriptFrontendControllerMock->id = '1234';
+               $testData['pageType'] = $this->typoScriptFrontendControllerMock->type = '4';
+               $testData['mainScript'] = $this->typoScriptFrontendControllerMock->config['mainScript'] = 'index.php';
+               $testData['absRefPrefix'] = $this->typoScriptFrontendControllerMock->absRefPrefix = '/prefix/';
+               $this->subject->currentRecord = 'tt_content:999';
+               $testData['locationData'] = $testData['pageId'] . ':' . $this->subject->currentRecord;
+
+               /** @var \PHPUnit_Framework_MockObject_MockObject|\TYPO3\CMS\Frontend\Page\PageRepository $pageRepositoryMock */
+               $pageRepositoryMock = $this->typoScriptFrontendControllerMock->sys_page;
+               $pageRepositoryMock->expects($expectedGetPageCalls)
+                       ->method('getPage')
+                       ->will($this->returnValue(
+                               array(
+                                       'uid' => $testData['pageId'],
+                                       'doktype' => \TYPO3\CMS\Frontend\Page\PageRepository::DOKTYPE_DEFAULT,
+                                       'url_scheme' => 0,
+                                       'title' => 'testpage',
+                               )
+                       )
+                       );
+
+               return $testData;
+       }
+}
diff --git a/typo3/sysext/jumpurl/Tests/Unit/JumpUrlHandlerTest.php b/typo3/sysext/jumpurl/Tests/Unit/JumpUrlHandlerTest.php
new file mode 100644 (file)
index 0000000..7b0d2aa
--- /dev/null
@@ -0,0 +1,353 @@
+<?php
+namespace TYPO3\CMS\Jumpurl\Tests\Unit;
+
+/*
+ * 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\Resource\Exception\FileDoesNotExistException;
+use TYPO3\CMS\Core\Resource\File;
+use TYPO3\CMS\Core\Resource\ResourceFactory;
+use TYPO3\CMS\Core\Utility\HttpUtility;
+use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;
+use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
+use TYPO3\CMS\Jumpurl\JumpUrlHandler;
+
+/**
+ * Testcase for handling jump URLs when given with a test parameter
+ */
+class JumpUrlHandlerTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
+
+       /**
+        * The default location data used for JumpUrl secure.
+        *
+        * @var string
+        */
+       protected $defaultLocationData = '1234:tt_content:999';
+
+       /**
+        * @var \PHPUnit_Framework_MockObject_MockObject|JumpUrlHandler
+        */
+       protected $jumpUrlHandler;
+
+       /**
+        * @var \PHPUnit_Framework_MockObject_MockObject|\TYPO3\CMS\Core\Tests\AccessibleObjectInterface|\TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController
+        */
+       protected $tsfe;
+
+       /**
+        * Sets environment variables and initializes global mock object.
+        */
+       public function setUp() {
+               parent::setUp();
+
+               $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] = '12345';
+
+               $this->jumpUrlHandler = $this->getMock(
+                       JumpUrlHandler::class,
+                       array('isLocationDataValid', 'getResourceFactory', 'getTypoScriptFrontendController', 'readFileAndExit', 'redirect')
+               );
+
+               $this->tsfe = $this->getAccessibleMock(
+                       TypoScriptFrontendController::class,
+                       array('getPagesTSconfig'),
+                       array(),
+                       '',
+                       FALSE
+               );
+               $this->jumpUrlHandler->expects($this->any())
+                       ->method('getTypoScriptFrontendController')
+                       ->will($this->returnValue($this->tsfe));
+       }
+
+       /**
+        * Provides a valid jump URL hash and a target URL
+        *
+        * @return array
+        */
+       public function jumpUrlDefaultValidParametersDataProvider() {
+               return array(
+                       'File with spaces and ampersands' => array(
+                               '691dbf63a21181e2d69bf78e61f1c9fd023aef2c',
+                               str_replace('%2F', '/', rawurlencode('typo3temp/phpunitJumpUrlTestFile with spaces & amps.txt')),
+                       ),
+                       'External URL' => array(
+                               '7d2261b12682a4b73402ae67415e09f294b29a55',
+                               'http://www.mytesturl.tld',
+                       ),
+                       'External URL with GET parameters' => array(
+                               'cfc95f583da7689238e98bbc8930ebd820f0d20f',
+                               'http://external.domain.tld?parameter1=' . rawurlencode('parameter[data]with&a lot-of-special/chars'),
+                       ),
+                       'External URL without www' => array(
+                               '8591c573601d17f37e06aff4ac14c78f107dd49e',
+                               'http://external.domain.tld',
+                       ),
+                       'Mailto link' => array(
+                               'bd82328dc40755f5d0411e2e16e7c0cbf33b51b7',
+                               'mailto:mail@ddress.tld',
+                       )
+               );
+       }
+
+       /**
+        * @test
+        * @dataProvider jumpUrlDefaultValidParametersDataProvider
+        * @param string $hash
+        * @param string $jumpUrl
+        */
+       public function jumpUrlDefaultAcceptsValidUrls($hash, $jumpUrl) {
+
+               $_GET['juHash'] = $hash;
+               $_GET['jumpurl'] = $jumpUrl;
+
+               $this->jumpUrlHandler->expects($this->once())
+                       ->method('redirect')
+                       ->with($jumpUrl, HttpUtility::HTTP_STATUS_303);
+
+               $this->jumpUrlHandler->canHandleCurrentUrl();
+               $this->jumpUrlHandler->handle();
+       }
+
+       /**
+        * @test
+        * @dataProvider jumpUrlDefaultValidParametersDataProvider
+        * @expectedException \Exception
+        * @expectedExceptionCode 1359987599
+        * @param string $hash
+        * @param string $jumpUrl
+        */
+       public function jumpUrlDefaultFailsOnInvalidHash($hash, $jumpUrl) {
+
+               $_GET['jumpurl'] = $jumpUrl;
+               $_GET['juHash'] = $hash . '1';
+
+               $this->jumpUrlHandler->canHandleCurrentUrl();
+               $this->jumpUrlHandler->handle();
+       }
+
+       /**
+        * @test
+        * @dataProvider jumpUrlDefaultValidParametersDataProvider
+        * @param string $hash
+        * @param string $jumpUrl
+        */
+       public function jumpUrlDefaultTransfersSession($hash, $jumpUrl) {
+
+               $tsConfig['TSFE.']['jumpUrl_transferSession'] = 1;
+
+               /** @var \PHPUnit_Framework_MockObject_MockObject|FrontendUserAuthentication $frontendUserMock */
+               $frontendUserMock = $this->getMock(FrontendUserAuthentication::class);
+               $frontendUserMock->id = 123;
+
+               $this->tsfe->_set('fe_user', $frontendUserMock);
+               $this->tsfe->expects($this->once())
+                       ->method('getPagesTSconfig')
+                       ->will($this->returnValue($tsConfig));
+
+               $sessionGetParameter = (strpos($jumpUrl, '?') === FALSE ? '?' : '') . '&FE_SESSION_KEY=123-fc9f825a9af59169895f3bb28267a42f';
+               $expectedJumpUrl = $jumpUrl . $sessionGetParameter;
+
+               $this->jumpUrlHandler->expects($this->once())
+                       ->method('redirect')
+                       ->with($expectedJumpUrl, HttpUtility::HTTP_STATUS_303);
+
+               $_GET['jumpurl'] = $jumpUrl;
+               $_GET['juHash'] = $hash;
+
+               $this->jumpUrlHandler->canHandleCurrentUrl();
+               $this->jumpUrlHandler->handle();
+       }
+
+       /**
+        * Provides a valid jump secure URL hash, a file path and related
+        * record data
+        *
+        * @return array
+        */
+       public function jumpUrlSecureValidParametersDataProvider() {
+               return array(
+                       array(
+                               '1933f3c181db8940acfcd4d16c74643947179948',
+                               'typo3temp/phpunitJumpUrlTestFile.txt',
+                       ),
+                       array(
+                               '304b8c8e022e92e6f4d34e97395da77705830818',
+                               str_replace('%2F', '/', rawurlencode('typo3temp/phpunitJumpUrlTestFile with spaces & amps.txt')),
+                       ),
+                       array(
+                               '304b8c8e022e92e6f4d34e97395da77705830818',
+                               str_replace('%2F', '/', rawurlencode('typo3temp/phpunitJumpUrlTestFile with spaces & amps.txt')),
+                       )
+               );
+       }
+
+       /**
+        * @test
+        * @dataProvider jumpUrlSecureValidParametersDataProvider
+        * @param string $hash
+        * @param string $jumpUrl
+        */
+       public function jumpUrlSecureAcceptsValidUrls($hash, $jumpUrl) {
+
+               $_GET['jumpurl'] = $jumpUrl;
+               $this->prepareJumpUrlSecureTest($hash);
+
+               $fileMock = $this->getMock(File::class, array('dummy'), array(), '', FALSE);
+               $resourceFactoryMock = $this->getMock(ResourceFactory::class, array('retrieveFileOrFolderObject'));
+
+               $resourceFactoryMock->expects($this->once())
+                       ->method('retrieveFileOrFolderObject')
+                       ->will($this->returnValue($fileMock));
+
+               $this->jumpUrlHandler->expects($this->once())
+                       ->method('isLocationDataValid')
+                       ->with($this->defaultLocationData)
+                       ->will($this->returnValue(TRUE));
+
+               $this->jumpUrlHandler->expects($this->once())
+                       ->method('getResourceFactory')
+                       ->will($this->returnValue($resourceFactoryMock));
+
+               $this->jumpUrlHandler->expects($this->once())
+                       ->method('readFileAndExit')
+                       ->with($fileMock);
+
+               $this->jumpUrlHandler->canHandleCurrentUrl();
+               $this->jumpUrlHandler->handle();
+       }
+
+       /**
+        * @test
+        * @dataProvider jumpUrlSecureValidParametersDataProvider
+        * @expectedException \Exception
+        * @expectedExceptionCode 1294585193
+        * @param string $hash
+        * @param string $jumpUrl
+        */
+       public function jumpUrlSecureFailsIfFileDoesNotExist($hash, $jumpUrl) {
+
+               $_GET['jumpurl'] = $jumpUrl;
+               $this->prepareJumpUrlSecureTest($hash);
+
+               $resourceFactoryMock = $this->getMock(ResourceFactory::class, array('retrieveFileOrFolderObject'));
+               $resourceFactoryMock->expects($this->once())
+                       ->method('retrieveFileOrFolderObject')
+                       ->will($this->throwException(new FileDoesNotExistException()));
+
+               $this->jumpUrlHandler->expects($this->once())
+                       ->method('isLocationDataValid')
+                       ->with($this->defaultLocationData)
+                       ->will($this->returnValue(TRUE));
+
+               $this->jumpUrlHandler->expects($this->once())
+                       ->method('getResourceFactory')
+                       ->will($this->returnValue($resourceFactoryMock));
+
+               $this->jumpUrlHandler->canHandleCurrentUrl();
+               $this->jumpUrlHandler->handle();
+       }
+
+       /**
+        * @test
+        * @dataProvider jumpUrlSecureValidParametersDataProvider
+        * @expectedException \Exception
+        * @expectedExceptionCode 1294585195
+        * @param string $hash
+        * @param string $jumpUrl
+        */
+       public function jumpUrlSecureFailsOnDeniedAccess($hash, $jumpUrl) {
+
+               $_GET['jumpurl'] = $jumpUrl;
+               $this->prepareJumpUrlSecureTest($hash);
+
+               $this->jumpUrlHandler->expects($this->once())
+                       ->method('isLocationDataValid')
+                       ->with($this->defaultLocationData)
+                       ->will($this->returnValue(FALSE));
+
+               $this->jumpUrlHandler->canHandleCurrentUrl();
+               $this->jumpUrlHandler->handle();
+       }
+
+       /**
+        * @test
+        * @dataProvider jumpUrlSecureValidParametersDataProvider
+        * @expectedException \Exception
+        * @expectedExceptionCode 1294585196
+        * @param string $hash
+        * @param string $jumpUrl
+        */
+       public function jumpUrlSecureFailsOnInvalidHash($hash, $jumpUrl
+       ) {
+               $_GET['juSecure'] = '1';
+               $_GET['juHash'] = $hash . '1';
+               $_GET['locationData'] = $this->defaultLocationData;
+
+               $this->jumpUrlHandler->canHandleCurrentUrl();
+               $this->jumpUrlHandler->handle();
+       }
+
+       /**
+        * @return array
+        */
+       public function jumpUrlSecureFailsOnForbiddenFileLocationDataProvider() {
+               return array(
+                       'totally forbidden' => array(
+                               '/a/totally/forbidden/path'
+                       ),
+                       'typo3conf file' => array(
+                               PATH_site . '/typo3conf/path'
+                       ),
+                       'file with forbidden character' => array(
+                               PATH_site . '/mypath/test.php'
+                       )
+               );
+       }
+
+       /**
+        * @test
+        * @dataProvider jumpUrlSecureFailsOnForbiddenFileLocationDataProvider
+        * @expectedException \Exception
+        * @expectedExceptionCode 1294585194
+        * @param string $path
+        * @param string $path
+        */
+       public function jumpUrlSecureFailsOnForbiddenFileLocation($path) {
+
+               $this->jumpUrlHandler->expects($this->once())
+                       ->method('isLocationDataValid')
+                       ->with('')
+                       ->will($this->returnValue(TRUE));
+
+
+               $hash = \TYPO3\CMS\Jumpurl\JumpUrlUtility::calculateHashSecure($path, '', '');
+
+               $_GET['jumpurl'] = $path;
+               $_GET['juSecure'] = '1';
+               $_GET['juHash'] = $hash;
+               $_GET['locationData'] = '';
+
+               $this->jumpUrlHandler->canHandleCurrentUrl();
+               $this->jumpUrlHandler->handle();
+       }
+
+       /**
+        * @param string $hash
+        * @return void
+        */
+       protected function prepareJumpUrlSecureTest($hash) {
+               $_GET['juSecure'] = '1';
+               $_GET['juHash'] = $hash;
+               $_GET['locationData'] = $this->defaultLocationData;
+       }
+}
\ No newline at end of file
diff --git a/typo3/sysext/jumpurl/Tests/Unit/JumpUrlProcessorMock.php b/typo3/sysext/jumpurl/Tests/Unit/JumpUrlProcessorMock.php
new file mode 100644 (file)
index 0000000..bbc37a7
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+namespace TYPO3\CMS\Jumpurl\Tests\Unit;
+
+/*
+ * 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\Jumpurl\JumpUrlProcessor;
+
+/**
+ * Testcase for handling jump URLs when given with a test parameter
+ */
+class JumpUrlProcessorMock extends JumpUrlProcessor {
+
+       /**
+        * Makes the parent getParametersForSecureFile() method accessible.
+        *
+        * @param string $jumpUrl
+        * @param array $configuration
+        * @return array
+        */
+       public function getParametersForSecureFile($jumpUrl, array $configuration) {
+               return parent::getParametersForSecureFile($jumpUrl, $configuration);
+       }
+}
\ No newline at end of file
diff --git a/typo3/sysext/jumpurl/Tests/Unit/JumpUrlProcessorTest.php b/typo3/sysext/jumpurl/Tests/Unit/JumpUrlProcessorTest.php
new file mode 100644 (file)
index 0000000..1e43ef3
--- /dev/null
@@ -0,0 +1,101 @@
+<?php
+namespace TYPO3\CMS\Jumpurl\Tests\Unit;
+
+/*
+ * 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\Frontend\ContentObject\ContentObjectRenderer;
+use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
+
+/**
+ * Testcase for handling jump URLs when given with a test parameter
+ */
+class JumpUrlProcessorTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
+
+       /**
+        * @var \PHPUnit_Framework_MockObject_MockObject|ContentObjectRenderer
+        */
+       protected $contentObjectRenderer;
+
+       /**
+        * The default location data used for JumpUrl secure.
+        *
+        * @var string
+        */
+       protected $defaultLocationData = '1234:tt_content:999';
+
+       /**
+        * @var \PHPUnit_Framework_MockObject_MockObject|JumpUrlProcessorMock
+        */
+       protected $jumpUrlProcessor;
+
+       /**
+        * @var \PHPUnit_Framework_MockObject_MockObject|\TYPO3\CMS\Core\Tests\AccessibleObjectInterface|\TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController
+        */
+       protected $tsfe;
+
+       /**
+        * Sets environment variables and initializes global mock object.
+        */
+       public function setUp() {
+
+               parent::setUp();
+
+               $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] = '12345';
+
+               $this->jumpUrlProcessor = $this->getMock(
+                       JumpUrlProcessorMock::class,
+                       array('getTypoScriptFrontendController', 'getContentObjectRenderer')
+               );
+
+               $this->tsfe = $this->getAccessibleMock(
+                       TypoScriptFrontendController::class,
+                       array('getPagesTSconfig'),
+                       array(),
+                       '',
+                       FALSE
+               );
+               $this->jumpUrlProcessor->expects($this->any())
+                       ->method('getTypoScriptFrontendController')
+                       ->will($this->returnValue($this->tsfe));
+
+               $this->contentObjectRenderer = $this->getMock(ContentObjectRenderer::class);
+               $this->jumpUrlProcessor->expects($this->any())
+                       ->method('getContentObjectRenderer')
+                       ->will($this->returnValue($this->contentObjectRenderer));
+       }
+
+       /**
+        * @test
+        */
+       public function getJumpUrlSecureParametersReturnsValidParameters() {
+
+               $this->tsfe->id = 456;
+               $this->contentObjectRenderer->currentRecord = 'tt_content:123';
+
+               $jumpUrlSecureParameters = $this->jumpUrlProcessor->getParametersForSecureFile(
+                       '/fileadmin/a/test/file.txt',
+                       array('mimeTypes' => 'dummy=application/x-executable,txt=text/plain')
+               );
+
+               $this->assertSame(
+                       array(
+                               'juSecure' => 1,
+                               'locationData' => '456:tt_content:123',
+                               'mimeType' => 'text/plain',
+                               'juHash' => '1cccb7f01c8a3f58ee890377b5de9bdc05115a37',
+                       ),
+                       $jumpUrlSecureParameters
+               );
+       }
+}
\ No newline at end of file
diff --git a/typo3/sysext/jumpurl/composer.json b/typo3/sysext/jumpurl/composer.json
new file mode 100644 (file)
index 0000000..cbc630f
--- /dev/null
@@ -0,0 +1,24 @@
+{
+       "name": "typo3/cms-jumpurl",
+       "type": "typo3-cms-extension",
+       "description": "TYPO3 Jump URL handling",
+       "homepage": "http://typo3.org",
+       "license": ["GPL-2.0+"],
+
+       "require": {
+               "typo3/cms-core": "*"
+       },
+       "replace": {
+               "jumpurl": "*"
+       },
+       "autoload": {
+               "psr-4": {
+                       "TYPO3\\CMS\\Jumpurl\\": "Classes/"
+               }
+       },
+       "autoload-dev": {
+               "psr-4": {
+                       "TYPO3\\CMS\\Jumpurl\\Tests\\": "Tests/"
+               }
+       }
+}
diff --git a/typo3/sysext/jumpurl/ext_emconf.php b/typo3/sysext/jumpurl/ext_emconf.php
new file mode 100644 (file)
index 0000000..b6eadcb
--- /dev/null
@@ -0,0 +1,21 @@
+<?php
+$EM_CONF[$_EXTKEY] = array(
+       'title' => 'JumpURL',
+       'description' => 'Allows to modify links to create Jump URLs created in the frontend of the TYPO3 Core.',
+       'category' => 'fe',
+       'author' => 'TYPO3 CMS Team',
+       'author_email' => 'info@typo3.org',
+       'state' => 'stable',
+       'uploadfolder' => 0,
+       'createDirs' => '',
+       'clearCacheOnLoad' => 1,
+       'author_company' => '',
+       'version' => '7.5.0',
+       'constraints' => array(
+               'depends' => array(
+                       'typo3' => '7.5.0-7.9.99',
+               ),
+               'conflicts' => array(),
+               'suggests' => array(),
+       ),
+);
diff --git a/typo3/sysext/jumpurl/ext_icon.png b/typo3/sysext/jumpurl/ext_icon.png
new file mode 100644 (file)
index 0000000..ef81ce2
Binary files /dev/null and b/typo3/sysext/jumpurl/ext_icon.png differ
diff --git a/typo3/sysext/jumpurl/ext_localconf.php b/typo3/sysext/jumpurl/ext_localconf.php
new file mode 100644 (file)
index 0000000..27285bc
--- /dev/null
@@ -0,0 +1,11 @@
+<?php
+defined('TYPO3_MODE') or die();
+
+// Register hooks
+$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['urlProcessing']['urlProcessors']['jumpurl']['processor'] = \TYPO3\CMS\Jumpurl\JumpUrlProcessor::class;
+$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['urlProcessing']['urlHandlers']['jumpurl'] = [
+       'handler' => \TYPO3\CMS\Jumpurl\JumpUrlHandler::class,
+       'before' => [
+               'frontendExternalUrl'
+       ],
+];