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