TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:09 +02:00
commit af8cc155b5
6818 changed files with 642608 additions and 0 deletions
@@ -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\DataHandling\SoftReference;
/**
* A generic parser class useful if tokenID prefixes are needed.
*/
abstract class AbstractSoftReferenceParser implements SoftReferenceParserInterface
{
protected string $tokenID_basePrefix = '';
protected string $parserKey = '';
protected array $parameters = [];
/**
* Make Token ID for input index.
*
* @param string $index Suffix value.
* @return string Token ID
*/
public function makeTokenID(string $index = ''): string
{
return md5($this->tokenID_basePrefix . ':' . $index);
}
/**
* @param string $parserKey The softref parser key.
* @param array $parameters Parameters of the softlink parser. Basically this is the content inside optional []-brackets after the softref keys. Parameters are exploded by ";
*/
public function setParserKey(string $parserKey, array $parameters): void
{
$this->parserKey = $parserKey;
$this->parameters = $parameters;
}
public function getParserKey(): string
{
return $this->parserKey;
}
protected function setTokenIdBasePrefix(string $table, string $uid, string $field, string $structurePath): void
{
$this->tokenID_basePrefix = implode(':', [$table, $uid, $field, $structurePath, $this->getParserKey()]);
}
}
@@ -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\DataHandling\SoftReference;
/**
* Finding email addresses in content and making them substitutable.
*/
class EmailSoftReferenceParser extends AbstractSoftReferenceParser
{
public function parse(string $table, string $field, int $uid, string $content, string $structurePath = ''): SoftReferenceParserResult
{
$this->setTokenIdBasePrefix($table, (string)$uid, $field, $structurePath);
$elements = [];
// Email:
$parts = preg_split('/([\s\'":<>]+)([A-Za-z0-9._-]+[^-][@][A-Za-z0-9._-]+[.].[A-Za-z0-9]+)/', ' ' . $content . ' ', 10000, PREG_SPLIT_DELIM_CAPTURE);
foreach ($parts as $idx => $value) {
if ($idx % 3 === 2) {
// Ignore invalid emails, which haven't been filtered out by regex.
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
continue;
}
$tokenID = $this->makeTokenID((string)$idx);
$elements[$idx] = [];
$elements[$idx]['matchString'] = $value;
if (in_array('subst', $this->parameters, true)) {
$parts[$idx] = '{softref:' . $tokenID . '}';
$elements[$idx]['subst'] = [
'type' => 'string',
'tokenID' => $tokenID,
'tokenValue' => $value,
];
}
}
}
return SoftReferenceParserResult::create(
substr(implode('', $parts), 1, -1),
$elements
);
}
}
@@ -0,0 +1,61 @@
<?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\DataHandling\SoftReference;
/**
* Finding reference to files from extensions in content, but only to notify about their existence. No substitution
*/
class ExtensionPathSoftReferenceParser implements SoftReferenceParserInterface
{
private const string REGEXP = '/([^[:alnum:]]+)(EXT:[[:alnum:]_]+\\/[^[:space:]"\',]*)/';
protected string $parserKey = '';
protected array $parameters = [];
public function parse(string $table, string $field, int $uid, string $content, string $structurePath = ''): SoftReferenceParserResult
{
$elements = [];
// Files starting with EXT:
$parts = preg_split(self::REGEXP, ' ' . $content . ' ', 10000, PREG_SPLIT_DELIM_CAPTURE) ?: [];
foreach ($parts as $idx => $value) {
if ($idx % 3 === 2) {
$elements[$idx] = [];
$elements[$idx]['matchString'] = $value;
}
}
return SoftReferenceParserResult::create(
substr(implode('', $parts), 1, -1),
$elements
);
}
/**
* @param string $parserKey The softref parser key.
* @param array $parameters Parameters of the softlink parser. Basically this is the content inside optional []-brackets after the softref keys. Parameters are exploded by ";
*/
public function setParserKey(string $parserKey, array $parameters): void
{
$this->parserKey = $parserKey;
$this->parameters = $parameters;
}
public function getParserKey(): string
{
return $this->parserKey;
}
}
@@ -0,0 +1,144 @@
<?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\DataHandling\SoftReference;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
#[Autoconfigure(public: true)]
class SoftReferenceParserFactory
{
protected array $softReferenceParsers = [];
public function __construct(
#[Autowire(service: 'cache.runtime')]
protected readonly FrontendInterface $runtimeCache,
protected readonly LoggerInterface $logger,
) {}
/**
* Adds a parser via DI.
*
* @internal
*/
public function addParser(SoftReferenceParserInterface $softReferenceParser, string $parserKey): void
{
if (!isset($this->softReferenceParsers[$parserKey])) {
$this->softReferenceParsers[$parserKey] = $softReferenceParser;
}
}
/**
* Returns array of soft parser references
*
* @param string $parserList softRef parser list
* @return array|null Array where the parser key is the key and the value is the parameter string, FALSE if no parsers were found
*/
protected function explodeSoftRefParserList(string $parserList): ?array
{
// Return immediately if list is blank:
if ($parserList === '') {
return null;
}
$cacheId = 'backend-softRefList-' . md5($parserList);
$parserListCache = $this->runtimeCache->get($cacheId);
if ($parserListCache !== false) {
return $parserListCache;
}
// Otherwise parse the list:
$keyList = GeneralUtility::trimExplode(',', $parserList, true);
$output = [];
foreach ($keyList as $val) {
$reg = [];
if (preg_match('/^([[:alnum:]_-]+)\\[(.*)\\]$/', $val, $reg)) {
$output[$reg[1]] = GeneralUtility::trimExplode(';', $reg[2], true);
} else {
$output[$val] = '';
}
}
$this->runtimeCache->set($cacheId, $output);
return $output;
}
/**
* @param array|null $forcedParameters
* @return iterable<SoftReferenceParserInterface>
*/
public function getParsersBySoftRefParserList(string $softRefParserList, ?array $forcedParameters = null): iterable
{
foreach ($this->explodeSoftRefParserList($softRefParserList) ?? [] as $parserKey => $parameters) {
if (!is_array($parameters)) {
$parameters = $forcedParameters ?? [];
}
if (!$this->hasSoftReferenceParser($parserKey)) {
$this->logger->warning('No soft reference parser exists for the key "{parserKey}".', ['parserKey' => $parserKey]);
continue;
}
$parser = $this->getSoftReferenceParser($parserKey);
$parser->setParserKey($parserKey, $parameters);
yield $parser;
}
}
public function hasSoftReferenceParser(string $softReferenceParserKey): bool
{
return isset($this->softReferenceParsers[$softReferenceParserKey]);
}
/**
* Get a Soft Reference Parser by the given soft reference key.
* Implementation must be registered in Configuration/Services.yaml
*
* VENDOR\YourExtension\SoftReference\UserDefinedSoftReferenceParser:
* tags:
* - name: softreference.parser
* parserKey: userdefined
*/
public function getSoftReferenceParser(string $softReferenceParserKey): SoftReferenceParserInterface
{
if ($softReferenceParserKey === '') {
throw new \InvalidArgumentException(
'The soft reference parser key cannot be empty.',
1627899274
);
}
if (!$this->hasSoftReferenceParser($softReferenceParserKey)) {
throw new \OutOfRangeException(
sprintf('No soft reference parser found for "%s".', $softReferenceParserKey),
1627899342
);
}
return $this->softReferenceParsers[$softReferenceParserKey];
}
/**
* Get all registered soft reference parsers
*/
public function getSoftReferenceParsers(): array
{
return $this->softReferenceParsers;
}
}
@@ -0,0 +1,58 @@
<?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\DataHandling\SoftReference;
/**
* Soft Reference parsing interface
*
* "Soft References" are references to database elements, files, email addresses, URLs etc.
* which are found in-text in content. The <a href="t3://page?[page_id]> tag from typical bodytext fields
* is an example of this.
* This interface defines the "parse" method, which parsers have to implement.
* TYPO3 has already implemented parsers for the most well-known types. Soft Reference Parsers can also be user-defined.
* The Soft Reference Parsers are used by the system to find these references and process them accordingly in import/export actions and copy operations.
*/
interface SoftReferenceParserInterface
{
/**
* Main function through which can parse content for a specific field.
*
* @param string $table Database table name
* @param string $field Field name for which processing occurs
* @param int $uid UID of the record
* @param string $content The content/value of the field
* @param string $structurePath If running from inside a FlexForm structure, this is the path of the tag.
* @return SoftReferenceParserResult Result object on positive matches, see description above.
* @see SoftReferenceParserResult
*/
public function parse(string $table, string $field, int $uid, string $content, string $structurePath = ''): SoftReferenceParserResult;
/**
* The two properties parserKey and parameters may be set to generate a unique token ID from them.
* This is not needed for every parser, but useful if a parser can deal with multiple parser keys.
*
* @param string $parserKey The softref parser key.
* @param array $parameters Parameters of the softlink parser. Basically this is the content inside optional []-brackets after the softref keys. Parameters are exploded by ";
*/
public function setParserKey(string $parserKey, array $parameters): void;
/**
* Returns the parser key, which was previously set by "setParserKey"
*/
public function getParserKey(): string;
}
@@ -0,0 +1,87 @@
<?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\DataHandling\SoftReference;
/**
* The result object has two properties: "content" and matched elements.
*
* content:
* Is a string containing the input content but possibly with tokens inside.
* Tokens are strings like {softref:[tokenID]}, which is a placeholder for a value extracted by a softref parser.
* For each token there MUST be an entry in the "elements" key, which has a "subst" key defining the tokenID and the tokenValue. See below.
*
* matched elements:
* is an array where the keys are insignificant, but the values are arrays with these keys:
* "matchString" => // The value of the match. This is only for informational purposes to show what was found.
* "error" => // An error message can be set here, like "file not found" etc.
* "subst" => [ // If this array is found there MUST be a token in the output content as well!
* "tokenID" => // The tokenID string corresponding to the token in output content, {softref:[tokenID]}. This is typically an md5 hash of a string defining uniquely the position of the element.
* "tokenValue" => // The value that the token substitutes in the text. Basically, if this value is inserted instead of the token the content should match what was inputted originally.
* "type" => // file / db / string = the type of substitution. "file" means it is a relative file [automatically mapped], "db" means a database record reference [automatically mapped], "string" means it is manually modified string content (eg. an email address)
* "relFileName" => // (for "file" type): Relative filename. May not necessarily exist. This could be noticed in the error key.
* "recordRef" => // (for "db" type) : Reference to DB record on the form [table]:[uid]. May not necessarily exist.
* "title" => // Title of element (for backend information)
* "description" => // Description of element (for backend information)
* ]
*/
final class SoftReferenceParserResult
{
private string $content = '';
private array $elements = [];
private bool $hasMatched = false;
public static function create(string $content, array $elements): self
{
if ($elements === []) {
return self::createWithoutMatches();
}
$obj = new self();
$obj->content = $content;
$obj->elements = $elements;
$obj->hasMatched = true;
return $obj;
}
public static function createWithoutMatches(): self
{
// @todo: set protected, use create() with empty elements instead.
return new self();
}
public function hasMatched(): bool
{
return $this->hasMatched;
}
public function hasContent(): bool
{
return $this->content !== '';
}
public function getContent(): string
{
return $this->content;
}
public function getMatchedElements(): array
{
return $this->elements;
}
}
@@ -0,0 +1,44 @@
<?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\DataHandling\SoftReference;
/**
* A full field value targeted for manual substitution (for import /export features)
*/
class SubstituteSoftReferenceParser extends AbstractSoftReferenceParser
{
public function parse(string $table, string $field, int $uid, string $content, string $structurePath = ''): SoftReferenceParserResult
{
$this->setTokenIdBasePrefix($table, (string)$uid, $field, $structurePath);
$tokenID = $this->makeTokenID();
return SoftReferenceParserResult::create(
'{softref:' . $tokenID . '}',
[
[
'matchString' => $content,
'subst' => [
'type' => 'string',
'tokenID' => $tokenID,
'tokenValue' => $content,
],
],
]
);
}
}
@@ -0,0 +1,299 @@
<?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\DataHandling\SoftReference;
use Psr\EventDispatcher\EventDispatcherInterface;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\DataHandling\Event\AppendLinkHandlerElementsEvent;
use TYPO3\CMS\Core\LinkHandling\Exception\UnknownLinkHandlerException;
use TYPO3\CMS\Core\LinkHandling\LinkService;
use TYPO3\CMS\Core\LinkHandling\TypoLinkCodecService;
use TYPO3\CMS\Core\Resource\AbstractFile;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* TypoLink value processing.
* Will process input value as a TypoLink value.
* References to page id or file, possibly with anchor/target, possibly commaseparated list.
*/
class TypolinkSoftReferenceParser extends AbstractSoftReferenceParser
{
protected EventDispatcherInterface $eventDispatcher;
public function __construct(EventDispatcherInterface $eventDispatcher)
{
$this->eventDispatcher = $eventDispatcher;
}
public function parse(string $table, string $field, int $uid, string $content, string $structurePath = ''): SoftReferenceParserResult
{
$this->setTokenIdBasePrefix($table, (string)$uid, $field, $structurePath);
// First, split the input string by a comma if the "linkList" parameter is set.
// An example: the link field for images in content elements of type "textpic" or "image". This field CAN be configured to define a link per image, separated by comma.
if (in_array('linkList', $this->parameters, true)) {
// Preserving whitespace on purpose.
$linkElement = explode(',', $content);
} else {
// If only one element, just set in this array to make it easy below.
$linkElement = [$content];
}
// Traverse the links now:
$elements = [];
foreach ($linkElement as $k => $typolinkValue) {
$tLP = $this->getTypoLinkParts($typolinkValue, $table, $uid);
$linkElement[$k] = $this->setTypoLinkPartsElement($tLP, $elements, $typolinkValue, $k);
}
return SoftReferenceParserResult::create(
implode(',', $linkElement),
$elements
);
}
/**
* Analyze content as a TypoLink value and return an array with properties.
* TypoLinks format is: <link [typolink] [browser target] [css class] [title attribute] [additionalParams]>.
* See TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::typolink()
* The syntax of the [typolink] part is: [typolink] = [page id][,[type value]][#[anchor, if integer = tt_content uid]]
* The extraction is based on how \TYPO3\CMS\Frontend\ContentObject::typolink() behaves.
*
* @param string $typolinkValue TypoLink value.
* @param string $referenceTable The reference table
* @param int $referenceUid The UID of the reference record
* @return array Array with the properties of the input link specified. The key "type" will reveal the type. If that is blank it could not be determined.
* @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::typolink()
* @see setTypoLinkPartsElement()
*/
protected function getTypoLinkParts(string $typolinkValue, string $referenceTable, int $referenceUid)
{
$finalTagParts = GeneralUtility::makeInstance(TypoLinkCodecService::class)->decode($typolinkValue);
$link_param = $finalTagParts['url'];
// we define various keys below, "url" might be misleading
unset($finalTagParts['url']);
if (stripos(rawurldecode(trim($link_param)), 'phar://') === 0) {
throw new \RuntimeException(
'phar scheme not allowed as soft reference target',
1530030672
);
}
$linkService = GeneralUtility::makeInstance(LinkService::class);
try {
$linkData = $linkService->resolve($link_param);
switch ($linkData['type']) {
case LinkService::TYPE_RECORD:
$referencePageId = $referenceTable === 'pages'
? $referenceUid
: (int)(BackendUtility::getRecord($referenceTable, $referenceUid)['pid'] ?? 0);
if ($referencePageId) {
$pageTsConfig = BackendUtility::getPagesTSconfig($referencePageId);
$table = $pageTsConfig['TCEMAIN.']['linkHandler.'][$linkData['identifier'] . '.']['configuration.']['table'] ?? $linkData['identifier'];
} else {
// Backwards compatibility for the old behaviour, where the identifier was saved as the table.
$table = $linkData['identifier'];
}
$finalTagParts['table'] = $table;
$finalTagParts['uid'] = $linkData['uid'];
break;
case LinkService::TYPE_PAGE:
$linkData['pageuid'] = (int)($linkData['pageuid'] ?? 0);
if (isset($linkData['pagetype'])) {
$linkData['pagetype'] = (int)$linkData['pagetype'];
}
if (isset($linkData['fragment'])) {
$finalTagParts['anchor'] = $linkData['fragment'];
}
break;
case LinkService::TYPE_FILE:
case LinkService::TYPE_UNKNOWN:
if (isset($linkData['file'])) {
$finalTagParts['type'] = LinkService::TYPE_FILE;
$linkData['file'] = $linkData['file'] instanceof AbstractFile ? $linkData['file']->getUid() : $linkData['file'];
} else {
$pU = parse_url($link_param);
parse_str($pU['query'] ?? '', $query);
if (isset($query['uid'])) {
$finalTagParts['type'] = LinkService::TYPE_FILE;
$finalTagParts['file'] = (int)$query['uid'];
}
}
break;
}
return array_merge($finalTagParts, $linkData);
} catch (UnknownLinkHandlerException $e) {
// Cannot handle anything
return $finalTagParts;
}
}
/**
* Recompile a TypoLink value from the array of properties made with getTypoLinkParts() into an elements array
*
* @param array $tLP TypoLink properties
* @param array $elements Array of elements to be modified with substitution / information entries.
* @param string $content The content to process.
* @param int $idx Index value of the found element - user to make unique but stable tokenID
* @return string The input content, possibly containing tokens now according to the added substitution entries in $elements
* @see getTypoLinkParts()
*/
protected function setTypoLinkPartsElement($tLP, &$elements, $content, $idx)
{
// Initialize, set basic values. In any case a link will be shown
$tokenID = $this->makeTokenID('setTypoLinkPartsElement:' . $idx);
$elements[$tokenID . ':' . $idx] = [];
$elements[$tokenID . ':' . $idx]['matchString'] = $content;
// Based on link type, maybe do more:
switch ((string)($tLP['type'] ?? '')) {
case LinkService::TYPE_EMAIL:
// Mail addresses can be substituted manually:
$elements[$tokenID . ':' . $idx]['subst'] = [
'type' => 'string',
'tokenID' => $tokenID,
'tokenValue' => (string)($tLP['email'] ?? ''),
];
// Output content will be the token instead:
$content = '{softref:' . $tokenID . '}';
break;
case LinkService::TYPE_TELEPHONE:
// phone number can be substituted manually:
$elements[$tokenID . ':' . $idx]['subst'] = [
'type' => 'string',
'tokenID' => $tokenID,
'tokenValue' => (string)($tLP['telephone'] ?? ''),
];
// Output content will be the token instead:
$content = '{softref:' . $tokenID . '}';
break;
case LinkService::TYPE_URL:
// URLs can be substituted manually
$elements[$tokenID . ':' . $idx]['subst'] = [
'type' => 'external',
'tokenID' => $tokenID,
'tokenValue' => (string)($tLP['url'] ?? ''),
];
// Output content will be the token instead:
$content = '{softref:' . $tokenID . '}';
break;
case LinkService::TYPE_FOLDER:
// This is a link to a folder...
unset($elements[$tokenID . ':' . $idx]);
return $content;
case LinkService::TYPE_FILE:
// Process files referenced by their FAL uid
if (isset($tLP['file'])) {
$fileId = $tLP['file'] instanceof AbstractFile ? $tLP['file']->getUid() : $tLP['file'];
// Token and substitute value
$elements[$tokenID . ':' . $idx]['subst'] = [
'type' => 'db',
'recordRef' => 'sys_file:' . $fileId,
'tokenID' => $tokenID,
'tokenValue' => 'file:' . $fileId,
];
// Output content will be the token instead:
$content = '{softref:' . $tokenID . '}';
} elseif ($tLP['identifier'] ?? false) {
$linkHandlerValue = explode(':', trim($tLP['identifier']), 2)[1];
if (MathUtility::canBeInterpretedAsInteger($linkHandlerValue)) {
// Token and substitute value
$elements[$tokenID . ':' . $idx]['subst'] = [
'type' => 'db',
'recordRef' => 'sys_file:' . $linkHandlerValue,
'tokenID' => $tokenID,
'tokenValue' => (string)$tLP['identifier'],
];
// Output content will be the token instead:
$content = '{softref:' . $tokenID . '}';
} else {
// This is a link to a folder...
return $content;
}
} else {
return $content;
}
break;
case LinkService::TYPE_PAGE:
// Rebuild page reference typolink part:
$content = '';
// Set page id:
if ($tLP['pageuid']) {
$content .= '{softref:' . $tokenID . '}';
$elements[$tokenID . ':' . $idx]['subst'] = [
'type' => 'db',
'recordRef' => 'pages:' . $tLP['pageuid'],
'tokenID' => $tokenID,
'tokenValue' => (string)$tLP['pageuid'],
];
}
// Add type if applicable
if ((string)($tLP['pagetype'] ?? '') !== '') {
$content .= ',' . $tLP['pagetype'];
}
// Add anchor if applicable
if ((string)($tLP['anchor'] ?? '') !== '') {
// Anchor is assumed to point to a content elements:
if (MathUtility::canBeInterpretedAsInteger($tLP['anchor'])) {
// Initialize a new entry because we have a new relation:
$newTokenID = $this->makeTokenID('setTypoLinkPartsElement:anchor:' . $idx);
$elements[$newTokenID . ':' . $idx] = [];
$elements[$newTokenID . ':' . $idx]['matchString'] = 'Anchor Content Element: ' . $tLP['anchor'];
$content .= '#{softref:' . $newTokenID . '}';
$elements[$newTokenID . ':' . $idx]['subst'] = [
'type' => 'db',
'recordRef' => 'tt_content:' . $tLP['anchor'],
'tokenID' => $newTokenID,
'tokenValue' => (string)$tLP['anchor'],
];
} else {
// Anchor is a hardcoded string
$content .= '#' . $tLP['anchor'];
}
}
break;
case LinkService::TYPE_RECORD:
$elements[$tokenID . ':' . $idx]['subst'] = [
'type' => 'db',
'recordRef' => $tLP['table'] . ':' . $tLP['uid'],
'tokenID' => $tokenID,
'tokenValue' => (string)$content,
];
$content = '{softref:' . $tokenID . '}';
break;
default:
$event = new AppendLinkHandlerElementsEvent($tLP, $content, $elements, $idx, $tokenID);
$this->eventDispatcher->dispatch($event);
$elements = $event->getElements();
$tLP = $event->getLinkParts();
$content = $event->getContent();
if (!$event->isResolved()) {
$elements[$tokenID . ':' . $idx]['error'] = 'Couldn\'t decide typolink mode.';
return $content;
}
}
// Finally, for all entries that was rebuild with tokens, add target, class, title and additionalParams in the end
$tLP['url'] = $content;
// Return rebuilt typolink value
return GeneralUtility::makeInstance(TypoLinkCodecService::class)->encode($tLP);
}
}
@@ -0,0 +1,144 @@
<?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\DataHandling\SoftReference;
use Psr\EventDispatcher\EventDispatcherInterface;
use TYPO3\CMS\Core\DataHandling\Event\AppendLinkHandlerElementsEvent;
use TYPO3\CMS\Core\Html\HtmlParser;
use TYPO3\CMS\Core\LinkHandling\LinkService;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* TypoLink tag processing.
* Will search for <link ...> and <a> tags in the content string and process any found.
*/
class TypolinkTagSoftReferenceParser extends AbstractSoftReferenceParser
{
protected EventDispatcherInterface $eventDispatcher;
public function __construct(EventDispatcherInterface $eventDispatcher)
{
$this->eventDispatcher = $eventDispatcher;
}
public function parse(string $table, string $field, int $uid, string $content, string $structurePath = ''): SoftReferenceParserResult
{
$this->setTokenIdBasePrefix($table, (string)$uid, $field, $structurePath);
// Parse string for special TYPO3 <link> tag:
$htmlParser = GeneralUtility::makeInstance(HtmlParser::class);
$linkService = GeneralUtility::makeInstance(LinkService::class);
$linkTags = $htmlParser->splitTags('a', $content);
// Traverse result:
$elements = [];
foreach ($linkTags as $key => $foundValue) {
if ($key % 2 && preg_match('/href="([^"]+)"/', $foundValue, $matches)) {
try {
$linkDetails = $linkService->resolve($matches[1]);
if ($linkDetails['type'] === LinkService::TYPE_FILE && preg_match('/file\?uid=(\d+)/', $matches[1], $fileIdMatch)) {
$token = $this->makeTokenID((string)$key);
$elements[$key]['matchString'] = $foundValue;
$linkTags[$key] = str_replace($matches[1], '{softref:' . $token . '}', $foundValue);
$elements[$key]['subst'] = [
'type' => 'db',
'recordRef' => 'sys_file:' . $fileIdMatch[1],
'tokenID' => $token,
'tokenValue' => 'file:' . ($linkDetails['file'] instanceof File ? $linkDetails['file']->getUid() : $fileIdMatch[1]),
];
} elseif ($linkDetails['type'] === LinkService::TYPE_PAGE && preg_match('/page\?[^#]*\buid=(\d+)(?:[^#]*#(\d+))?/', $matches[1], $pageAndAnchorMatches)) {
$token = $this->makeTokenID((string)$key);
$content = '{softref:' . $token . '}';
$elements[$key]['matchString'] = $foundValue;
$elements[$key]['subst'] = [
'type' => 'db',
'recordRef' => 'pages:' . ($linkDetails['pageuid'] ?? 0),
'tokenID' => $token,
'tokenValue' => $linkDetails['pageuid'] ?? '',
];
if (isset($pageAndAnchorMatches[2])) {
// Anchor is assumed to point to a content elements:
if (MathUtility::canBeInterpretedAsInteger($pageAndAnchorMatches[2])) {
// Initialize a new entry because we have a new relation:
$newTokenID = $this->makeTokenID('setTypoLinkPartsElement:anchor:' . $key);
$elements[$newTokenID . ':' . $key] = [];
$elements[$newTokenID . ':' . $key]['matchString'] = 'Anchor Content Element: ' . $pageAndAnchorMatches[2];
$content .= '#{softref:' . $newTokenID . '}';
$elements[$newTokenID . ':' . $key]['subst'] = [
'type' => 'db',
'recordRef' => 'tt_content:' . $pageAndAnchorMatches[2],
'tokenID' => $newTokenID,
'tokenValue' => $pageAndAnchorMatches[2],
];
} else {
// Anchor is a hardcoded string
$content .= '#' . $pageAndAnchorMatches[2];
}
}
$linkTags[$key] = str_replace($matches[1], $content, $foundValue);
} elseif ($linkDetails['type'] === LinkService::TYPE_URL) {
$token = $this->makeTokenID((string)$key);
$elements[$key]['matchString'] = $foundValue;
$linkTags[$key] = str_replace($matches[1], '{softref:' . $token . '}', $foundValue);
$elements[$key]['subst'] = [
'type' => 'external',
'tokenID' => $token,
'tokenValue' => (string)($linkDetails['url'] ?? ''),
];
} elseif ($linkDetails['type'] === LinkService::TYPE_EMAIL) {
$token = $this->makeTokenID((string)$key);
$elements[$key]['matchString'] = $foundValue;
$linkTags[$key] = str_replace($matches[1], '{softref:' . $token . '}', $foundValue);
$elements[$key]['subst'] = [
'type' => 'string',
'tokenID' => $token,
'tokenValue' => (string)($linkDetails['email'] ?? ''),
];
} elseif ($linkDetails['type'] === LinkService::TYPE_TELEPHONE) {
$token = $this->makeTokenID((string)$key);
$elements[$key]['matchString'] = $foundValue;
$linkTags[$key] = str_replace($matches[1], '{softref:' . $token . '}', $foundValue);
$elements[$key]['subst'] = [
'type' => 'string',
'tokenID' => $token,
'tokenValue' => (string)($linkDetails['telephone'] ?? ''),
];
} else {
$token = $this->makeTokenID((string)$key);
$event = new AppendLinkHandlerElementsEvent($linkDetails, $content, $elements, $key, $token);
$this->eventDispatcher->dispatch($event);
if (!$event->isResolved()) {
continue;
}
$elements = $event->getElements();
}
} catch (\Exception $e) {
// skip invalid links
}
}
}
// Return output:
return SoftReferenceParserResult::create(
implode('', $linkTags),
$elements
);
}
}
@@ -0,0 +1,71 @@
<?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\DataHandling\SoftReference;
/**
* Finding URLs in content
*/
class UrlSoftReferenceParser extends AbstractSoftReferenceParser
{
/**
* We do not use a-z for letters, so we can also allow unicode characters. For this purpose we use \p{L}, for example.
* And we must use the modifier "u" in the regex.
* Domains may contain umlauts (ä,ö,ü).
*
* PHP regex with unicode character properties:
* \p{L}: letter
* \p{Ll}: lower case letter
* @see https://www.php.net/manual/en/regexp.reference.unicode.php
*/
protected const REGEXP = '/([^[:alnum:]\'"=]+|\\s+)((https?|ftp):\\/\\/[!#$&-;=?-\[\]_\\p{L}~](?:[!#$&-;=?-\[\]_\\p{L}~]+|%[0-9\\p{L}]{2})*)/u';
public function parse(string $table, string $field, int $uid, string $content, string $structurePath = ''): SoftReferenceParserResult
{
$elements = [];
$modifiedContent = ' ' . $content . ' ';
// Find all URLs using preg_match_all
$matches = [];
if (preg_match_all(self::REGEXP, $modifiedContent, $matches, PREG_SET_ORDER)) {
// Process each match
foreach ($matches as $idx => $match) {
$prefix = $match[1];
$url = $match[2];
$tokenID = $this->makeTokenID((string)$idx);
$elements[$idx] = [];
$elements[$idx]['matchString'] = $url;
if (in_array('subst', $this->parameters, true)) {
// Replace the URL with a token in the content
$modifiedContent = str_replace($prefix . $url, $prefix . '{softref:' . $tokenID . '}', $modifiedContent);
$elements[$idx]['subst'] = [
'type' => 'string',
'tokenID' => $tokenID,
'tokenValue' => $url,
];
}
}
}
return SoftReferenceParserResult::create(
substr($modifiedContent, 1, -1),
$elements
);
}
}