Skip to content

Commit

Permalink
[TASK] Add permalink tests and third party document permalink resolving
Browse files Browse the repository at this point in the history
  • Loading branch information
garvinhicking committed Nov 18, 2024
1 parent 03e4aa6 commit 24e6b7f
Show file tree
Hide file tree
Showing 20 changed files with 241,709 additions and 49 deletions.
2 changes: 1 addition & 1 deletion legacy_hook/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"psr/http-message": "^1.1",
"symfony/cache": "^5.4",
"symfony/finder": "^5.4",
"t3docs/typo3-version-handling": "^0.14.0"
"t3docs/typo3-version-handling": "^0.16.3"
},
"require-dev": {
"roave/security-advisories": "dev-latest",
Expand Down
18 changes: 9 additions & 9 deletions legacy_hook/composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

110 changes: 71 additions & 39 deletions legacy_hook/src/DocumentationLinker.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Contracts\Cache\ItemInterface;
use T3Docs\VersionHandling\DefaultInventories;
use T3Docs\VersionHandling\Typo3VersionMapping;

/**
* Redirect to a specify interlink target, example:
Expand All @@ -32,6 +33,9 @@
* linkToDocs.php?shortcode=t3coreapi:caching@main
* -> forwards to: https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ApiOverview/CachingFramework/Index.html#caching
*
* linkToDocs.php?shortcode=georgringer-news:start@main
* -> forwards to: https://docs.typo3.org/p/georgringer/news/en-us/Index.html#start
*
* Also, all TYPO3 core extensions can be resolved via "typo3-cms-XXX" prefixing:
* linkToDocs.php?shortcode=typo3-cms-seo:introduction@main
* -> forwards to: https://docs.typo3.org/c/typo3/cms-seo/main/en-us/Introduction/Index.html#introduction
Expand Down Expand Up @@ -80,9 +84,9 @@
* https://docs.typo3.org/c/typo3/cms-XXX/main/en-us/objects.inv.json
*
* Additional TYPO3 Manuals:
* https://docs.typo3.org/other/typo3/cms-XXX/main/en-us/objects.inv.json
* https://docs.typo3.org/other/typo3/XXX/main/en-us/objects.inv.json
*
* Public TYPO3 extensions (not within the scope of redirection at the moment):
* Public TYPO3 extensions (xxx/yyy is the composer packagist key):
* https://docs.typo3.org/p/XXX/YYY/main/en-us/objects.inv.json
*
* The logic is this:
Expand All @@ -99,6 +103,8 @@
* https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/objects.inv.json
* https://docs.typo3.org/other/t3docs/render-guides/objects.inv.json
* https://docs.typo3.org/p/georgringer/news/main/en-us/objects.inv.json
*
* @see Unit Test in legacy_hook/tests/Unit/PermalinksTest.php for examples.
*/
final readonly class DocumentationLinker
{
Expand All @@ -123,47 +129,52 @@ public function redirectToLink(): Response
$responseDescriber = $this->cache->get($cacheKey, function (ItemInterface $item) use ($url): ResponseDescriber {
$item->expiresAfter($this->cacheTime);

if (preg_match(
'/^' .
'([a-z0-9\-_]+):' . // $repository
'([a-z0-9\-_]+)' . // $index
'(@[a-z0-9\.-]+)?' . // $version
'$/imsU',
$url,
$matches)
) {
[, $repository, $index] = $matches;
$version = str_replace('@', '', $matches[3] ?? '') ?: 'main';
$entrypoint = $this->resolveEntryPoint($repository, $version);
$objectsContents = $this->getObjectsFile($entrypoint);

if ($objectsContents === '') {
return new ResponseDescriber(404, [], 'Invalid shortcode, no objects.inv.json found.');
}
return $this->resolvePermalink($url);
});

if (function_exists('json_validate') && !json_validate($objectsContents)) {
return new ResponseDescriber(404, [], 'Invalid shortcode, defective objects.inv.json.');
}
return new Response($responseDescriber->statusCode, $responseDescriber->headers, $responseDescriber->body);
}

$json = json_decode($objectsContents, true);
if (!is_array($json)) {
return new ResponseDescriber(404, [], 'Invalid shortcode, invalid objects.inv.json.');
}
public function resolvePermalink(string $url): ResponseDescriber
{
if (preg_match(
'/^' .
'([a-z0-9\-_]+):' . // $repository
'([a-z0-9\-_]+)' . // $index
'(@[a-z0-9\.-]+)?' . // $version
'$/imsU',
$url,
$matches)
) {
[, $repository, $index] = $matches;
$version = str_replace('@', '', $matches[3] ?? '') ?: 'main';
$entrypoint = $this->resolveEntryPoint($repository, $version);
$objectsContents = $this->getObjectsFile($entrypoint);

$link = $this->parseInventoryForIndex($index, $json);
if ($link === '') {
return new ResponseDescriber(404, [], 'Invalid shortcode, could not find index.');
}
if ($objectsContents === '') {
return new ResponseDescriber(404, [], 'Invalid shortcode, no objects.inv.json found.');
}

$forwardUrl = 'https://docs.typo3.org/' . $entrypoint . $link;
if (function_exists('json_validate') && !json_validate($objectsContents)) {
return new ResponseDescriber(404, [], 'Invalid shortcode, defective objects.inv.json.');
}

return new ResponseDescriber(307, ['Location' => $forwardUrl], 'Redirect to ' . $forwardUrl);
$json = json_decode($objectsContents, true);
if (!is_array($json)) {
return new ResponseDescriber(404, [], 'Invalid shortcode, invalid objects.inv.json.');
}

return new ResponseDescriber(404, [], 'Invalid shortcode.');
});
$link = $this->parseInventoryForIndex($index, $json);
if ($link === '') {
return new ResponseDescriber(404, [], 'Invalid shortcode, could not find index.');
}

return new Response($responseDescriber->statusCode, $responseDescriber->headers, $responseDescriber->body);
$forwardUrl = 'https://docs.typo3.org/' . $entrypoint . $link;

return new ResponseDescriber(307, ['Location' => $forwardUrl], 'Redirect to ' . $forwardUrl);
}

return new ResponseDescriber(404, [], 'Invalid shortcode.');
}

