build(), it's needed // to have the original $linkParameter in the result array. Otherwise, places may break like e.g. the // DatabaseRecordLinkBuilder. Can we safely set this here directly and avoiding calls before build like // "$linkDetails['typoLinkParameter'] = $redirectTarget;" - e.g. like in the ext:redirects // TYPO3\CMS\Redirects\Service\RedirectService::resolveLinkDetailsFromLinkTarget() and other places. // Resolve FAL-api "file:UID-of-sys_file-record" and "file:combined-identifier" if (stripos($linkParameter, 'file:') === 0) { $result = $this->getFileOrFolderObjectFromMixedIdentifier(substr($linkParameter, 5)); } elseif (GeneralUtility::validEmail((string)parse_url($linkParameter, PHP_URL_PATH))) { $result['type'] = LinkService::TYPE_EMAIL; $result['email'] = $linkParameter; } elseif (str_starts_with($linkParameter, 'tel:')) { $result['type'] = LinkService::TYPE_TELEPHONE; $result['telephone'] = $linkParameter; } elseif (str_contains($linkParameter, ':')) { // Check for link-handler keyword [$linkHandlerKeyword, $linkHandlerValue] = explode(':', $linkParameter, 2); $result['type'] = strtolower(trim($linkHandlerKeyword)); if ($linkHandlerValue === '') { return [ 'type' => LinkService::TYPE_UNKNOWN, 'url' => $linkParameter, ]; } $result['url'] = $linkParameter; $result['value'] = $linkHandlerValue; if ($result['type'] === LinkService::TYPE_RECORD) { [$a['identifier'], $tableAndUid] = explode(':', $linkHandlerValue, 2); $tableAndUid = explode(':', $tableAndUid); if (count($tableAndUid) > 1) { $a['table'] = $tableAndUid[0]; $a['uid'] = (int)$tableAndUid[1]; } else { // this case can happen if there is the very old linkhandler syntax, which was only record:: $a['table'] = $a['identifier']; $a['uid'] = (int)$tableAndUid[0]; } $result = array_merge($result, $a); } } else { // special handling without a scheme $isLocalFile = 0; $fileChar = (int)strpos($linkParameter, '/'); $urlChar = (int)strpos($linkParameter, '.'); $isIdOrAlias = MathUtility::canBeInterpretedAsInteger($linkParameter); $matches = []; // capture old RTE links relative to TYPO3 Backend /typo3/ if (preg_match('#../(?:index\\.php)?\\?id=([^&]+)#', $linkParameter, $matches)) { $linkParameter = $matches[1]; $isIdOrAlias = true; } $containsSlash = false; if (!$isIdOrAlias) { // Detects if a file is found in site-root and if so it will be treated like a normal file. [$rootFileDat] = explode('?', rawurldecode($linkParameter)); $containsSlash = str_contains($rootFileDat, '/'); $pathInfo = pathinfo($rootFileDat); $fileExtension = strtolower($pathInfo['extension'] ?? ''); if (!$containsSlash && trim($rootFileDat) && ( @is_file(Environment::getPublicPath() . '/' . $rootFileDat) || $fileExtension === 'php' || $fileExtension === 'html' || $fileExtension === 'htm' ) ) { $isLocalFile = 1; } elseif ($containsSlash) { // Adding this so realurl directories are linked right (non-existing). $isLocalFile = 2; } } // url (external): If doubleSlash or if a '.' comes before a '/'. if (!$isIdOrAlias && $isLocalFile !== 1 && $urlChar && (!$containsSlash || $urlChar < $fileChar)) { $result['type'] = LinkService::TYPE_URL; $result['url'] = UrlLinkHandler::getDefaultScheme() . '://' . $linkParameter; // file (internal) or folder } elseif ($containsSlash || $isLocalFile) { $result = $this->getFileOrFolderObjectFromMixedIdentifier($linkParameter); } else { // Integer or alias (alias is without slashes or periods or commas, that is // 'nospace,alphanum_x,lower,unique' according to definition in $GLOBALS['TCA']!) $result = $this->resolvePageRelatedParameters($linkParameter); } } return $result; } /** * Internal method to do some magic to get a page parts, additional params, fragment / section hash * * @param string $data the input variable, can be "mypage,23" with fragments, keys * * @return array the result array with the page type set */ protected function resolvePageRelatedParameters(string $data): array { $result = ['type' => LinkService::TYPE_PAGE]; if (str_contains($data, '#')) { [$data, $result['fragment']] = explode('#', $data, 2); } // check for additional parameters if (str_contains($data, '?')) { [$data, $result['parameters']] = explode('?', $data, 2); } elseif (str_contains($data, '&')) { [$data, $result['parameters']] = explode('&', $data, 2); } $data = rtrim($data, ','); if (empty($data)) { $result['pageuid'] = 'current'; } elseif ($data[0] === '#') { $result['pageuid'] = 'current'; $result['fragment'] = substr($data, 1); } elseif (str_contains($data, ',')) { [$result['pageuid'], $result['pagetype']] = explode(',', $data, 2); } elseif (str_contains($data, '/')) { $data = explode('/', trim($data, '/')); $result['pageuid'] = array_shift($data); foreach ($data as $k => $item) { if ((int)$data[$k] % 2 === 0 && !empty($data[$k + 1])) { // @todo: revisit. Looks fishy and has no coverage? $result['page' . $data[$k]] = $data[$k + 1]; } } } else { $result['pageuid'] = $data; } if (MathUtility::canBeInterpretedAsInteger($result['pageuid'])) { $result['pageuid'] = (int)$result['pageuid']; } return $result; } /** * Internal method that fetches a file or folder object based on the file or folder combined identifier * * @param string $mixedIdentifier can be something like "2" (file uid), "fileadmin/i/like.png" or "2:/myidentifier/" * * @return array the result with the type (file or folder) set */ protected function getFileOrFolderObjectFromMixedIdentifier(string $mixedIdentifier): array { $result = []; try { $fileIdentifier = $mixedIdentifier; $fragment = null; if (str_contains($fileIdentifier, '#')) { [$fileIdentifier, $fragment] = explode('#', $fileIdentifier, 2); } try { $fileOrFolderObject = $this->getResourceFactory()->retrieveFileOrFolderObject($fileIdentifier); } catch (ResourceException $e) { $fileOrFolderObject = null; } // Links to a file/folder in the main TYPO3 directory should not be considered as file links, but an external link if ($fileOrFolderObject instanceof ResourceInterface && $fileOrFolderObject->getStorage()->isFallbackStorage()) { return [ 'type' => LinkService::TYPE_URL, 'url' => $mixedIdentifier, ]; } // Link to a folder or file if ($fileOrFolderObject instanceof File) { $result['type'] = LinkService::TYPE_FILE; $result['file'] = $fileOrFolderObject; if ($fragment) { $result['fragment'] = $fragment; } } elseif ($fileOrFolderObject instanceof Folder) { $result['type'] = LinkService::TYPE_FOLDER; $result['folder'] = $fileOrFolderObject; if ($fragment) { $result['fragment'] = $fragment; } } elseif (str_starts_with($mixedIdentifier, '/')) { $result['type'] = LinkService::TYPE_URL; $result['url'] = $mixedIdentifier; } else { $result['type'] = LinkService::TYPE_UNKNOWN; $result['file'] = $mixedIdentifier; } } catch (\RuntimeException $e) { // Element wasn't found $result['type'] = LinkService::TYPE_UNKNOWN; $result['file'] = $mixedIdentifier; } return $result; } /** * Initializes the resource factory (only once) */ protected function getResourceFactory(): ResourceFactory { if (!$this->resourceFactory) { $this->resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); } return $this->resourceFactory; } }