TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\LinkHandling;
|
||||
|
||||
/**
|
||||
* Resolves emails
|
||||
*/
|
||||
class EmailLinkHandler implements LinkHandlingInterface
|
||||
{
|
||||
/**
|
||||
* Returns the link to an email as a string
|
||||
*/
|
||||
public function asString(array $parameters): string
|
||||
{
|
||||
$queryParameters = [];
|
||||
foreach (['subject', 'cc', 'bcc', 'body'] as $additionalInfo) {
|
||||
if (isset($parameters[$additionalInfo])) {
|
||||
$queryParameters[$additionalInfo] = rawurldecode(trim($parameters[$additionalInfo]));
|
||||
}
|
||||
}
|
||||
$result = 'mailto:' . trim($parameters['email']);
|
||||
if ($queryParameters !== []) {
|
||||
// We need to percent-encode additional parameters (RFC 3986)
|
||||
$result .= '?' . http_build_query($queryParameters, '', '&', PHP_QUERY_RFC3986);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the email address without the "mailto:" prefix
|
||||
* in the 'email' property of the array.
|
||||
*/
|
||||
public function resolveHandlerData(array $data): array
|
||||
{
|
||||
$linkParts = parse_url($data['email'] ?? '');
|
||||
$data['email'] = trim($linkParts['path'] ?? '');
|
||||
if (isset($linkParts['query'])) {
|
||||
$result = [];
|
||||
parse_str($linkParts['query'], $result);
|
||||
foreach (['subject', 'cc', 'bcc', 'body'] as $additionalInfo) {
|
||||
if (isset($result[$additionalInfo])) {
|
||||
$data[$additionalInfo] = trim($result[$additionalInfo]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\LinkHandling\Event;
|
||||
|
||||
use TYPO3\CMS\Core\Exception;
|
||||
|
||||
/**
|
||||
* Listeners are able to modify the resolved link result data
|
||||
*/
|
||||
final class AfterLinkResolvedByStringRepresentationEvent
|
||||
{
|
||||
public function __construct(
|
||||
private array $result,
|
||||
private readonly string $urn,
|
||||
private readonly ?Exception $resolveException
|
||||
) {}
|
||||
|
||||
public function getResult(): array
|
||||
{
|
||||
return $this->result;
|
||||
}
|
||||
|
||||
public function setResult(array $result): void
|
||||
{
|
||||
$this->result = $result;
|
||||
}
|
||||
|
||||
public function getUrn(): string
|
||||
{
|
||||
return $this->urn;
|
||||
}
|
||||
|
||||
public function getResolveException(): ?Exception
|
||||
{
|
||||
return $this->resolveException;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\LinkHandling\Event;
|
||||
|
||||
/**
|
||||
* Listeners are able to modify the decoded link parts of a TypoLink
|
||||
*/
|
||||
final class AfterTypoLinkDecodedEvent
|
||||
{
|
||||
public function __construct(
|
||||
private array $typoLinkParts,
|
||||
private readonly string $typoLink,
|
||||
private readonly string $delimiter,
|
||||
private readonly string $emptyValueSymbol
|
||||
) {}
|
||||
|
||||
public function getTypoLinkParts(): array
|
||||
{
|
||||
return $this->typoLinkParts;
|
||||
}
|
||||
|
||||
public function setTypoLinkParts(array $typoLinkParts): void
|
||||
{
|
||||
$this->typoLinkParts = $typoLinkParts;
|
||||
}
|
||||
|
||||
public function getTypoLink(): string
|
||||
{
|
||||
return $this->typoLink;
|
||||
}
|
||||
|
||||
public function getDelimiter(): string
|
||||
{
|
||||
return $this->delimiter;
|
||||
}
|
||||
|
||||
public function getEmptyValueSymbol(): string
|
||||
{
|
||||
return $this->emptyValueSymbol;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\LinkHandling\Event;
|
||||
|
||||
/**
|
||||
* Listeners are able to modify the to be encoded TypoLink parameters
|
||||
*/
|
||||
final class BeforeTypoLinkEncodedEvent
|
||||
{
|
||||
public function __construct(
|
||||
private array $parameters,
|
||||
private readonly array $typoLinkParts,
|
||||
private readonly string $delimiter,
|
||||
private readonly string $emptyValueSymbol
|
||||
) {}
|
||||
|
||||
public function getParameters(): array
|
||||
{
|
||||
return $this->parameters;
|
||||
}
|
||||
|
||||
public function setParameters(array $parameters): void
|
||||
{
|
||||
$this->parameters = $parameters;
|
||||
}
|
||||
|
||||
public function getTypoLinkParts(): array
|
||||
{
|
||||
return $this->typoLinkParts;
|
||||
}
|
||||
|
||||
public function getDelimiter(): string
|
||||
{
|
||||
return $this->delimiter;
|
||||
}
|
||||
|
||||
public function getEmptyValueSymbol(): string
|
||||
{
|
||||
return $this->emptyValueSymbol;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\LinkHandling\Exception;
|
||||
|
||||
use TYPO3\CMS\Core\Exception;
|
||||
|
||||
/**
|
||||
* Exception raised if no matching link handler is found.
|
||||
*/
|
||||
class UnknownLinkHandlerException extends Exception {}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\LinkHandling\Exception;
|
||||
|
||||
use TYPO3\CMS\Core\Exception;
|
||||
|
||||
/**
|
||||
* Exception raised if urn is not known.
|
||||
*/
|
||||
class UnknownUrnException extends Exception {}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\LinkHandling;
|
||||
|
||||
use TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException;
|
||||
use TYPO3\CMS\Core\Resource\FileInterface;
|
||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||
use TYPO3\CMS\Core\Resource\Security\FileNameValidator;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Interface for classes which are transforming a tag link hrefs for folders, in order to
|
||||
* use FAL to store them in database, which means that files can be moved in the fileadmin
|
||||
* without breaking file links in the frontend/backend
|
||||
*/
|
||||
class FileLinkHandler implements LinkHandlingInterface
|
||||
{
|
||||
/**
|
||||
* The Base URN
|
||||
*/
|
||||
protected string $baseUrn = 't3://file';
|
||||
|
||||
/**
|
||||
* The resource factory object to resolve file objects
|
||||
*/
|
||||
protected ResourceFactory $resourceFactory;
|
||||
|
||||
/**
|
||||
* Returns the link to a file as a string
|
||||
*/
|
||||
public function asString(array $parameters): string
|
||||
{
|
||||
if ($parameters['file'] === null) {
|
||||
return '';
|
||||
}
|
||||
$uid = $parameters['file']->getUid();
|
||||
// I am not sure about this use case. Maybe if the file was not indexed and saved to DB (migration from old systems)
|
||||
if ($uid > 0) {
|
||||
$urn = '?uid=' . $uid;
|
||||
} else {
|
||||
$identifier = $parameters['file']->getIdentifier();
|
||||
$urn = '?identifier=' . urlencode($identifier);
|
||||
}
|
||||
if (!empty($parameters['fragment'])) {
|
||||
$urn .= '#' . $parameters['fragment'];
|
||||
}
|
||||
return $this->baseUrn . $urn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a file object inside the array data from the string
|
||||
*
|
||||
* @param array $data with the "file" property containing a File object
|
||||
* @throws \TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException
|
||||
*/
|
||||
public function resolveHandlerData(array $data): array
|
||||
{
|
||||
try {
|
||||
$file = $this->resolveFile($data);
|
||||
$fileNameValidator = GeneralUtility::makeInstance(FileNameValidator::class);
|
||||
if ($file !== null && (!$fileNameValidator->isValid(basename($file->getIdentifier()))
|
||||
|| !$fileNameValidator->isValid($file->getName()))
|
||||
) {
|
||||
$file = null;
|
||||
}
|
||||
} catch (FileDoesNotExistException $e) {
|
||||
$file = null;
|
||||
}
|
||||
$result = ['file' => $file];
|
||||
if (!empty($data['fragment'])) {
|
||||
$result['fragment'] = $data['fragment'];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FileDoesNotExistException
|
||||
*/
|
||||
protected function resolveFile(array $data): ?FileInterface
|
||||
{
|
||||
if (is_numeric($data['uid'] ?? false)) {
|
||||
return $this->getResourceFactory()->getFileObject($data['uid']);
|
||||
}
|
||||
if (is_string($data['identifier'] ?? false) && $data['identifier'] !== '') {
|
||||
return $this->getResourceFactory()->getFileObjectFromCombinedIdentifier($data['identifier']);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the resource factory (only once)
|
||||
*/
|
||||
protected function getResourceFactory(): ResourceFactory
|
||||
{
|
||||
return $this->resourceFactory ??= GeneralUtility::makeInstance(ResourceFactory::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\LinkHandling;
|
||||
|
||||
use TYPO3\CMS\Core\Resource\Exception\FolderDoesNotExistException;
|
||||
use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException;
|
||||
use TYPO3\CMS\Core\Resource\Folder;
|
||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Interface for classes which are transforming a tag link hrefs for folders, in order to
|
||||
* use FAL to store them in database, which means that folders can be moved in the fileadmin
|
||||
* without breaking folder links in the frontend/backend
|
||||
*/
|
||||
class FolderLinkHandler implements LinkHandlingInterface
|
||||
{
|
||||
protected string $baseUrn = 't3://folder';
|
||||
|
||||
protected ?ResourceFactory $resourceFactory = null;
|
||||
|
||||
/**
|
||||
* Returns a link notation to a folder
|
||||
*/
|
||||
public function asString(array $parameters): string
|
||||
{
|
||||
if (!($parameters['folder'] ?? null) instanceof Folder) {
|
||||
return '';
|
||||
}
|
||||
// the magic with prepending slash if it is missing will not work on windows
|
||||
return $this->baseUrn . '?storage=' . $parameters['folder']->getStorage()->getUid()
|
||||
. '&identifier=' . urlencode('/' . ltrim($parameters['folder']->getIdentifier(), '/'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a folder object inside the array data from the string
|
||||
*
|
||||
* @param array $data with the "folder" property containing a Folder object
|
||||
*/
|
||||
public function resolveHandlerData(array $data): array
|
||||
{
|
||||
$combinedIdentifier = ($data['storage'] ?? '0') . ':' . $data['identifier'];
|
||||
try {
|
||||
$folder = $this->getResourceFactory()->getFolderObjectFromCombinedIdentifier($combinedIdentifier);
|
||||
} catch (FolderDoesNotExistException|InsufficientFolderAccessPermissionsException $e) {
|
||||
$folder = null;
|
||||
}
|
||||
return ['folder' => $folder];
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the resource factory (only once)
|
||||
*/
|
||||
protected function getResourceFactory(): ResourceFactory
|
||||
{
|
||||
if (!$this->resourceFactory) {
|
||||
$this->resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
|
||||
}
|
||||
return $this->resourceFactory;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\LinkHandling;
|
||||
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\Resource\Exception as ResourceException;
|
||||
use TYPO3\CMS\Core\Resource\File;
|
||||
use TYPO3\CMS\Core\Resource\Folder;
|
||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||
use TYPO3\CMS\Core\Resource\ResourceInterface;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
|
||||
/**
|
||||
* Class to resolve and convert the "old" link information (email, external url, file, page etc)
|
||||
* to a URL or new format for migration
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class LegacyLinkNotationConverter
|
||||
{
|
||||
protected ?ResourceFactory $resourceFactory = null;
|
||||
|
||||
/**
|
||||
* Part of the typolink construction functionality, called by typoLink()
|
||||
* Used to resolve "legacy"-based typolinks.
|
||||
*
|
||||
* Tries to get the type of the link from the link parameter
|
||||
* could be
|
||||
* - "mailto" an email address
|
||||
* - "url" external URL
|
||||
* - "file" a local file (checked AFTER getPublicUrl() is called)
|
||||
* - "page" a page (integer)
|
||||
*
|
||||
* Does NOT check if the page exists or the file exists.
|
||||
*
|
||||
* @param string $linkParameter could be "fileadmin/myfile.jpg", "info@typo3.org", "13" or "http://www.typo3.org"
|
||||
*/
|
||||
public function resolve(string $linkParameter): array
|
||||
{
|
||||
$a = [];
|
||||
if (stripos(rawurldecode(trim($linkParameter)), 'phar://') === 0) {
|
||||
throw new \RuntimeException(
|
||||
'phar scheme not allowed as soft reference target',
|
||||
1530030673
|
||||
);
|
||||
}
|
||||
|
||||
$result = [];
|
||||
// @todo If resolved result (linkDetails) are later used to build an uri using LinkBuilder->build(), it's needed
|
||||
// to have the original $linkParameter in the result array. Otherwise, places may break like e.g. the
|
||||
// DatabaseRecordLinkBuilder. Can we safely set this here directly and avoiding calls before build like
|
||||
// "$linkDetails['typoLinkParameter'] = $redirectTarget;" - e.g. like in the ext:redirects
|
||||
// TYPO3\CMS\Redirects\Service\RedirectService::resolveLinkDetailsFromLinkTarget() and other places.
|
||||
|
||||
// Resolve FAL-api "file:UID-of-sys_file-record" and "file:combined-identifier"
|
||||
if (stripos($linkParameter, 'file:') === 0) {
|
||||
$result = $this->getFileOrFolderObjectFromMixedIdentifier(substr($linkParameter, 5));
|
||||
} elseif (GeneralUtility::validEmail((string)parse_url($linkParameter, PHP_URL_PATH))) {
|
||||
$result['type'] = LinkService::TYPE_EMAIL;
|
||||
$result['email'] = $linkParameter;
|
||||
} elseif (str_starts_with($linkParameter, 'tel:')) {
|
||||
$result['type'] = LinkService::TYPE_TELEPHONE;
|
||||
$result['telephone'] = $linkParameter;
|
||||
} elseif (str_contains($linkParameter, ':')) {
|
||||
// Check for link-handler keyword
|
||||
[$linkHandlerKeyword, $linkHandlerValue] = explode(':', $linkParameter, 2);
|
||||
$result['type'] = strtolower(trim($linkHandlerKeyword));
|
||||
if ($linkHandlerValue === '') {
|
||||
return [
|
||||
'type' => LinkService::TYPE_UNKNOWN,
|
||||
'url' => $linkParameter,
|
||||
];
|
||||
}
|
||||
$result['url'] = $linkParameter;
|
||||
$result['value'] = $linkHandlerValue;
|
||||
if ($result['type'] === LinkService::TYPE_RECORD) {
|
||||
[$a['identifier'], $tableAndUid] = explode(':', $linkHandlerValue, 2);
|
||||
$tableAndUid = explode(':', $tableAndUid);
|
||||
if (count($tableAndUid) > 1) {
|
||||
$a['table'] = $tableAndUid[0];
|
||||
$a['uid'] = (int)$tableAndUid[1];
|
||||
} else {
|
||||
// this case can happen if there is the very old linkhandler syntax, which was only record:<table>:<uid>
|
||||
$a['table'] = $a['identifier'];
|
||||
$a['uid'] = (int)$tableAndUid[0];
|
||||
}
|
||||
$result = array_merge($result, $a);
|
||||
}
|
||||
} else {
|
||||
// special handling without a scheme
|
||||
$isLocalFile = 0;
|
||||
$fileChar = (int)strpos($linkParameter, '/');
|
||||
$urlChar = (int)strpos($linkParameter, '.');
|
||||
|
||||
$isIdOrAlias = MathUtility::canBeInterpretedAsInteger($linkParameter);
|
||||
$matches = [];
|
||||
// capture old RTE links relative to TYPO3 Backend /typo3/
|
||||
if (preg_match('#../(?:index\\.php)?\\?id=([^&]+)#', $linkParameter, $matches)) {
|
||||
$linkParameter = $matches[1];
|
||||
$isIdOrAlias = true;
|
||||
}
|
||||
$containsSlash = false;
|
||||
if (!$isIdOrAlias) {
|
||||
// Detects if a file is found in site-root and if so it will be treated like a normal file.
|
||||
[$rootFileDat] = explode('?', rawurldecode($linkParameter));
|
||||
$containsSlash = str_contains($rootFileDat, '/');
|
||||
$pathInfo = pathinfo($rootFileDat);
|
||||
$fileExtension = strtolower($pathInfo['extension'] ?? '');
|
||||
if (!$containsSlash
|
||||
&& trim($rootFileDat)
|
||||
&& (
|
||||
@is_file(Environment::getPublicPath() . '/' . $rootFileDat)
|
||||
|| $fileExtension === 'php'
|
||||
|| $fileExtension === 'html'
|
||||
|| $fileExtension === 'htm'
|
||||
)
|
||||
) {
|
||||
$isLocalFile = 1;
|
||||
} elseif ($containsSlash) {
|
||||
// Adding this so realurl directories are linked right (non-existing).
|
||||
$isLocalFile = 2;
|
||||
}
|
||||
}
|
||||
|
||||
// url (external): If doubleSlash or if a '.' comes before a '/'.
|
||||
if (!$isIdOrAlias && $isLocalFile !== 1 && $urlChar && (!$containsSlash || $urlChar < $fileChar)) {
|
||||
$result['type'] = LinkService::TYPE_URL;
|
||||
$result['url'] = UrlLinkHandler::getDefaultScheme() . '://' . $linkParameter;
|
||||
// file (internal) or folder
|
||||
} elseif ($containsSlash || $isLocalFile) {
|
||||
$result = $this->getFileOrFolderObjectFromMixedIdentifier($linkParameter);
|
||||
} else {
|
||||
// Integer or alias (alias is without slashes or periods or commas, that is
|
||||
// 'nospace,alphanum_x,lower,unique' according to definition in $GLOBALS['TCA']!)
|
||||
$result = $this->resolvePageRelatedParameters($linkParameter);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to do some magic to get a page parts, additional params, fragment / section hash
|
||||
*
|
||||
* @param string $data the input variable, can be "mypage,23" with fragments, keys
|
||||
*
|
||||
* @return array the result array with the page type set
|
||||
*/
|
||||
protected function resolvePageRelatedParameters(string $data): array
|
||||
{
|
||||
$result = ['type' => LinkService::TYPE_PAGE];
|
||||
if (str_contains($data, '#')) {
|
||||
[$data, $result['fragment']] = explode('#', $data, 2);
|
||||
}
|
||||
// check for additional parameters
|
||||
if (str_contains($data, '?')) {
|
||||
[$data, $result['parameters']] = explode('?', $data, 2);
|
||||
} elseif (str_contains($data, '&')) {
|
||||
[$data, $result['parameters']] = explode('&', $data, 2);
|
||||
}
|
||||
$data = rtrim($data, ',');
|
||||
if (empty($data)) {
|
||||
$result['pageuid'] = 'current';
|
||||
} elseif ($data[0] === '#') {
|
||||
$result['pageuid'] = 'current';
|
||||
$result['fragment'] = substr($data, 1);
|
||||
} elseif (str_contains($data, ',')) {
|
||||
[$result['pageuid'], $result['pagetype']] = explode(',', $data, 2);
|
||||
} elseif (str_contains($data, '/')) {
|
||||
$data = explode('/', trim($data, '/'));
|
||||
$result['pageuid'] = array_shift($data);
|
||||
foreach ($data as $k => $item) {
|
||||
if ((int)$data[$k] % 2 === 0 && !empty($data[$k + 1])) {
|
||||
// @todo: revisit. Looks fishy and has no coverage?
|
||||
$result['page' . $data[$k]] = $data[$k + 1];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$result['pageuid'] = $data;
|
||||
}
|
||||
if (MathUtility::canBeInterpretedAsInteger($result['pageuid'])) {
|
||||
$result['pageuid'] = (int)$result['pageuid'];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method that fetches a file or folder object based on the file or folder combined identifier
|
||||
*
|
||||
* @param string $mixedIdentifier can be something like "2" (file uid), "fileadmin/i/like.png" or "2:/myidentifier/"
|
||||
*
|
||||
* @return array the result with the type (file or folder) set
|
||||
*/
|
||||
protected function getFileOrFolderObjectFromMixedIdentifier(string $mixedIdentifier): array
|
||||
{
|
||||
$result = [];
|
||||
try {
|
||||
$fileIdentifier = $mixedIdentifier;
|
||||
$fragment = null;
|
||||
if (str_contains($fileIdentifier, '#')) {
|
||||
[$fileIdentifier, $fragment] = explode('#', $fileIdentifier, 2);
|
||||
}
|
||||
try {
|
||||
$fileOrFolderObject = $this->getResourceFactory()->retrieveFileOrFolderObject($fileIdentifier);
|
||||
} catch (ResourceException $e) {
|
||||
$fileOrFolderObject = null;
|
||||
}
|
||||
// Links to a file/folder in the main TYPO3 directory should not be considered as file links, but an external link
|
||||
if ($fileOrFolderObject instanceof ResourceInterface && $fileOrFolderObject->getStorage()->isFallbackStorage()) {
|
||||
return [
|
||||
'type' => LinkService::TYPE_URL,
|
||||
'url' => $mixedIdentifier,
|
||||
];
|
||||
}
|
||||
// Link to a folder or file
|
||||
if ($fileOrFolderObject instanceof File) {
|
||||
$result['type'] = LinkService::TYPE_FILE;
|
||||
$result['file'] = $fileOrFolderObject;
|
||||
if ($fragment) {
|
||||
$result['fragment'] = $fragment;
|
||||
}
|
||||
} elseif ($fileOrFolderObject instanceof Folder) {
|
||||
$result['type'] = LinkService::TYPE_FOLDER;
|
||||
$result['folder'] = $fileOrFolderObject;
|
||||
if ($fragment) {
|
||||
$result['fragment'] = $fragment;
|
||||
}
|
||||
} elseif (str_starts_with($mixedIdentifier, '/')) {
|
||||
$result['type'] = LinkService::TYPE_URL;
|
||||
$result['url'] = $mixedIdentifier;
|
||||
} else {
|
||||
$result['type'] = LinkService::TYPE_UNKNOWN;
|
||||
$result['file'] = $mixedIdentifier;
|
||||
}
|
||||
} catch (\RuntimeException $e) {
|
||||
// Element wasn't found
|
||||
$result['type'] = LinkService::TYPE_UNKNOWN;
|
||||
$result['file'] = $mixedIdentifier;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the resource factory (only once)
|
||||
*/
|
||||
protected function getResourceFactory(): ResourceFactory
|
||||
{
|
||||
if (!$this->resourceFactory) {
|
||||
$this->resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
|
||||
}
|
||||
return $this->resourceFactory;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\LinkHandling;
|
||||
|
||||
/**
|
||||
* Interface for classes which are transforming a tag link hrefs to records or resources
|
||||
* basically any URLs that should not be saved directly in the database on as is basis
|
||||
* since they might be moved, changed by admin working in backend
|
||||
*/
|
||||
interface LinkHandlingInterface
|
||||
{
|
||||
/**
|
||||
* @var non-empty-string will be used for links without a scheme if no default scheme is configured
|
||||
*
|
||||
* @internal Do not use directly; please use `UrlLinkHandler::getDefaultScheme()` instead to also take the
|
||||
* configured default scheme into account.
|
||||
*/
|
||||
public const DEFAULT_SCHEME = 'http';
|
||||
|
||||
/**
|
||||
* Returns a string interpretation of the link href query from objects, something like
|
||||
*
|
||||
* - t3://page?uid=23&my=value#cool
|
||||
* - https://www.typo3.org/
|
||||
* - t3://file?uid=13
|
||||
* - t3://folder?storage=2&identifier=/my/folder/
|
||||
* - mailto:mac@safe.com
|
||||
*
|
||||
* array of data -> string
|
||||
*
|
||||
* @param array $parameters
|
||||
*/
|
||||
public function asString(array $parameters): string;
|
||||
|
||||
/**
|
||||
* Returns an array with data interpretation of the link href from parsed query parameters of urn
|
||||
* representation.
|
||||
*
|
||||
* array of strings -> array of data
|
||||
*
|
||||
* @param array $data
|
||||
*/
|
||||
public function resolveHandlerData(array $data): array;
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\LinkHandling;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use TYPO3\CMS\Core\LinkHandling\Event\AfterLinkResolvedByStringRepresentationEvent;
|
||||
use TYPO3\CMS\Core\LinkHandling\Exception\UnknownLinkHandlerException;
|
||||
use TYPO3\CMS\Core\LinkHandling\Exception\UnknownUrnException;
|
||||
use TYPO3\CMS\Core\SingletonInterface;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\PathUtility;
|
||||
|
||||
/**
|
||||
* Class responsible to find what kind of resource (type) is used
|
||||
* to link to (email, external url, file, page etc)
|
||||
* with the possibility to get a system-wide understandable "urn" to identify
|
||||
* what type it actually is, based on the scheme or prefix.
|
||||
*/
|
||||
class LinkService implements SingletonInterface
|
||||
{
|
||||
public const TYPE_PAGE = 'page';
|
||||
public const TYPE_INPAGE = 'inpage';
|
||||
public const TYPE_URL = 'url';
|
||||
public const TYPE_EMAIL = 'email';
|
||||
public const TYPE_TELEPHONE = 'telephone';
|
||||
public const TYPE_FILE = 'file';
|
||||
public const TYPE_FOLDER = 'folder';
|
||||
public const TYPE_RECORD = 'record';
|
||||
public const TYPE_UNKNOWN = 'unknown';
|
||||
|
||||
/**
|
||||
* All registered LinkHandlers
|
||||
*
|
||||
* @var LinkHandlingInterface[]
|
||||
*/
|
||||
protected $handlers;
|
||||
|
||||
/**
|
||||
* LinkService constructor initializes the registered handlers.
|
||||
*/
|
||||
public function __construct(
|
||||
protected readonly EventDispatcherInterface $eventDispatcher,
|
||||
) {
|
||||
$registeredLinkHandlers = $GLOBALS['TYPO3_CONF_VARS']['SYS']['linkHandler'] ?? [];
|
||||
$registeredLinkHandlers = is_array($registeredLinkHandlers) ? $registeredLinkHandlers : [];
|
||||
/** @var array<string,class-string> $registeredLinkHandlers */
|
||||
if ($registeredLinkHandlers !== []) {
|
||||
foreach ($registeredLinkHandlers as $type => $handlerClassName) {
|
||||
if (!isset($this->handlers[$type]) || !is_object($this->handlers[$type])) {
|
||||
$handler = GeneralUtility::makeInstance($handlerClassName);
|
||||
if ($handler instanceof LinkHandlingInterface) {
|
||||
$this->handlers[$type] = $handler;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Part of the typolink construction functionality, called by typoLink()
|
||||
* Used to resolve "legacy"-based typolinks and URNs.
|
||||
*
|
||||
* Tries to get the type of the link from the link parameter
|
||||
* could be
|
||||
* - "mailto" an email address
|
||||
* - "url" external URL
|
||||
* - "file" a local file (checked AFTER getPublicUrl() is called)
|
||||
* - "page" a page (integer)
|
||||
*
|
||||
* Does NOT check if the page exists or the file exists.
|
||||
*
|
||||
* @param string $linkParameter could be "fileadmin/myfile.jpg", "info@typo3.org", "13" or "http://www.typo3.org"
|
||||
*/
|
||||
public function resolve(string $linkParameter): array
|
||||
{
|
||||
try {
|
||||
// Check if the new syntax with "t3://" is used
|
||||
return $this->resolveByStringRepresentation($linkParameter);
|
||||
} catch (UnknownUrnException $e) {
|
||||
$legacyLinkNotationConverter = GeneralUtility::makeInstance(LegacyLinkNotationConverter::class);
|
||||
return $legacyLinkNotationConverter->resolve($linkParameter);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with data interpretation of the link target, something like t3://page?uid=23.
|
||||
*
|
||||
* @throws Exception\UnknownLinkHandlerException
|
||||
* @throws Exception\UnknownUrnException
|
||||
*/
|
||||
public function resolveByStringRepresentation(string $urn): array
|
||||
{
|
||||
$result = [];
|
||||
$resolveException = null;
|
||||
try {
|
||||
// linking to any t3:// syntax
|
||||
if (stripos($urn, 't3://') === 0) {
|
||||
// lets parse the urn
|
||||
$urnParsed = parse_url($urn);
|
||||
$type = $urnParsed['host'];
|
||||
if (isset($urnParsed['query'])) {
|
||||
parse_str(htmlspecialchars_decode($urnParsed['query']), $data);
|
||||
} else {
|
||||
$data = [];
|
||||
}
|
||||
$fragment = $urnParsed['fragment'] ?? null;
|
||||
|
||||
if (isset($this->handlers[$type])) {
|
||||
$result = $this->handlers[$type]->resolveHandlerData($data);
|
||||
$result['type'] = $type;
|
||||
} else {
|
||||
$resolveException = new UnknownLinkHandlerException('LinkHandler for ' . $type . ' was not registered', 1460581769);
|
||||
}
|
||||
// this was historically named "section"
|
||||
if ($fragment) {
|
||||
$result['fragment'] = $fragment;
|
||||
}
|
||||
} elseif (($this->handlers[self::TYPE_URL] ?? false) && PathUtility::hasProtocolAndScheme($urn)) {
|
||||
$result = $this->handlers[self::TYPE_URL]->resolveHandlerData(['url' => $urn]);
|
||||
$result['type'] = self::TYPE_URL;
|
||||
} elseif (($this->handlers[self::TYPE_EMAIL] ?? false) && str_starts_with(strtolower($urn), 'mailto:')) {
|
||||
$result = $this->handlers[self::TYPE_EMAIL]->resolveHandlerData(['email' => $urn]);
|
||||
$result['type'] = self::TYPE_EMAIL;
|
||||
} elseif (($this->handlers[self::TYPE_TELEPHONE] ?? false) && str_starts_with(strtolower($urn), 'tel:')) {
|
||||
$result = $this->handlers[self::TYPE_TELEPHONE]->resolveHandlerData(['telephone' => $urn]);
|
||||
$result['type'] = self::TYPE_TELEPHONE;
|
||||
}
|
||||
} finally {
|
||||
$result = $this->eventDispatcher->dispatch(
|
||||
new AfterLinkResolvedByStringRepresentationEvent(
|
||||
result: $result,
|
||||
urn: $urn,
|
||||
resolveException: $resolveException
|
||||
)
|
||||
)->getResult();
|
||||
|
||||
if (empty($result['type'])) {
|
||||
// In case no link type could be resolved and UnknownLinkHandlerException
|
||||
// has been added before, throw the exception now to inform calling components.
|
||||
if ($resolveException === null) {
|
||||
// Use the general UnknownUrnException in case neither a defined
|
||||
// handler nor an event listener could resolve the given URN.
|
||||
$resolveException = new UnknownUrnException('No valid URN to resolve found', 1457177667);
|
||||
}
|
||||
|
||||
throw $resolveException;
|
||||
}
|
||||
}
|
||||
|
||||
// @todo If resolved result (linkDetails) are later used to build an uri using LinkBuilder->build(), it's needed
|
||||
// to have the original $linkParameter in the result array. Otherwise, places may break like e.g. the
|
||||
// DatabaseRecordLinkBuilder. Can we safely set this here directly and avoiding calls before build like
|
||||
// "$linkDetails['typoLinkParameter'] = $redirectTarget;" - e.g. like in the ext:redirects
|
||||
// TYPO3\CMS\Redirects\Service\RedirectService::resolveLinkDetailsFromLinkTarget() and other places.
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string interpretation of the link target, something like
|
||||
*
|
||||
* - t3://page?uid=23&my=value#cool
|
||||
* - https://www.typo3.org/
|
||||
* - t3://file?uid=13
|
||||
* - t3://folder?storage=2&identifier=/my/folder/
|
||||
* - mailto:mac@safe.com
|
||||
*
|
||||
* @param array $parameters
|
||||
* @throws Exception\UnknownLinkHandlerException
|
||||
*/
|
||||
public function asString(array $parameters): string
|
||||
{
|
||||
$linkHandler = $this->handlers[$parameters['type']] ?? null;
|
||||
if ($linkHandler !== null) {
|
||||
return $this->handlers[$parameters['type']]->asString($parameters);
|
||||
}
|
||||
if (isset($parameters['url']) && !empty($parameters['url'])) {
|
||||
// This usually happens for tel: or other types where a URL is available and the
|
||||
// legacy link service could resolve at least something
|
||||
return $parameters['url'];
|
||||
}
|
||||
throw new UnknownLinkHandlerException('No valid handlers found for type: ' . $parameters['type'], 1460629247);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\LinkHandling;
|
||||
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
|
||||
/**
|
||||
* Resolves links to pages and the parameters given
|
||||
*/
|
||||
class PageLinkHandler implements LinkHandlingInterface
|
||||
{
|
||||
/**
|
||||
* The Base URN for this link handling to act on
|
||||
* @var string
|
||||
*/
|
||||
protected $baseUrn = 't3://page';
|
||||
|
||||
/**
|
||||
* Returns all valid parameters for linking to a TYPO3 page as a string
|
||||
*/
|
||||
public function asString(array $parameters): string
|
||||
{
|
||||
$urn = $this->baseUrn . (isset($parameters['pageuid']) ? '?uid=' . $parameters['pageuid'] : '');
|
||||
$urn = rtrim($urn, ':');
|
||||
// Page type is set and not empty (= "0" in this case means it is not empty)
|
||||
if (isset($parameters['pagetype']) && strlen((string)$parameters['pagetype']) > 0) {
|
||||
$urn .= '&type=' . $parameters['pagetype'];
|
||||
}
|
||||
if (!empty($parameters['parameters'])) {
|
||||
$urn .= '&' . ltrim($parameters['parameters'], '?&');
|
||||
}
|
||||
if (!empty($parameters['fragment'])) {
|
||||
$urn .= '#' . $parameters['fragment'];
|
||||
}
|
||||
|
||||
return $urn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all relevant information built in the link to a page (see asString())
|
||||
*/
|
||||
public function resolveHandlerData(array $data): array
|
||||
{
|
||||
$result = [];
|
||||
if (isset($data['uid'])) {
|
||||
$result['pageuid'] = MathUtility::canBeInterpretedAsInteger($data['uid']) ? (int)$data['uid'] : $data['uid'];
|
||||
unset($data['uid']);
|
||||
}
|
||||
if (isset($data['type'])) {
|
||||
$result['pagetype'] = $data['type'];
|
||||
unset($data['type']);
|
||||
}
|
||||
if (!empty($data)) {
|
||||
$result['parameters'] = http_build_query($data, '', '&', PHP_QUERY_RFC3986);
|
||||
}
|
||||
if (empty($result)) {
|
||||
$result['pageuid'] = 'current';
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\LinkHandling;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
|
||||
use TYPO3\CMS\Frontend\Typolink\LinkFactory;
|
||||
|
||||
/**
|
||||
* @internal Used internally in handling and resolving pages of type link
|
||||
*/
|
||||
#[Autoconfigure(public: true, shared: true)]
|
||||
readonly class PageTypeLinkResolver
|
||||
{
|
||||
public function __construct(
|
||||
protected TypoLinkCodecService $linkCodecService,
|
||||
protected LinkService $linkService,
|
||||
protected LinkFactory $linkFactory,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Returns the resolved frontend link for a page with type link (doktype 3)
|
||||
*/
|
||||
public function resolvePageLinkUrl(array $pageRecord, ServerRequestInterface $request, ?ContentObjectRenderer $contentObjectRenderer = null): string
|
||||
{
|
||||
if ((int)($pageRecord['doktype'] ?? 0) !== PageRepository::DOKTYPE_LINK) {
|
||||
throw new \RuntimeException(
|
||||
sprintf('This class may only be used with pages of doktype "Link" (3), doktype %s is not supported', $pageRecord['doktype']),
|
||||
1762776856,
|
||||
);
|
||||
}
|
||||
$typolink = (string)($pageRecord['link'] ?? '');
|
||||
if ($contentObjectRenderer === null) {
|
||||
$contentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class);
|
||||
$contentObjectRenderer->setRequest($request);
|
||||
}
|
||||
$url = $contentObjectRenderer->typoLink_URL(['parameter' => $typolink]);
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the HTTP status for redirect in the middleware
|
||||
*
|
||||
* If the destination is a page, we use 307 to preserve the request type
|
||||
* For external URLs and custom types we use 303 see other. It prevents
|
||||
* browsers from accidentally sending POST data to the external site.
|
||||
*
|
||||
* Emails addresses, phone numbers and folders cannot be forwarded and
|
||||
* return null.
|
||||
*/
|
||||
public function getRedirectStatus(array $pageRecord): ?int
|
||||
{
|
||||
$typolinkParts = $this->resolveTypolinkParts($pageRecord);
|
||||
switch ($typolinkParts['type']) {
|
||||
case 'file':
|
||||
$file = $typolinkParts['file'];
|
||||
if ($file->getStorage()->isPublic()) {
|
||||
return 302;
|
||||
}
|
||||
return null;
|
||||
case 'page':
|
||||
return 307;
|
||||
case 'email':
|
||||
case 'telephone':
|
||||
case 'folder':
|
||||
return null;
|
||||
default:
|
||||
return 303;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the decoded TypoLink parts like url, target, additional parameters etc.
|
||||
* merged with the resolved url part, which contains information about the link type
|
||||
* (page, file, external, ..)
|
||||
*/
|
||||
public function resolveTypolinkParts(array $pageRecord): array
|
||||
{
|
||||
if ((int)($pageRecord['doktype'] ?? 0) !== PageRepository::DOKTYPE_LINK) {
|
||||
throw new \RuntimeException(
|
||||
sprintf('This class may only be used with pages of doktype "Link" (3), doktype %s is not supported', $pageRecord['doktype']),
|
||||
1762776917,
|
||||
);
|
||||
}
|
||||
$typolinkTargetData = $this->linkCodecService->decode($pageRecord['link'] ?? '');
|
||||
$typolinkTargetLinkParts = $this->linkService->resolve($typolinkTargetData['url']);
|
||||
|
||||
return array_merge($typolinkTargetData, $typolinkTargetLinkParts);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\LinkHandling;
|
||||
|
||||
/**
|
||||
* Resolves links to records and the parameters given
|
||||
*/
|
||||
class RecordLinkHandler implements LinkHandlingInterface
|
||||
{
|
||||
/**
|
||||
* The Base URN for this link handling to act on
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $baseUrn = 't3://record';
|
||||
|
||||
/**
|
||||
* Returns all valid parameters for linking to a TYPO3 page as a string
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function asString(array $parameters): string
|
||||
{
|
||||
if (empty($parameters['identifier']) || empty($parameters['uid'])) {
|
||||
throw new \InvalidArgumentException('The RecordLinkHandler expects identifier and uid as $parameter configuration.', 1486155150);
|
||||
}
|
||||
$urn = $this->baseUrn;
|
||||
$urn .= sprintf('?identifier=%s&uid=%s', $parameters['identifier'], $parameters['uid']);
|
||||
|
||||
if (!empty($parameters['fragment'])) {
|
||||
$urn .= sprintf('#%s', $parameters['fragment']);
|
||||
}
|
||||
|
||||
return $urn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all relevant information built in the link to a page (see asString())
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function resolveHandlerData(array $data): array
|
||||
{
|
||||
if (empty($data['identifier']) || empty($data['uid'])) {
|
||||
throw new \InvalidArgumentException('The RecordLinkHandler expects identifier, uid as $data configuration', 1486155151);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\LinkHandling;
|
||||
|
||||
/**
|
||||
* Resolves telephone numbers
|
||||
*/
|
||||
class TelephoneLinkHandler implements LinkHandlingInterface
|
||||
{
|
||||
/**
|
||||
* Returns the link to a telephone number as a string
|
||||
*/
|
||||
public function asString(array $parameters): string
|
||||
{
|
||||
$telephoneNumber = preg_replace('/(?:[^\d\+,;]+)/', '', $parameters['telephone']);
|
||||
|
||||
return 'tel:' . $telephoneNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the telephone number without the "tel:" prefix
|
||||
* in the 'telephone' property of the array.
|
||||
*/
|
||||
public function resolveHandlerData(array $data): array
|
||||
{
|
||||
return ['telephone' => substr($data['telephone'], 4)];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\LinkHandling;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Core\LinkHandling\Event\AfterTypoLinkDecodedEvent;
|
||||
use TYPO3\CMS\Core\LinkHandling\Event\BeforeTypoLinkEncodedEvent;
|
||||
|
||||
/**
|
||||
* This class provides basic functionality to encode and decode typolink strings
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
final readonly class TypoLinkCodecService
|
||||
{
|
||||
/**
|
||||
* Delimiter for TypoLink string parts
|
||||
*/
|
||||
private const string DELIMITER = ' ';
|
||||
|
||||
/**
|
||||
* Symbol for TypoLink parts not specified
|
||||
*/
|
||||
private const string EMPTY_VALUE_SYMBOL = '-';
|
||||
|
||||
public function __construct(private EventDispatcherInterface $eventDispatcher) {}
|
||||
|
||||
/**
|
||||
* Encode TypoLink parts to a single string
|
||||
*
|
||||
* @param array{url?: string, target?: string, class?: string, title?: string, additionalParams?: string, rel?: string, download?: string} $typoLinkParts
|
||||
* @return string A correctly encoded TypoLink string
|
||||
*/
|
||||
public function encode(array $typoLinkParts): string
|
||||
{
|
||||
if (empty($typoLinkParts) || !isset($typoLinkParts['url'])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Get empty structure
|
||||
$reverseSortedParameters = array_reverse($this->decode(''), true);
|
||||
// Add optional rel support as sixth TypoLink part.
|
||||
if (array_key_exists('rel', $typoLinkParts) || array_key_exists('download', $typoLinkParts)) {
|
||||
$reverseSortedParameters = ['rel' => '', ...$reverseSortedParameters];
|
||||
}
|
||||
// Add optional download support as seventh TypoLink part.
|
||||
if (array_key_exists('download', $typoLinkParts)) {
|
||||
$reverseSortedParameters = ['download' => '', ...$reverseSortedParameters];
|
||||
}
|
||||
$aValueWasSet = false;
|
||||
foreach ($reverseSortedParameters as $key => &$value) {
|
||||
$value = $typoLinkParts[$key] ?? '';
|
||||
// escape special character \ and "
|
||||
$value = str_replace(['\\', '"'], ['\\\\', '\\"'], $value);
|
||||
// enclose with quotes if a string contains the delimiter
|
||||
if (str_contains($value, self::DELIMITER)) {
|
||||
$value = '"' . $value . '"';
|
||||
}
|
||||
// fill with - if another values has already been set
|
||||
if ($value === '' && $aValueWasSet) {
|
||||
$value = self::EMPTY_VALUE_SYMBOL;
|
||||
}
|
||||
if ($value !== '') {
|
||||
$aValueWasSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
$reverseSortedParameters = $this->eventDispatcher->dispatch(
|
||||
new BeforeTypoLinkEncodedEvent(
|
||||
parameters: $reverseSortedParameters,
|
||||
typoLinkParts: $typoLinkParts,
|
||||
delimiter: self::DELIMITER,
|
||||
emptyValueSymbol: self::EMPTY_VALUE_SYMBOL
|
||||
)
|
||||
)->getParameters();
|
||||
|
||||
return trim(implode(self::DELIMITER, array_reverse($reverseSortedParameters, true)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a TypoLink string into its parts
|
||||
*
|
||||
* @param string $typoLink The properly encoded TypoLink string
|
||||
* @return array{url: string, target: string, class: string, title: string, additionalParams: string, rel?: string, download?: string}
|
||||
*/
|
||||
public function decode(string $typoLink): array
|
||||
{
|
||||
$typoLink = trim($typoLink);
|
||||
if ($typoLink !== '') {
|
||||
$parts = str_replace(['\\\\', '\\"'], ['\\', '"'], str_getcsv($typoLink, self::DELIMITER, '"', '\\'));
|
||||
} else {
|
||||
$parts = [];
|
||||
}
|
||||
|
||||
// The order of the entries is crucial!!
|
||||
$typoLinkParts = [
|
||||
'url' => isset($parts[0]) ? trim($parts[0]) : '',
|
||||
'target' => isset($parts[1]) && $parts[1] !== self::EMPTY_VALUE_SYMBOL ? trim($parts[1]) : '',
|
||||
'class' => isset($parts[2]) && $parts[2] !== self::EMPTY_VALUE_SYMBOL ? trim($parts[2]) : '',
|
||||
'title' => isset($parts[3]) && $parts[3] !== self::EMPTY_VALUE_SYMBOL ? trim($parts[3]) : '',
|
||||
'additionalParams' => isset($parts[4]) && $parts[4] !== self::EMPTY_VALUE_SYMBOL ? trim($parts[4]) : '',
|
||||
];
|
||||
if (isset($parts[5]) && $parts[5] !== self::EMPTY_VALUE_SYMBOL) {
|
||||
$typoLinkParts['rel'] = trim($parts[5]);
|
||||
}
|
||||
if (isset($parts[6]) && $parts[6] !== self::EMPTY_VALUE_SYMBOL) {
|
||||
$typoLinkParts['download'] = trim($parts[6]);
|
||||
}
|
||||
|
||||
return $this->eventDispatcher->dispatch(
|
||||
new AfterTypoLinkDecodedEvent(
|
||||
typoLinkParts: $typoLinkParts,
|
||||
typoLink: $typoLink,
|
||||
delimiter: self::DELIMITER,
|
||||
emptyValueSymbol: self::EMPTY_VALUE_SYMBOL
|
||||
)
|
||||
)->getTypoLinkParts();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\LinkHandling;
|
||||
|
||||
/**
|
||||
* This class represents an object containing the resolved parameters of a typolink
|
||||
*/
|
||||
final readonly class TypolinkParameter implements \JsonSerializable
|
||||
{
|
||||
public function __construct(
|
||||
public string $url = '',
|
||||
public string $target = '',
|
||||
public string $class = '',
|
||||
public string $title = '',
|
||||
public string $additionalParams = '',
|
||||
public array $customParams = [],
|
||||
) {}
|
||||
|
||||
public static function createFromTypolinkParts(array $typoLinkParts): TypolinkParameter
|
||||
{
|
||||
$url = $typoLinkParts['url'] ?? '';
|
||||
$target = $typoLinkParts['target'] ?? '';
|
||||
$class = $typoLinkParts['class'] ?? '';
|
||||
$title = $typoLinkParts['title'] ?? '';
|
||||
$additionalParams = $typoLinkParts['additionalParams'] ?? '';
|
||||
unset($typoLinkParts['url'], $typoLinkParts['target'], $typoLinkParts['class'], $typoLinkParts['title'], $typoLinkParts['additionalParams']);
|
||||
|
||||
return new self(
|
||||
$url,
|
||||
$target,
|
||||
$class,
|
||||
$title,
|
||||
$additionalParams,
|
||||
$typoLinkParts
|
||||
);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return array_merge([
|
||||
'url' => $this->url,
|
||||
'target' => $this->target,
|
||||
'class' => $this->class,
|
||||
'title' => $this->title,
|
||||
'additionalParams' => $this->additionalParams,
|
||||
], $this->customParams);
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\LinkHandling;
|
||||
|
||||
/**
|
||||
* Resolves URLs (simple, no magic needed)
|
||||
*/
|
||||
class UrlLinkHandler implements LinkHandlingInterface
|
||||
{
|
||||
/**
|
||||
* Returns the URL as given
|
||||
*/
|
||||
public function asString(array $parameters): string
|
||||
{
|
||||
return $this->addHttpSchemeAsFallback($parameters['url']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL as is
|
||||
*
|
||||
* @param array $data (needs 'url') inside
|
||||
*/
|
||||
public function resolveHandlerData(array $data): array
|
||||
{
|
||||
return ['url' => $this->addHttpSchemeAsFallback($data['url'])];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that a scheme is always added, if www.typo3.org was added previously.
|
||||
*
|
||||
* @param string $url the URL
|
||||
*/
|
||||
protected function addHttpSchemeAsFallback(string $url): string
|
||||
{
|
||||
if (!empty($url)) {
|
||||
// We expect this an absolute path, and leave as is. We also leave double slashes ('//') as is.
|
||||
if (str_starts_with($url, '/')) {
|
||||
return $url;
|
||||
}
|
||||
$scheme = parse_url($url, PHP_URL_SCHEME);
|
||||
if (empty($scheme)) {
|
||||
$url = self::getDefaultScheme() . '://' . $url;
|
||||
} elseif (in_array(strtolower($scheme), ['javascript', 'data'], true)) {
|
||||
// deny using insecure scheme's like `javascript:` or `data:` as URL scheme
|
||||
$url = '';
|
||||
}
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the scheme (e.g. `http`) to be used for links with URLs without a scheme, e.g., for `www.example.com`.
|
||||
*
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public static function getDefaultScheme(): string
|
||||
{
|
||||
return ($GLOBALS['TYPO3_CONF_VARS']['SYS']['defaultScheme'] ?? '')
|
||||
?: LinkHandlingInterface::DEFAULT_SCHEME;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user