private function parseInventoryForIndex(string $index, array $json): string
Expand Down Expand Up @@ -206,21 +217,42 @@ private function parseInventoryForIndex(string $index, array $json): string
// Note: Currently hardcoded to 'en-us'
private function resolveEntryPoint(string $repository, string $version): string
{
$useCoreVersionResolving = true;
if (preg_match('/^typo3-(cms-[0-9a-z\-]+)$/i', $repository, $repositoryParts)) {
// CASE: TYPO3 core manuals
$entrypoint = 'https://docs.typo3.org/c/typo3/' . strtolower($repositoryParts[1]) . '/{typo3_version}/en-us/';
} elseif ($inventory = DefaultInventories::tryFrom($repository)) {
// CASE: Official TYPO3 Documentation with known inventories. Provides "{typo3_version}" internally
// (some inventories DO NOT have that and always go to 'main'!)
$entrypoint = $inventory->getUrl();
} else {
// $entrypoint = 'https://docs.typo3.org/p/' . strtolower($repository) . '/{typo3_version}/en-us/';
// The '/p/' notation is currently out-of-scope. Would need special handling of slashes.
$entrypoint = '';
// CASE: Third party documentation, based on composer-keys like https://docs.typo3.org/p/georgringer/news
// A permalink like https://docs.typo3.org/permalink/someVendor-some-extension/ is resolved to https://docs.typo3.org/p/somevendor/some-extension/
$entrypoint = 'https://docs.typo3.org/p/' . preg_replace('/-/', '/', strtolower($repository), 1) . '/{typo3_version}/en-us/';
$useCoreVersionResolving = false;
}

if ($useCoreVersionResolving) {
// Core Version resolving. Uses the composer package t3docs/typo3-version-handling which allows to
// interpret strings as "dev", "stable", "oldstable" and can map "12" to latest 12.4.x version.
// If not resolvable, uses the raw version number as lookup (for example "12.4"). An invalid version
// string like "99.9999" will later fail when searching for the directory.
$resolvedVersionEnum = Typo3VersionMapping::tryFrom($version);
if ($resolvedVersionEnum === null) {
$resolvedVersion = $version;
} else {
$resolvedVersion = $resolvedVersionEnum->getVersion();
}
} else {
// Third party extensions use their own versioning.
$resolvedVersion = $version;
}

// Do replacements.
// The 'https://docs.typo3.org/' notation comes from the external dependency,
// normalize it again here, strip any hostname component to only get a directory.
// @todo maybe make this prettier, security-wise this only allows domain names coming from DefaultInventories.
$entrypoint = str_replace('{typo3_version}', $version, $entrypoint);
$entrypoint = str_replace('{typo3_version}', $resolvedVersion, $entrypoint);
return preg_replace('/^.*:\/\/[^\/]+\//msU', '', $entrypoint);
}

Expand Down
Loading

0 comments on commit 24e6b7f

Please sign in to comment.