1<?php
2
3/*
4 * This file is part of the TYPO3 CMS project.
5 *
6 * It is free software; you can redistribute it and/or modify it under
7 * the terms of the GNU General Public License, either version 2
8 * of the License, or any later version.
9 *
10 * For the full copyright and license information, please read the
11 * LICENSE.txt file that was distributed with this source code.
12 *
13 * The TYPO3 project - inspiring people to share!
14 */
15
16namespace TYPO3\CMS\Core\Database;
17
18use Psr\EventDispatcher\EventDispatcherInterface;
19use TYPO3\CMS\Backend\Utility\BackendUtility;
20use TYPO3\CMS\Core\DataHandling\Event\AppendLinkHandlerElementsEvent;
21use TYPO3\CMS\Core\Html\HtmlParser;
22use TYPO3\CMS\Core\LinkHandling\Exception\UnknownLinkHandlerException;
23use TYPO3\CMS\Core\LinkHandling\LinkService;
24use TYPO3\CMS\Core\Resource\File;
25use TYPO3\CMS\Core\Resource\FileInterface;
26use TYPO3\CMS\Core\SingletonInterface;
27use TYPO3\CMS\Core\Utility\GeneralUtility;
28use TYPO3\CMS\Core\Utility\MathUtility;
29use TYPO3\CMS\Frontend\Service\TypoLinkCodecService;
30
31/**
32 * Soft Reference processing class
33 * "Soft References" are references to database elements, files, email addresses, URls etc.
34 * which are found in-text in content. The <link [page_id]> tag from typical bodytext fields
35 * are an example of this.
36 * This class contains generic parsers for the most well-known types
37 * which are default for most TYPO3 installations. Soft References can also be userdefined.
38 * The Soft Reference parsers are used by the system to find these references and process them accordingly in import/export actions and copy operations.
39 *
40 * Example of usage
41 * Soft References:
42 * if ($conf['softref'] && (strong)$value !== ''))	{	// Check if a TCA configured field has softreferences defined (see TYPO3 Core API document)
43 * $softRefs = \TYPO3\CMS\Backend\Utility\BackendUtility::explodeSoftRefParserList($conf['softref']);		// Explode the list of softreferences/parameters
44 * if ($softRefs !== FALSE) { // If there are soft references
45 * foreach($softRefs as $spKey => $spParams)	{	// Traverse soft references
46 * $softRefObj = \TYPO3\CMS\Backend\Utility\BackendUtility::softRefParserObj($spKey);	// create / get object
47 * if (is_object($softRefObj))	{	// If there was an object returned...:
48 * $resultArray = $softRefObj->findRef($table, $field, $uid, $softRefValue, $spKey, $spParams);	// Do processing
49 *
50 * Result Array:
51 * The Result array should contain two keys: "content" and "elements".
52 * "content" is a string containing the input content but possibly with tokens inside.
53 * Tokens are strings like {softref:[tokenID]} which is a placeholder for a value extracted by a softref parser
54 * 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.
55 * "elements" is an array where the keys are insignificant, but the values are arrays with these keys:
56 * "matchString" => The value of the match. This is only for informational purposes to show what was found.
57 * "error"	=> An error message can be set here, like "file not found" etc.
58 * "subst" => array(	// If this array is found there MUST be a token in the output content as well!
59 * "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.
60 * "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.
61 * "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)
62 * "relFileName" => (for "file" type): Relative filename. May not necessarily exist. This could be noticed in the error key.
63 * "recordRef" => (for "db" type) : Reference to DB record on the form [table]:[uid]. May not necessarily exist.
64 * "title" => Title of element (for backend information)
65 * "description" => Description of element (for backend information)
66 * )
67 */
68/**
69 * Class for processing of the default soft reference types for CMS:
70 *
71 * - 'substitute' : A full field value targeted for manual substitution (for import /export features)
72 * - 'notify' : Just report if a value is found, nothing more.
73 * - 'images' : HTML <img> tags for RTE images
74 * - 'typolink' : references to page id or file, possibly with anchor/target, possibly commaseparated list.
75 * - 'typolink_tag' : As typolink, but searching for <link> tag to encapsulate it.
76 * - 'email' : Email highlight
77 * - 'url' : URL highlights (with a scheme)
78 */
79class SoftReferenceIndex implements SingletonInterface
80{
81    /**
82     * @var string
83     */
84    public $tokenID_basePrefix = '';
85
86    /**
87     * @var EventDispatcherInterface
88     */
89    protected $eventDispatcher;
90
91    /**
92     * @var int
93     */
94    private $referenceUid = 0;
95
96    /**
97     * @var string
98     */
99    private $referenceTable = '';
100
101    public function __construct(EventDispatcherInterface $eventDispatcher)
102    {
103        $this->eventDispatcher = $eventDispatcher;
104    }
105
106    /**
107     * Main function through which all processing happens
108     *
109     * @param string $table Database table name
110     * @param string $field Field name for which processing occurs
111     * @param int $uid UID of the record
112     * @param string $content The content/value of the field
113     * @param string $spKey The softlink parser key. This is only interesting if more than one parser is grouped in the same class. That is the case with this parser.
114     * @param array $spParams Parameters of the softlink parser. Basically this is the content inside optional []-brackets after the softref keys. Parameters are exploded by ";
115     * @param string $structurePath If running from inside a FlexForm structure, this is the path of the tag.
116     * @return array|bool|null Result array on positive matches, see description above. Otherwise FALSE or null
117     */
118    public function findRef($table, $field, $uid, $content, $spKey, $spParams, $structurePath = '')
119    {
120        $this->referenceUid = $uid;
121        $this->referenceTable = $table;
122        $this->tokenID_basePrefix = $table . ':' . $uid . ':' . $field . ':' . $structurePath . ':' . $spKey;
123        switch ($spKey) {
124            case 'notify':
125                // Simple notification
126                $resultArray = [
127                    'elements' => [
128                        [
129                            'matchString' => $content
130                        ]
131                    ]
132                ];
133                $retVal = $resultArray;
134                break;
135            case 'substitute':
136                $tokenID = $this->makeTokenID();
137                $resultArray = [
138                    'content' => '{softref:' . $tokenID . '}',
139                    'elements' => [
140                        [
141                            'matchString' => $content,
142                            'subst' => [
143                                'type' => 'string',
144                                'tokenID' => $tokenID,
145                                'tokenValue' => $content
146                            ]
147                        ]
148                    ]
149                ];
150                $retVal = $resultArray;
151                break;
152            case 'typolink':
153                $retVal = $this->findRef_typolink($content, $spParams);
154                break;
155            case 'typolink_tag':
156                $retVal = $this->findRef_typolink_tag($content);
157                break;
158            case 'ext_fileref':
159                $retVal = $this->findRef_extension_fileref($content);
160                break;
161            case 'email':
162                $retVal = $this->findRef_email($content, $spParams);
163                break;
164            case 'url':
165                $retVal = $this->findRef_url($content, $spParams);
166                break;
167            default:
168                $retVal = false;
169        }
170        $this->referenceUid = 0;
171        $this->referenceTable = '';
172        return $retVal;
173    }
174
175    /**
176     * TypoLink value processing.
177     * Will process input value as a TypoLink value.
178     *
179     * @param string $content The input content to analyze
180     * @param array $spParams Parameters set for the softref parser key in TCA/columns. value "linkList" will split the string by comma before processing.
181     * @return array|null Result array on positive matches, see description above. Otherwise null
182     * @see \TYPO3\CMS\Frontend\ContentObject::typolink()
183     * @see getTypoLinkParts()
184     */
185    public function findRef_typolink($content, $spParams)
186    {
187        // First, split the input string by a comma if the "linkList" parameter is set.
188        // 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.
189        if (is_array($spParams) && in_array('linkList', $spParams)) {
190            // Preserving whitespace on purpose.
191            $linkElement = explode(',', $content);
192        } else {
193            // If only one element, just set in this array to make it easy below.
194            $linkElement = [$content];
195        }
196        // Traverse the links now:
197        $elements = [];
198        foreach ($linkElement as $k => $typolinkValue) {
199            $tLP = $this->getTypoLinkParts($typolinkValue);
200            $linkElement[$k] = $this->setTypoLinkPartsElement($tLP, $elements, $typolinkValue, $k);
201        }
202        // Return output:
203        if (!empty($elements)) {
204            $resultArray = [
205                'content' => implode(',', $linkElement),
206                'elements' => $elements
207            ];
208            return $resultArray;
209        }
210
211        return null;
212    }
213
214    /**
215     * TypoLink tag processing.
216     * Will search for <link ...> and <a> tags in the content string and process any found.
217     *
218     * @param string $content The input content to analyze
219     * @return array|null Result array on positive matches, see description above. Otherwise null
220     * @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::typolink()
221     * @see getTypoLinkParts()
222     */
223    public function findRef_typolink_tag($content)
224    {
225        // Parse string for special TYPO3 <link> tag:
226        $htmlParser = GeneralUtility::makeInstance(HtmlParser::class);
227        $linkService = GeneralUtility::makeInstance(LinkService::class);
228        $linkTags = $htmlParser->splitTags('a', $content);
229        // Traverse result:
230        $elements = [];
231        foreach ($linkTags as $key => $foundValue) {
232            if ($key % 2) {
233                if (preg_match('/href="([^"]+)"/', $foundValue, $matches)) {
234                    try {
235                        $linkDetails = $linkService->resolve($matches[1]);
236                        if ($linkDetails['type'] === LinkService::TYPE_FILE && preg_match('/file\?uid=(\d+)/', $matches[1], $fileIdMatch)) {
237                            $token = $this->makeTokenID($key);
238                            $elements[$key]['matchString'] = $linkTags[$key];
239                            $linkTags[$key] = str_replace($matches[1], '{softref:' . $token . '}', $linkTags[$key]);
240                            $elements[$key]['subst'] = [
241                                'type' => 'db',
242                                'recordRef' => 'sys_file:' . $fileIdMatch[1],
243                                'tokenID' => $token,
244                                'tokenValue' => 'file:' . ($linkDetails['file'] instanceof File ? $linkDetails['file']->getUid() : $fileIdMatch[1])
245                            ];
246                        } elseif ($linkDetails['type'] === LinkService::TYPE_PAGE && preg_match('/page\?uid=(\d+)#?(\d+)?/', $matches[1], $pageAndAnchorMatches)) {
247                            $token = $this->makeTokenID($key);
248                            $content = '{softref:' . $token . '}';
249                            $elements[$key]['matchString'] = $linkTags[$key];
250                            $elements[$key]['subst'] = [
251                                'type' => 'db',
252                                'recordRef' => 'pages:' . $linkDetails['pageuid'],
253                                'tokenID' => $token,
254                                'tokenValue' => $linkDetails['pageuid']
255                            ];
256                            if (isset($pageAndAnchorMatches[2]) && $pageAndAnchorMatches[2] !== '') {
257                                // Anchor is assumed to point to a content elements:
258                                if (MathUtility::canBeInterpretedAsInteger($pageAndAnchorMatches[2])) {
259                                    // Initialize a new entry because we have a new relation:
260                                    $newTokenID = $this->makeTokenID('setTypoLinkPartsElement:anchor:' . $key);
261                                    $elements[$newTokenID . ':' . $key] = [];
262                                    $elements[$newTokenID . ':' . $key]['matchString'] = 'Anchor Content Element: ' . $pageAndAnchorMatches[2];
263                                    $content .= '#{softref:' . $newTokenID . '}';
264                                    $elements[$newTokenID . ':' . $key]['subst'] = [
265                                        'type' => 'db',
266                                        'recordRef' => 'tt_content:' . $pageAndAnchorMatches[2],
267                                        'tokenID' => $newTokenID,
268                                        'tokenValue' => $pageAndAnchorMatches[2]
269                                    ];
270                                } else {
271                                    // Anchor is a hardcoded string
272                                    $content .= '#' . $pageAndAnchorMatches[2];
273                                }
274                            }
275                            $linkTags[$key] = str_replace($matches[1], $content, $linkTags[$key]);
276                        } elseif ($linkDetails['type'] === LinkService::TYPE_URL) {
277                            $token = $this->makeTokenID($key);
278                            $elements[$key]['matchString'] = $linkTags[$key];
279                            $linkTags[$key] = str_replace($matches[1], '{softref:' . $token . '}', $linkTags[$key]);
280                            $elements[$key]['subst'] = [
281                                'type' => 'external',
282                                'tokenID' => $token,
283                                'tokenValue' => $linkDetails['url']
284                            ];
285                        } elseif ($linkDetails['type'] === LinkService::TYPE_EMAIL) {
286                            $token = $this->makeTokenID($key);
287                            $elements[$key]['matchString'] = $linkTags[$key];
288                            $linkTags[$key] = str_replace($matches[1], '{softref:' . $token . '}', $linkTags[$key]);
289                            $elements[$key]['subst'] = [
290                                'type' => 'string',
291                                'tokenID' => $token,
292                                'tokenValue' => $linkDetails['email']
293                            ];
294                        } elseif ($linkDetails['type'] === LinkService::TYPE_TELEPHONE) {
295                            $token = $this->makeTokenID($key);
296                            $elements[$key]['matchString'] = $linkTags[$key];
297                            $linkTags[$key] = str_replace($matches[1], '{softref:' . $token . '}', $linkTags[$key]);
298                            $elements[$key]['subst'] = [
299                                'type' => 'string',
300                                'tokenID' => $token,
301                                'tokenValue' => $linkDetails['telephone']
302                            ];
303                        }
304                    } catch (\Exception $e) {
305                        // skip invalid links
306                    }
307                }
308            }
309        }
310        // Return output:
311        if (!empty($elements)) {
312            $resultArray = [
313                'content' => implode('', $linkTags),
314                'elements' => $elements
315            ];
316            return $resultArray;
317        }
318
319        return null;
320    }
321
322    /**
323     * Finding email addresses in content and making them substitutable.
324     *
325     * @param string $content The input content to analyze
326     * @param array $spParams Parameters set for the softref parser key in TCA/columns
327     * @return array|null Result array on positive matches, see description above. Otherwise null
328     */
329    public function findRef_email($content, $spParams)
330    {
331        $elements = [];
332        // Email:
333        $parts = preg_split('/([^[:alnum:]]+)([A-Za-z0-9\\._-]+[@][A-Za-z0-9\\._-]+[\\.].[A-Za-z0-9]+)/', ' ' . $content . ' ', 10000, PREG_SPLIT_DELIM_CAPTURE);
334        foreach ($parts as $idx => $value) {
335            if ($idx % 3 == 2) {
336                $tokenID = $this->makeTokenID($idx);
337                $elements[$idx] = [];
338                $elements[$idx]['matchString'] = $value;
339                if (is_array($spParams) && in_array('subst', $spParams)) {
340                    $parts[$idx] = '{softref:' . $tokenID . '}';
341                    $elements[$idx]['subst'] = [
342                        'type' => 'string',
343                        'tokenID' => $tokenID,
344                        'tokenValue' => $value
345                    ];
346                }
347            }
348        }
349        // Return output:
350        if (!empty($elements)) {
351            $resultArray = [
352                'content' => substr(implode('', $parts), 1, -1),
353                'elements' => $elements
354            ];
355            return $resultArray;
356        }
357
358        return null;
359    }
360
361    /**
362     * Finding URLs in content
363     *
364     * @param string $content The input content to analyze
365     * @param array $spParams Parameters set for the softref parser key in TCA/columns
366     * @return array|null Result array on positive matches, see description above. Otherwise null
367     */
368    public function findRef_url($content, $spParams)
369    {
370        $elements = [];
371        // URLs
372        $parts = preg_split('/([^[:alnum:]"\']+)((https?|ftp):\\/\\/[^[:space:]"\'<>]*)([[:space:]])/', ' ' . $content . ' ', 10000, PREG_SPLIT_DELIM_CAPTURE);
373        foreach ($parts as $idx => $value) {
374            if ($idx % 5 == 3) {
375                unset($parts[$idx]);
376            }
377            if ($idx % 5 == 2) {
378                $tokenID = $this->makeTokenID($idx);
379                $elements[$idx] = [];
380                $elements[$idx]['matchString'] = $value;
381                if (is_array($spParams) && in_array('subst', $spParams)) {
382                    $parts[$idx] = '{softref:' . $tokenID . '}';
383                    $elements[$idx]['subst'] = [
384                        'type' => 'string',
385                        'tokenID' => $tokenID,
386                        'tokenValue' => $value
387                    ];
388                }
389            }
390        }
391        // Return output:
392        if (!empty($elements)) {
393            $resultArray = [
394                'content' => substr(implode('', $parts), 1, -1),
395                'elements' => $elements
396            ];
397            return $resultArray;
398        }
399
400        return null;
401    }
402
403    /**
404     * Finding reference to files from extensions in content, but only to notify about their existence. No substitution
405     *
406     * @param string $content The input content to analyze
407     * @return array|null Result array on positive matches, see description above. Otherwise null
408     */
409    public function findRef_extension_fileref($content)
410    {
411        $elements = [];
412        // Files starting with EXT:
413        $parts = preg_split('/([^[:alnum:]"\']+)(EXT:[[:alnum:]_]+\\/[^[:space:]"\',]*)/', ' ' . $content . ' ', 10000, PREG_SPLIT_DELIM_CAPTURE) ?: [];
414        foreach ($parts as $idx => $value) {
415            if ($idx % 3 == 2) {
416                $this->makeTokenID((string)$idx);
417                $elements[$idx] = [];
418                $elements[$idx]['matchString'] = $value;
419            }
420        }
421        // Return output:
422        if (!empty($elements)) {
423            $resultArray = [
424                'content' => substr(implode('', $parts), 1, -1),
425                'elements' => $elements
426            ];
427            return $resultArray;
428        }
429
430        return null;
431    }
432
433    /*************************
434     *
435     * Helper functions
436     *
437     *************************/
438
439    /**
440     * Analyze content as a TypoLink value and return an array with properties.
441     * TypoLinks format is: <link [typolink] [browser target] [css class] [title attribute] [additionalParams]>.
442     * See TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::typolink()
443     * The syntax of the [typolink] part is: [typolink] = [page id][,[type value]][#[anchor, if integer = tt_content uid]]
444     * The extraction is based on how \TYPO3\CMS\Frontend\ContentObject::typolink() behaves.
445     *
446     * @param string $typolinkValue TypoLink value.
447     * @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.
448     * @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::typolink()
449     * @see setTypoLinkPartsElement()
450     */
451    public function getTypoLinkParts($typolinkValue)
452    {
453        $finalTagParts = GeneralUtility::makeInstance(TypoLinkCodecService::class)->decode($typolinkValue);
454
455        $link_param = $finalTagParts['url'];
456        // we define various keys below, "url" might be misleading
457        unset($finalTagParts['url']);
458
459        if (stripos(rawurldecode(trim($link_param)), 'phar://') === 0) {
460            throw new \RuntimeException(
461                'phar scheme not allowed as soft reference target',
462                1530030672
463            );
464        }
465
466        $linkService = GeneralUtility::makeInstance(LinkService::class);
467        try {
468            $linkData = $linkService->resolve($link_param);
469            switch ($linkData['type']) {
470                case LinkService::TYPE_RECORD:
471                    $referencePageId = $this->referenceTable === 'pages'
472                        ? $this->referenceUid
473                        : (int)(BackendUtility::getRecord($this->referenceTable, $this->referenceUid)['pid'] ?? 0);
474                    if ($referencePageId) {
475                        $pageTsConfig = BackendUtility::getPagesTSconfig($referencePageId);
476                        $table = $pageTsConfig['TCEMAIN.']['linkHandler.'][$linkData['identifier'] . '.']['configuration.']['table'] ?? $linkData['identifier'];
477                    } else {
478                        // Backwards compatibility for the old behaviour, where the identifier was saved as the table.
479                        $table = $linkData['identifier'];
480                    }
481                    $finalTagParts['table'] = $table;
482                    $finalTagParts['uid'] = $linkData['uid'];
483                    break;
484                case LinkService::TYPE_PAGE:
485                    $linkData['pageuid'] = (int)$linkData['pageuid'];
486                    if (isset($linkData['pagetype'])) {
487                        $linkData['pagetype'] = (int)$linkData['pagetype'];
488                    }
489                    if (isset($linkData['fragment'])) {
490                        $finalTagParts['anchor'] = $linkData['fragment'];
491                    }
492                    break;
493                case LinkService::TYPE_FILE:
494                case LinkService::TYPE_UNKNOWN:
495                    if (isset($linkData['file'])) {
496                        $finalTagParts['type'] = LinkService::TYPE_FILE;
497                        $linkData['file'] = $linkData['file'] instanceof FileInterface ? $linkData['file']->getUid() : $linkData['file'];
498                    } else {
499                        $pU = parse_url($link_param);
500                        parse_str($pU['query'] ?? '', $query);
501                        if (isset($query['uid'])) {
502                            $finalTagParts['type'] = LinkService::TYPE_FILE;
503                            $finalTagParts['file'] = (int)$query['uid'];
504                        }
505                    }
506                    break;
507            }
508            return array_merge($finalTagParts, $linkData);
509        } catch (UnknownLinkHandlerException $e) {
510            // Cannot handle anything
511            return $finalTagParts;
512        }
513    }
514
515    /**
516     * Recompile a TypoLink value from the array of properties made with getTypoLinkParts() into an elements array
517     *
518     * @param array $tLP TypoLink properties
519     * @param array $elements Array of elements to be modified with substitution / information entries.
520     * @param string $content The content to process.
521     * @param int $idx Index value of the found element - user to make unique but stable tokenID
522     * @return string The input content, possibly containing tokens now according to the added substitution entries in $elements
523     * @see getTypoLinkParts()
524     */
525    public function setTypoLinkPartsElement($tLP, &$elements, $content, $idx)
526    {
527        // Initialize, set basic values. In any case a link will be shown
528        $tokenID = $this->makeTokenID('setTypoLinkPartsElement:' . $idx);
529        $elements[$tokenID . ':' . $idx] = [];
530        $elements[$tokenID . ':' . $idx]['matchString'] = $content;
531        // Based on link type, maybe do more:
532        switch ((string)$tLP['type']) {
533            case LinkService::TYPE_EMAIL:
534                // Mail addresses can be substituted manually:
535                $elements[$tokenID . ':' . $idx]['subst'] = [
536                    'type' => 'string',
537                    'tokenID' => $tokenID,
538                    'tokenValue' => $tLP['email']
539                ];
540                // Output content will be the token instead:
541                $content = '{softref:' . $tokenID . '}';
542                break;
543            case LinkService::TYPE_TELEPHONE:
544                // phone number can be substituted manually:
545                $elements[$tokenID . ':' . $idx]['subst'] = [
546                    'type' => 'string',
547                    'tokenID' => $tokenID,
548                    'tokenValue' => $tLP['telephone']
549                ];
550                // Output content will be the token instead:
551                $content = '{softref:' . $tokenID . '}';
552                break;
553            case LinkService::TYPE_URL:
554                // URLs can be substituted manually
555                $elements[$tokenID . ':' . $idx]['subst'] = [
556                    'type' => 'external',
557                    'tokenID' => $tokenID,
558                    'tokenValue' => $tLP['url']
559                ];
560                // Output content will be the token instead:
561                $content = '{softref:' . $tokenID . '}';
562                break;
563            case LinkService::TYPE_FOLDER:
564                // This is a link to a folder...
565                unset($elements[$tokenID . ':' . $idx]);
566                return $content;
567            case LinkService::TYPE_FILE:
568                // Process files referenced by their FAL uid
569                if (isset($tLP['file'])) {
570                    $fileId = $tLP['file'] instanceof FileInterface ? $tLP['file']->getUid() : $tLP['file'];
571                    // Token and substitute value
572                    $elements[$tokenID . ':' . $idx]['subst'] = [
573                        'type' => 'db',
574                        'recordRef' => 'sys_file:' . $fileId,
575                        'tokenID' => $tokenID,
576                        'tokenValue' => 'file:' . $fileId,
577                    ];
578                    // Output content will be the token instead:
579                    $content = '{softref:' . $tokenID . '}';
580                } elseif ($tLP['identifier']) {
581                    [$linkHandlerKeyword, $linkHandlerValue] = explode(':', trim($tLP['identifier']), 2);
582                    if (MathUtility::canBeInterpretedAsInteger($linkHandlerValue)) {
583                        // Token and substitute value
584                        $elements[$tokenID . ':' . $idx]['subst'] = [
585                            'type' => 'db',
586                            'recordRef' => 'sys_file:' . $linkHandlerValue,
587                            'tokenID' => $tokenID,
588                            'tokenValue' => $tLP['identifier'],
589                        ];
590                        // Output content will be the token instead:
591                        $content = '{softref:' . $tokenID . '}';
592                    } else {
593                        // This is a link to a folder...
594                        return $content;
595                    }
596                } else {
597                    return $content;
598                }
599                break;
600            case LinkService::TYPE_PAGE:
601                // Rebuild page reference typolink part:
602                $content = '';
603                // Set page id:
604                if ($tLP['pageuid']) {
605                    $content .= '{softref:' . $tokenID . '}';
606                    $elements[$tokenID . ':' . $idx]['subst'] = [
607                        'type' => 'db',
608                        'recordRef' => 'pages:' . $tLP['pageuid'],
609                        'tokenID' => $tokenID,
610                        'tokenValue' => $tLP['pageuid']
611                    ];
612                }
613                // Add type if applicable
614                if ((string)($tLP['pagetype'] ?? '') !== '') {
615                    $content .= ',' . $tLP['pagetype'];
616                }
617                // Add anchor if applicable
618                if ((string)($tLP['anchor'] ?? '') !== '') {
619                    // Anchor is assumed to point to a content elements:
620                    if (MathUtility::canBeInterpretedAsInteger($tLP['anchor'])) {
621                        // Initialize a new entry because we have a new relation:
622                        $newTokenID = $this->makeTokenID('setTypoLinkPartsElement:anchor:' . $idx);
623                        $elements[$newTokenID . ':' . $idx] = [];
624                        $elements[$newTokenID . ':' . $idx]['matchString'] = 'Anchor Content Element: ' . $tLP['anchor'];
625                        $content .= '#{softref:' . $newTokenID . '}';
626                        $elements[$newTokenID . ':' . $idx]['subst'] = [
627                            'type' => 'db',
628                            'recordRef' => 'tt_content:' . $tLP['anchor'],
629                            'tokenID' => $newTokenID,
630                            'tokenValue' => $tLP['anchor']
631                        ];
632                    } else {
633                        // Anchor is a hardcoded string
634                        $content .= '#' . $tLP['anchor'];
635                    }
636                }
637                break;
638            case LinkService::TYPE_RECORD:
639                $elements[$tokenID . ':' . $idx]['subst'] = [
640                    'type' => 'db',
641                    'recordRef' => $tLP['table'] . ':' . $tLP['uid'],
642                    'tokenID' => $tokenID,
643                    'tokenValue' => $content,
644                ];
645
646                $content = '{softref:' . $tokenID . '}';
647                break;
648            default:
649                $event = new AppendLinkHandlerElementsEvent($tLP, $content, $elements, $idx, $tokenID);
650                $this->eventDispatcher->dispatch($event);
651
652                $elements = $event->getElements();
653                $tLP = $event->getLinkParts();
654                $content = $event->getContent();
655
656                if (!$event->isResolved()) {
657                    $elements[$tokenID . ':' . $idx]['error'] = 'Couldn\'t decide typolink mode.';
658                    return $content;
659                }
660        }
661        // Finally, for all entries that was rebuild with tokens, add target, class, title and additionalParams in the end:
662        $tLP['url'] = $content;
663        $content = GeneralUtility::makeInstance(TypoLinkCodecService::class)->encode($tLP);
664
665        // Return rebuilt typolink value:
666        return $content;
667    }
668
669    /**
670     * Make Token ID for input index.
671     *
672     * @param string $index Suffix value.
673     * @return string Token ID
674     */
675    public function makeTokenID($index = '')
676    {
677        return md5($this->tokenID_basePrefix . ':' . $index);
678    }
679}
680