neos / neos-development-collection

The unified repository containing the Neos core packages, used for Neos development.
https://www.neos.io/
GNU General Public License v3.0
266 stars 223 forks source link

BUG: Asset redirects too unflexible for cloud environments #4815

Open kitsunet opened 11 months ago

kitsunet commented 11 months ago

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 the Neos\Media\Domain\Service\AssetService::replaceAssetResource does Cloud Target implementations can then add their own implementations.

Steps To Reproduce

Environment

doesn't matter / any

Anything else?

No response

kitsunet commented 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.

rolandschuetz commented 9 months ago

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