TYPO3 CMS  TYPO3_8-7
SoftReferenceIndex.php
Go to the documentation of this file.
1 <?php
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 
23 
73 {
77  public $tokenID_basePrefix = '';
78 
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 
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 
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 
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]['subst'] = [
274  'type' => 'db',
275  'recordRef' => 'sys_file:' . $fileIdMatch[1],
276  'tokenID' => $token,
277  'tokenValue' => 'file:' . ($linkDetails['file'] instanceof File ? $linkDetails['file']->getUid() : $fileIdMatch[1])
278  ];
279  } elseif ($linkDetails['type'] === LinkService::TYPE_PAGE && preg_match('/page\?uid=(\d+)#?(\d+)?/', $matches[1], $pageAndAnchorMatches)) {
280  $token = $this->makeTokenID($key);
281  $linkTags[$key] = str_replace($matches[1], '{softref:' . $token . '}', $linkTags[$key]);
282  $elements[$key]['subst'] = [
283  'type' => 'db',
284  'recordRef' => 'pages:' . $linkDetails['pageuid'] . (isset($pageAndAnchorMatches[2]) ? '#c' . $pageAndAnchorMatches[2] : ''),
285  'tokenID' => $token,
286  'tokenValue' => $linkDetails['pageuid'] . (isset($pageAndAnchorMatches[2]) ? '#c' . $pageAndAnchorMatches[2] : '')
287  ];
288  } elseif ($linkDetails['type'] === LinkService::TYPE_URL) {
289  $token = $this->makeTokenID($key);
290  $linkTags[$key] = str_replace($matches[1], '{softref:' . $token . '}', $linkTags[$key]);
291  $elements[$key]['subst'] = [
292  'type' => 'external',
293  'tokenID' => $token,
294  'tokenValue' => $linkDetails['url']
295  ];
296  }
297  } catch (\Exception $e) {
298  // skip invalid links
299  }
300  } else {
301  // keep the legacy code for now
302  $typolinkValue = preg_replace('/<LINK[[:space:]]+/i', '', substr($foundValue, 0, -1));
303  $tLP = $this->getTypoLinkParts($typolinkValue);
304  $linkTags[$k] = '<LINK ' . $this->setTypoLinkPartsElement($tLP, $elements, $typolinkValue, $k) . '>';
305  }
306  }
307  }
308  // Return output:
309  if (!empty($elements)) {
310  $resultArray = [
311  'content' => implode('', $linkTags),
312  'elements' => $elements
313  ];
314  return $resultArray;
315  }
316  }
317 
325  public function findRef_email($content, $spParams)
326  {
327  // Email:
328  $parts = preg_split('/([^[:alnum:]]+)([A-Za-z0-9\\._-]+[@][A-Za-z0-9\\._-]+[\\.].[A-Za-z0-9]+)/', ' ' . $content . ' ', 10000, PREG_SPLIT_DELIM_CAPTURE);
329  foreach ($parts as $idx => $value) {
330  if ($idx % 3 == 2) {
331  $tokenID = $this->makeTokenID($idx);
332  $elements[$idx] = [];
333  $elements[$idx]['matchString'] = $value;
334  if (is_array($spParams) && in_array('subst', $spParams)) {
335  $parts[$idx] = '{softref:' . $tokenID . '}';
336  $elements[$idx]['subst'] = [
337  'type' => 'string',
338  'tokenID' => $tokenID,
339  'tokenValue' => $value
340  ];
341  }
342  }
343  }
344  // Return output:
345  if (!empty($elements)) {
346  $resultArray = [
347  'content' => substr(implode('', $parts), 1, -1),
348  'elements' => $elements
349  ];
350  return $resultArray;
351  }
352  }
353 
361  public function findRef_url($content, $spParams)
362  {
363  // URLs
364  $parts = preg_split('/([^[:alnum:]"\']+)((https?|ftp):\\/\\/[^[:space:]"\'<>]*)([[:space:]])/', ' ' . $content . ' ', 10000, PREG_SPLIT_DELIM_CAPTURE);
365  foreach ($parts as $idx => $value) {
366  if ($idx % 5 == 3) {
367  unset($parts[$idx]);
368  }
369  if ($idx % 5 == 2) {
370  $tokenID = $this->makeTokenID($idx);
371  $elements[$idx] = [];
372  $elements[$idx]['matchString'] = $value;
373  if (is_array($spParams) && in_array('subst', $spParams)) {
374  $parts[$idx] = '{softref:' . $tokenID . '}';
375  $elements[$idx]['subst'] = [
376  'type' => 'string',
377  'tokenID' => $tokenID,
378  'tokenValue' => $value
379  ];
380  }
381  }
382  }
383  // Return output:
384  if (!empty($elements)) {
385  $resultArray = [
386  'content' => substr(implode('', $parts), 1, -1),
387  'elements' => $elements
388  ];
389  return $resultArray;
390  }
391  }
392 
400  public function findRef_extension_fileref($content, $spParams)
401  {
402  // Files starting with EXT:
403  $parts = preg_split('/([^[:alnum:]"\']+)(EXT:[[:alnum:]_]+\\/[^[:space:]"\',]*)/', ' ' . $content . ' ', 10000, PREG_SPLIT_DELIM_CAPTURE);
404  foreach ($parts as $idx => $value) {
405  if ($idx % 3 == 2) {
406  $this->makeTokenID($idx);
407  $elements[$idx] = [];
408  $elements[$idx]['matchString'] = $value;
409  }
410  }
411  // Return output:
412  if (!empty($elements)) {
413  $resultArray = [
414  'content' => substr(implode('', $parts), 1, -1),
415  'elements' => $elements
416  ];
417  return $resultArray;
418  }
419  }
420 
421  /*************************
422  *
423  * Helper functions
424  *
425  *************************/
426 
438  public function getTypoLinkParts($typolinkValue)
439  {
440  $finalTagParts = GeneralUtility::makeInstance(TypoLinkCodecService::class)->decode($typolinkValue);
441 
442  $link_param = $finalTagParts['url'];
443  // we define various keys below, "url" might be misleading
444  unset($finalTagParts['url']);
445 
446  if (stripos(rawurldecode(trim($link_param)), 'phar://') === 0) {
447  throw new \RuntimeException(
448  'phar scheme not allowed as soft reference target',
449  1530030672
450  );
451  }
452 
453  // Parse URL:
454  $pU = @parse_url($link_param);
455 
456  // If it's a mail address:
457  if (strstr($link_param, '@') && !$pU['scheme']) {
458  $link_param = preg_replace('/^mailto:/i', '', $link_param);
459  $finalTagParts['LINK_TYPE'] = 'mailto';
460  $finalTagParts['url'] = trim($link_param);
461  return $finalTagParts;
462  }
463 
464  if ($pU['scheme'] === 't3' && $pU['host'] === LinkService::TYPE_RECORD) {
465  $finalTagParts['LINK_TYPE'] = LinkService::TYPE_RECORD;
466  $finalTagParts['url'] = $link_param;
467  }
468 
469  list($linkHandlerKeyword, $linkHandlerValue) = explode(':', trim($link_param), 2);
470 
471  // Dispatch available signal slots.
472  $linkHandlerFound = false;
473  list($linkHandlerFound, $finalTagParts) = $this->emitGetTypoLinkParts($linkHandlerFound, $finalTagParts, $linkHandlerKeyword, $linkHandlerValue);
474  if ($linkHandlerFound) {
475  return $finalTagParts;
476  }
477 
478  // Check for FAL link-handler keyword
479  if ($linkHandlerKeyword === 'file') {
480  $finalTagParts['LINK_TYPE'] = 'file';
481  $finalTagParts['identifier'] = trim($link_param);
482  return $finalTagParts;
483  }
484 
485  $isLocalFile = 0;
486  $fileChar = (int)strpos($link_param, '/');
487  $urlChar = (int)strpos($link_param, '.');
488 
489  // Detects if a file is found in site-root and if so it will be treated like a normal file.
490  list($rootFileDat) = explode('?', rawurldecode($link_param));
491  $containsSlash = strstr($rootFileDat, '/');
492  $rFD_fI = pathinfo($rootFileDat);
493  $fileExtension = strtolower($rFD_fI['extension']);
494  if (!$containsSlash && trim($rootFileDat) && (@is_file(PATH_site . $rootFileDat) || $fileExtension === 'php' || $fileExtension === 'html' || $fileExtension === 'htm')) {
495  $isLocalFile = 1;
496  } elseif ($containsSlash) {
497  // Adding this so realurl directories are linked right (non-existing).
498  $isLocalFile = 2;
499  }
500  if ($pU['scheme'] || ($isLocalFile != 1 && $urlChar && (!$containsSlash || $urlChar < $fileChar))) { // url (external): If doubleSlash or if a '.' comes before a '/'.
501  $finalTagParts['LINK_TYPE'] = 'url';
502  $finalTagParts['url'] = $link_param;
503  } elseif ($containsSlash || $isLocalFile) { // file (internal)
504  $splitLinkParam = explode('?', $link_param);
505  if (file_exists(rawurldecode($splitLinkParam[0])) || $isLocalFile) {
506  $finalTagParts['LINK_TYPE'] = 'file';
507  $finalTagParts['filepath'] = rawurldecode($splitLinkParam[0]);
508  $finalTagParts['query'] = $splitLinkParam[1];
509  }
510  } else {
511  // integer or alias (alias is without slashes or periods or commas, that is
512  // 'nospace,alphanum_x,lower,unique' according to definition in $GLOBALS['TCA']!)
513  $finalTagParts['LINK_TYPE'] = 'page';
514 
515  $link_params_parts = explode('#', $link_param);
516  // Link-data del
517  $link_param = trim($link_params_parts[0]);
518 
519  if ((string)$link_params_parts[1] !== '') {
520  $finalTagParts['anchor'] = trim($link_params_parts[1]);
521  }
522 
523  // Splitting the parameter by ',' and if the array counts more than 1 element it's a id/type/? pair
524  $pairParts = GeneralUtility::trimExplode(',', $link_param);
525  if (count($pairParts) > 1) {
526  $link_param = $pairParts[0];
527  $finalTagParts['type'] = $pairParts[1]; // Overruling 'type'
528  }
529 
530  // Checking if the id-parameter is an alias.
531  if ((string)$link_param !== '') {
532  if (!\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($link_param)) {
533  $finalTagParts['alias'] = $link_param;
534  $link_param = $this->getPageIdFromAlias($link_param);
535  }
536 
537  $finalTagParts['page_id'] = (int)$link_param;
538  }
539  }
540 
541  return $finalTagParts;
542  }
543 
554  public function setTypoLinkPartsElement($tLP, &$elements, $content, $idx)
555  {
556  // Initialize, set basic values. In any case a link will be shown
557  $tokenID = $this->makeTokenID('setTypoLinkPartsElement:' . $idx);
558  $elements[$tokenID . ':' . $idx] = [];
559  $elements[$tokenID . ':' . $idx]['matchString'] = $content;
560  // Based on link type, maybe do more:
561  switch ((string)$tLP['LINK_TYPE']) {
562  case 'mailto':
563 
564  case 'url':
565  // Mail addresses and URLs can be substituted manually:
566  $elements[$tokenID . ':' . $idx]['subst'] = [
567  'type' => 'string',
568  'tokenID' => $tokenID,
569  'tokenValue' => $tLP['url']
570  ];
571  // Output content will be the token instead:
572  $content = '{softref:' . $tokenID . '}';
573  break;
574  case 'file':
575  // Process files referenced by their FAL uid
576  if ($tLP['identifier']) {
577  list($linkHandlerKeyword, $linkHandlerValue) = explode(':', trim($tLP['identifier']), 2);
578  if (\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($linkHandlerValue)) {
579  // Token and substitute value
580  $elements[$tokenID . ':' . $idx]['subst'] = [
581  'type' => 'db',
582  'recordRef' => 'sys_file:' . $linkHandlerValue,
583  'tokenID' => $tokenID,
584  'tokenValue' => $tLP['identifier'],
585  ];
586  // Output content will be the token instead:
587  $content = '{softref:' . $tokenID . '}';
588  } else {
589  // This is a link to a folder...
590  return $content;
591  }
592  } else {
593  return $content;
594  }
595  break;
596  case 'page':
597  // Rebuild page reference typolink part:
598  $content = '';
599  // Set page id:
600  if ($tLP['page_id']) {
601  $content .= '{softref:' . $tokenID . '}';
602  $elements[$tokenID . ':' . $idx]['subst'] = [
603  'type' => 'db',
604  'recordRef' => 'pages:' . $tLP['page_id'],
605  'tokenID' => $tokenID,
606  'tokenValue' => $tLP['alias'] ? $tLP['alias'] : $tLP['page_id']
607  ];
608  }
609  // Add type if applicable
610  if ((string)$tLP['type'] !== '') {
611  $content .= ',' . $tLP['type'];
612  }
613  // Add anchor if applicable
614  if ((string)$tLP['anchor'] !== '') {
615  // Anchor is assumed to point to a content elements:
616  if (\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($tLP['anchor'])) {
617  // Initialize a new entry because we have a new relation:
618  $newTokenID = $this->makeTokenID('setTypoLinkPartsElement:anchor:' . $idx);
619  $elements[$newTokenID . ':' . $idx] = [];
620  $elements[$newTokenID . ':' . $idx]['matchString'] = 'Anchor Content Element: ' . $tLP['anchor'];
621  $content .= '#{softref:' . $newTokenID . '}';
622  $elements[$newTokenID . ':' . $idx]['subst'] = [
623  'type' => 'db',
624  'recordRef' => 'tt_content:' . $tLP['anchor'],
625  'tokenID' => $newTokenID,
626  'tokenValue' => $tLP['anchor']
627  ];
628  } else {
629  // Anchor is a hardcoded string
630  $content .= '#' . $tLP['type'];
631  }
632  }
633  break;
635  $elements[$tokenID . ':' . $idx]['subst'] = [
636  'type' => 'db',
637  'recordRef' => $tLP['table'] . ':' . $tLP['uid'],
638  'tokenID' => $tokenID,
639  'tokenValue' => $content,
640  ];
641 
642  $content = '{softref:' . $tokenID . '}';
643  break;
644  default:
645  $linkHandlerFound = false;
646  list($linkHandlerFound, $tLP, $content, $newElements) = $this->emitSetTypoLinkPartsElement($linkHandlerFound, $tLP, $content, $elements, $idx, $tokenID);
647  // We need to merge the array, otherwise we would loose the reference.
649 
650  if (!$linkHandlerFound) {
651  $elements[$tokenID . ':' . $idx]['error'] = 'Couldn\'t decide typolink mode.';
652  return $content;
653  }
654  }
655  // Finally, for all entries that was rebuild with tokens, add target, class, title and additionalParams in the end:
656  $tLP['url'] = $content;
657  $content = GeneralUtility::makeInstance(TypoLinkCodecService::class)->encode($tLP);
658 
659  // Return rebuilt typolink value:
660  return $content;
661  }
662 
669  public function getPageIdFromAlias($link_param)
670  {
671  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
672  $queryBuilder->getRestrictions()
673  ->removeAll()
674  ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
675  ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
676 
677  $pageUid = $queryBuilder->select('uid')
678  ->from('pages')
679  ->where(
680  $queryBuilder->expr()->eq('alias', $queryBuilder->createNamedParameter($link_param, \PDO::PARAM_STR))
681  )
682  ->setMaxResults(1)
683  ->execute()
684  ->fetchColumn(0);
685 
686  return (int)$pageUid;
687  }
688 
695  public function makeTokenID($index = '')
696  {
697  return md5($this->tokenID_basePrefix . ':' . $index);
698  }
699 
703  protected function getSignalSlotDispatcher()
704  {
705  return GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\SignalSlot\Dispatcher::class);
706  }
707 
715  protected function emitGetTypoLinkParts($linkHandlerFound, $finalTagParts, $linkHandlerKeyword, $linkHandlerValue)
716  {
717  return $this->getSignalSlotDispatcher()->dispatch(get_class($this), 'getTypoLinkParts', [$linkHandlerFound, $finalTagParts, $linkHandlerKeyword, $linkHandlerValue]);
718  }
719 
729  protected function emitSetTypoLinkPartsElement($linkHandlerFound, $tLP, $content, $elements, $idx, $tokenID)
730  {
731  return $this->getSignalSlotDispatcher()->dispatch(get_class($this), 'setTypoLinkPartsElement', [$linkHandlerFound, $tLP, $content, $elements, $idx, $tokenID, $this]);
732  }
733 }
static isFirstPartOfStr($str, $partStr)
static getFileAbsFileName($filename, $_=null, $_2=null)
static trimExplode($delim, $string, $removeEmptyValues=false, $limit=0)
static makeInstance($className,... $constructorArguments)
emitSetTypoLinkPartsElement($linkHandlerFound, $tLP, $content, $elements, $idx, $tokenID)
emitGetTypoLinkParts($linkHandlerFound, $finalTagParts, $linkHandlerKeyword, $linkHandlerValue)
findRef($table, $field, $uid, $content, $spKey, $spParams, $structurePath='')
static mergeRecursiveWithOverrule(array &$original, array $overrule, $addKeys=true, $includeEmptyValues=true, $enableUnsetFeature=true)
setTypoLinkPartsElement($tLP, &$elements, $content, $idx)