Open kitsunet opened 11 months ago
@rolandschuetz I created this after looking into it. It could be done via AOP but it's quite dirty. I guess a workaround for older installations might be a command tool that converts resource redirects from the database storage to ones that work with the cloud storage but no "live" integration. Could potentially be done in a cron job then. But I would focus to create this infrastructure for Neos 9.0 now.
Our workaround: Maybe this helps someone in a similar situation. So as an intermediate solution for our customers on Neos 8 we do not add final uris to assets in the content anymore, instead we create links to a custom controller, that then redirects to our current asset link.
The code to achieve this:
DistributionPackages/CodeQ.Site/Classes/Aop/ConvertUrisImplementationAspect.php
<?php
namespace CodeQ\Site\Aop;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Aop\JoinPointInterface;
/**
* @Flow\Aspect
*/
class ConvertUrisImplementationAspect
{
/**
* Adds target="_blank" if its an asset permalink
*
* @Flow\Around("method(Neos\Neos\Fusion\ConvertUrisImplementation->replaceLinkTargets())")
* @param JoinPointInterface $joinPoint
* @return string
*/
public function aroundReplaceLinkTargets(JoinPointInterface $joinPoint): string
{
$processedContent = $joinPoint->getAdviceChain()->proceed($joinPoint);
return \preg_replace_callback(
'~<a\s+.*?href="(.*?)/_asset/(.*?)".*?>~i',
static function ($matches) {
[$linkText] = $matches;
return self::setAttribute('target', '_blank', $linkText);
},
$processedContent
);
}
/**
* Taken from \Neos\Neos\Fusion\ConvertUrisImplementation
*
* Set or add value to the a attribute
*
* @param string $attribute The attribute, ('target' or 'rel')
* @param string $value The value of the attribute to add
* @param string $content The content to parse
* @return string
*/
private static function setAttribute(string $attribute, string $value, string $content): string
{
// The attribute is already set
if (\preg_match_all('~\s+' . $attribute . '="(.*?)~i', $content, $matches)) {
// If the attribute is target or the value is already set, leave the attribute as it is
if ($attribute === 'target' || \preg_match('~' . $attribute . '=".*?' . $value . '.*?"~i', $content)) {
return $content;
}
// Add the attribute to the list
return \preg_replace('/' . $attribute . '="(.*?)"/', sprintf('%s="$1 %s"', $attribute, $value), $content);
}
// Add the missing attribute with the value
return \str_replace('<a', sprintf('<a %s="%s"', $attribute, $value), $content);
}
}
DistributionPackages/CodeQ.Site/Classes/Aop/LinkingServiceAspect.php
<?php
namespace CodeQ\Site\Aop;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Aop\JoinPointInterface;
use Neos\Flow\Http\Exception;
use Neos\Flow\Log\Utility\LogEnvironment;
use Neos\Flow\Mvc\ActionRequestFactory;
use Neos\Flow\Mvc\Exception\InvalidActionNameException;
use Neos\Flow\Mvc\Exception\InvalidArgumentNameException;
use Neos\Flow\Mvc\Exception\InvalidArgumentTypeException;
use Neos\Flow\Mvc\Exception\InvalidControllerNameException;
use Neos\Flow\Mvc\Routing\Exception\MissingActionNameException;
use Neos\Flow\Mvc\Routing\UriBuilder;
use Neos\Http\Factories\ServerRequestFactory;
use Neos\Http\Factories\UriFactory;
use Neos\Neos\Service\LinkingService;
use Psr\Http\Message\UriInterface;
use Psr\Log\LoggerInterface;
/**
* @Flow\Aspect
*/
class LinkingServiceAspect
{
/**
* @param JoinPointInterface $joinPoint
* @return string|null
*
* @Flow\Around("method(Neos\Neos\Service\LinkingService->resolveAssetUri())")
*/
public function aroundResolveAssetUri(JoinPointInterface $joinPoint)
{
/** @var string|UriInterface $uri */
$uri = $joinPoint->getMethodArgument('uri');
/** @var LinkingService $linkingService */
$linkingService = $joinPoint->getProxy();
$targetObject = $linkingService->convertUriToObject($uri);
if ($targetObject === null) {
// error case is handled by the original method
return $joinPoint->getAdviceChain()->proceed($joinPoint);
}
try {
$uriFactory = new UriFactory();
$httpRequest = (new ServerRequestFactory($uriFactory))->createServerRequest('GET', '');
$request = (new ActionRequestFactory())->createActionRequest($httpRequest);
$uriBuilder = new UriBuilder();
$uriBuilder->setRequest($request);
return $uriBuilder->uriFor('index', ['asset' => $targetObject], 'AssetProxy', 'CodeQ.Site');
} catch (Exception|MissingActionNameException|InvalidArgumentNameException|InvalidActionNameException|InvalidArgumentTypeException|InvalidControllerNameException $e) {
}
return $joinPoint->getAdviceChain()->proceed($joinPoint);
}
}
DistributionPackages/CodeQ.Site/Classes/Controller/AssetProxyController.php
<?php
namespace CodeQ\Site\Controller;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Mvc\Controller\ActionController;
use Neos\Flow\ResourceManagement\ResourceManager;
use Neos\Media\Domain\Model\Asset;
class AssetProxyController extends ActionController
{
/**
* @Flow\Inject
* @var ResourceManager
*/
protected $resourceManager;
public function indexAction(Asset $asset): void
{
$publicUri = $this->resourceManager->getPublicPersistentResourceUri($asset->getResource());
// The codes 200, 301 and 302 are cached by the Nginx proxy
$this->redirectToUri($publicUri, 0, 302);
}
}
DistributionPackages/CodeQ.Site/Configuration/Policy.yaml
privilegeTargets:
'Neos\Flow\Security\Authorization\Privilege\Method\MethodPrivilege':
'CodeQ.Site:AssetProxyController':
label: 'Can access all AssetProxyController actions'
matcher: 'within(CodeQ\Site\Controller\AssetProxyController)'
roles:
'Neos.Flow:Everybody':
privileges:
- privilegeTarget: 'CodeQ.Site:AssetProxyController'
permission: GRANT
DistributionPackages/CodeQ.Site/Configuration/Routes.yaml
roles:
'Neos.Flow:Everybody':
privileges:
- privilegeTarget: 'CodeQ.Site:FeedbackController.submit'
permission: GRANT
- privilegeTarget: 'CodeQ.Site:VertiGis.all'
permission: GRANT
- privilegeTarget: 'CodeQ.Site:AssetProxyController'
permission: GRANT
Is there an existing issue for this?
Current Behavior
If you replace an asset resource via the media UI (or code) a check for the existence of RedirectStorageInterface and the respective option for asset redirects is made and an redirect entry for the two (old/new) asset Uris is created. This will only work on local systems with a file try and fallback approach on the webserver (so first the requested old resource uri is tried by the web server and if that fails - as the resource is no longer there - it gives the request to index.php and it eventually ends up in the redirect handler). This cannot work if your assets are stored in a cloud storage as there cannot be a file existence check and fallback from the webserver in this case. Also the implementation with a code (soft) dependency on RedirectStorageInterface could be cleaner. See
Neos\Media\Domain\Service\AssetService::replaceAssetResource
Expected Behavior
We can use asset redirects also for cloud storages or generally other configurations than the one described above.
My suggested plan is:
Add interface to Media (rough name idea
AssetRedirectHandlerInterface
) that provides a method to handle possible asset redirects Add Service to Media that selects a matching implementation of above interface for the ResourceTarget of "old asset resource" with a possible default fallback. Add interface implementation to Redirect (Storage) package that does about the same as now theNeos\Media\Domain\Service\AssetService::replaceAssetResource
does Cloud Target implementations can then add their own implementations.Steps To Reproduce
Environment
Anything else?
No response