Skip to content

Commit

Permalink
Merge pull request #787 from ezsystems/impl_EZP-20305_linkCrossSiteAc…
Browse files Browse the repository at this point in the history
…cess

Fix EZP-20305: Be able to link from one SiteAccess to another
  • Loading branch information
lolautruche committed Apr 15, 2014
2 parents 95e543c + baba021 commit 9805c5b
Show file tree
Hide file tree
Showing 48 changed files with 1,880 additions and 324 deletions.
2 changes: 2 additions & 0 deletions doc/bc/changes-5.3.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ Changes affecting version compatibility with former or future versions.
* In semantic configuration, `ezpublish.system.<siteAccessName>.session_name` is deprecated.
Use `ezpublish.system.<siteAccessName>.session.name` instead.

* `Regex\URI` and `Regex\Host` SiteAccess matchers are deprecated as reverse match is not possible with them (i.e. see `VersatileMatcher` interface).

* All Location based SortClauses, as well as PriorityCriterion and DepthCriterion has been
deprecated for content search use since their behaviour is unpredictable by design when
content has several locations. Instead use same functionality on new Location Search API.
Expand Down
67 changes: 67 additions & 0 deletions doc/specifications/siteaccess/cross_siteaccess_links.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Cross SiteAccess links

## Description
When using the *multisite* feature, it is sometimes useful to be able to **generate cross-links** between the different sites.
This allows to link different resources referenced in a same content repository, but configured independently with different
tree roots.

## Solution
To implement this feature, a new `VersatileMatcher` was added to allow SiteAccess matchers to be able to *reverse-match*.
All existing matchers implement this new interface, except the Regexp based matchers which have been deprecated.

The SiteAccess router has been added a `matchByName()` method to reflect this addition.

> **Note:** SiteAccess router public methods have also been extracted to a new interface, `SiteAccessRouterInterface`.
Abstract URLGenerator and `DefaultRouter` have been updated as well.

## Usage
*Twig example*
```jinja
{# Linking a location #}
<a href="{{ url( location, {"siteaccess": "some_siteaccess_name"} ) }}">{{ ez_content_name( content ) }}</a>
{# Linking a regular route #}
<a href="{{ url( "some_route_name", {"siteaccess": "some_siteaccess_name"} ) }}">Hello world!</a>
```

*PHP example*
```php
namespace Acme\TestBundle\Controller;

use eZ\Bundle\EzPublishCoreBundle\Controller as BaseController;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

class MyController extends BaseController
{
public function fooAction()
{
// ...

$location = $this->getRepository()->getLocationService()->loadLocation( 123 );
$locationUrl = $this->generateUrl(
$location,
array( 'siteaccess' => 'some_siteaccess_name' ),
UrlGeneratorInterface::ABSOLUTE_PATH
);

$regularRouteUrl = $this->generateUrl(
'some_route_name',
array( 'siteaccess' => 'some_siteaccess_name' ),
UrlGeneratorInterface::ABSOLUTE_PATH
);

// ...
}
}
```

> **Important**: As SiteAccess matchers can involve hosts and ports, it is **highly recommended** to generate cross-siteaccess
> links in the absolute form (e.g. using `url()` Twig helper).
## Troubleshooting
* The first matcher succeeding always wins, so be careful when using *catch-all* matchers like `URIElement`.
* If passed SiteAccess name is not a valid one, an `InvalidArgumentException` will be thrown.
* If matcher used to match provided SiteAccess doesn't implement `VersatileMatcher`, the link will be generated for the current SiteAccess.
* When using `Compound\LogicalAnd`, all inner matchers **must match**. If at least one matcher doesn't implement `VersatileMatcher`, it will fail.
* When using `Compound\LogicalOr`, the first inner matcher succeeding will win.
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ public function process( ContainerBuilder $container )
'setLegacyAwareRoutes',
array( '%ezpublish.default_router.legacy_aware_routes%' )
);
$defaultRouter->addMethodCall(
'setSiteAccessRouter',
array( new Reference( 'ezpublish.siteaccess_router' ) )
);
if ( !$defaultRouter->hasTag( 'router' ) )
{
$defaultRouter->addTag(
Expand Down
7 changes: 4 additions & 3 deletions eZ/Bundle/EzPublishCoreBundle/Resources/config/routing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ services:
abstract: true
calls:
- [setRequestContext, [@router.request_context]]
- [setSiteAccess, [@?ezpublish.siteaccess=]]
- [setSiteAccessRouter, [@ezpublish.siteaccess_router]]
- [setLogger, [@?logger]]

ezpublish.urlalias_router:
class: %ezpublish.urlalias_router.class%
Expand All @@ -49,10 +52,8 @@ services:

ezpublish.urlalias_generator:
class: %ezpublish.urlalias_generator.class%
arguments: [@ezpublish.api.repository, @router.default, @?logger]
arguments: [@ezpublish.api.repository, @router.default]
parent: ezpublish.url_generator.base
calls:
- [setSiteAccess, [@ezpublish.siteaccess]]

ezpublish.siteaccess.matcher_builder:
class: %ezpublish.siteaccess.matcher_builder.class%
Expand Down
80 changes: 77 additions & 3 deletions eZ/Bundle/EzPublishCoreBundle/Routing/DefaultRouter.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
namespace eZ\Bundle\EzPublishCoreBundle\Routing;

use eZ\Publish\Core\MVC\ConfigResolverInterface;
use eZ\Publish\Core\MVC\Symfony\Routing\SimplifiedRequest;
use eZ\Publish\Core\MVC\Symfony\SiteAccess;
use eZ\Publish\Core\MVC\Symfony\SiteAccess\SiteAccessAware;
use eZ\Publish\Core\MVC\Symfony\SiteAccess\SiteAccessRouterInterface;
use eZ\Publish\Core\MVC\Symfony\SiteAccess\URILexer;
use Symfony\Bundle\FrameworkBundle\Routing\Router;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
Expand All @@ -37,6 +39,11 @@ class DefaultRouter extends Router implements RequestMatcherInterface, SiteAcces
*/
protected $configResolver;

/**
* @var \eZ\Publish\Core\MVC\Symfony\SiteAccess\SiteAccessRouterInterface
*/
protected $siteAccessRouter;

public function setConfigResolver( ConfigResolverInterface $configResolver )
{
$this->configResolver = $configResolver;
Expand Down Expand Up @@ -68,6 +75,14 @@ public function setLegacyAwareRoutes( array $routes )
$this->legacyAwareRoutes = $routes;
}

/**
* @param \eZ\Publish\Core\MVC\Symfony\SiteAccess\SiteAccessRouterInterface $siteAccessRouter
*/
public function setSiteAccessRouter( SiteAccessRouterInterface $siteAccessRouter )
{
$this->siteAccessRouter = $siteAccessRouter;
}

/**
* @param \Symfony\Component\HttpFoundation\Request $request The request to match
*
Expand Down Expand Up @@ -101,10 +116,34 @@ public function matchRequest( Request $request )

public function generate( $name, $parameters = array(), $referenceType = self::ABSOLUTE_PATH )
{
$siteAccess = $this->siteAccess;
$originalContext = $context = $this->getContext();
$isSiteAccessAware = $this->isSiteAccessAwareRoute( $name );

// Retrieving the appropriate SiteAccess to generate the link for.
if ( isset( $parameters['siteaccess'] ) && $isSiteAccessAware )
{
$siteAccess = $this->siteAccessRouter->matchByName( $parameters['siteaccess'] );
if ( $siteAccess instanceof SiteAccess && $siteAccess->matcher instanceof SiteAccess\VersatileMatcher )
{
// Switch request context for link generation.
$context = $this->getContextBySimplifiedRequest( $siteAccess->matcher->getRequest() );
$this->setContext( $context );
}
else if ( $this->logger )
{
$siteAccess = $this->siteAccess;
$this->logger->notice( "Could not generate a link using provided 'siteaccess' parameter: {$parameters['siteaccess']}. Generating using current context." );
}

unset( $parameters['siteaccess'] );
}

$url = parent::generate( $name, $parameters, $referenceType );
if ( $this->isSiteAccessAwareRoute( $name ) && isset( $this->siteAccess ) && $this->siteAccess->matcher instanceof URILexer )

// Now putting back SiteAccess URI if needed.
if ( $isSiteAccessAware && $siteAccess && $siteAccess->matcher instanceof URILexer )
{
$context = $this->getContext();
if ( $referenceType == self::ABSOLUTE_URL || $referenceType == self::NETWORK_PATH )
{
$scheme = $context->getScheme();
Expand All @@ -126,9 +165,11 @@ public function generate( $name, $parameters = array(), $referenceType = self::A
}

$linkUri = $base ? substr( $url, strpos( $url, $base ) + strlen( $base ) ) : $url;
$url = str_replace( $linkUri, $this->siteAccess->matcher->analyseLink( $linkUri ), $url );
$url = str_replace( $linkUri, $siteAccess->matcher->analyseLink( $linkUri ), $url );
}

// Switch back to original context, for next links generation.
$this->setContext( $originalContext );
return $url;
}

Expand Down Expand Up @@ -172,4 +213,37 @@ protected function isLegacyAwareRoute( $routeName )

return false;
}

/**
* Merges context from $simplifiedRequest into a clone of the current context.
*
* @param SimplifiedRequest $simplifiedRequest
*
* @return \Symfony\Component\Routing\RequestContext
*/
public function getContextBySimplifiedRequest( SimplifiedRequest $simplifiedRequest )
{
$context = clone $this->context;
if ( $simplifiedRequest->scheme )
{
$context->setScheme( $simplifiedRequest->scheme );
}

if ( $simplifiedRequest->port )
{
$context->setHttpPort( $simplifiedRequest->port );
}

if ( $simplifiedRequest->host )
{
$context->setHost( $simplifiedRequest->host );
}

if ( $simplifiedRequest->pathinfo )
{
$context->setPathInfo( $simplifiedRequest->pathinfo );
}

return $context;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ public function testAddRouterWithDefaultRouter( $declaredPriority, $expectedPrio
'setLegacyAwareRoutes',
array( '%ezpublish.default_router.legacy_aware_routes%' )
);
$this->assertContainerBuilderHasServiceDefinitionWithMethodCall(
'router.default',
'setSiteAccessRouter',
array( new Reference( 'ezpublish.siteaccess_router' ) )
);
$this->assertContainerBuilderHasServiceDefinitionWithMethodCall(
'ezpublish.chain_router',
'add',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
namespace eZ\Bundle\EzPublishCoreBundle\Tests\Routing;

use eZ\Bundle\EzPublishCoreBundle\Routing\DefaultRouter;
use eZ\Publish\Core\MVC\Symfony\Routing\SimplifiedRequest;
use eZ\Publish\Core\MVC\Symfony\SiteAccess;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\RequestContext;
use ReflectionObject;

class DefaultRouterTest extends \PHPUnit_Framework_TestCase
{
Expand All @@ -26,11 +28,17 @@ class DefaultRouterTest extends \PHPUnit_Framework_TestCase
*/
private $configResolver;

/**
* @var \Symfony\Component\Routing\RequestContext
*/
private $requestContext;

protected function setUp()
{
parent::setUp();
$this->container = $this->getMock( 'Symfony\\Component\\DependencyInjection\\ContainerInterface' );
$this->configResolver = $this->getMock( 'eZ\\Publish\\Core\\MVC\\ConfigResolverInterface' );
$this->requestContext = new RequestContext();
}

/**
Expand All @@ -40,9 +48,10 @@ protected function setUp()
*/
private function generateRouter( array $mockedMethods = array() )
{
/** @var \PHPUnit_Framework_MockObject_MockObject|DefaultRouter $router */
$router = $this
->getMockBuilder( 'eZ\\Bundle\\EzPublishCoreBundle\\Routing\\DefaultRouter' )
->setConstructorArgs( array( $this->container, 'foo' ) )
->setConstructorArgs( array( $this->container, 'foo', array(), $this->requestContext ) )
->setMethods( array_merge( $mockedMethods ) )
->getMock();
$router->setConfigResolver( $this->configResolver );
Expand Down Expand Up @@ -266,4 +275,58 @@ public function providerGenerateWithSiteAccess()
array( '/foo/bar/baz', '/foo/bar/baz', '/foo/bar/baz', 'test_siteaccess', true, false, '_dontwantsiteaccess' ),
);
}

public function testGenerateReverseSiteAccessMatch()
{
$routeName = 'some_route_name';
$urlGenerated = 'http://phoenix-rises.fm/foo/bar';

$siteAccessName = 'foo_test';
$siteAccessRouter = $this->getMock( 'eZ\Publish\Core\MVC\Symfony\SiteAccess\SiteAccessRouterInterface' );
$versatileMatcher = $this->getMock( 'eZ\Publish\Core\MVC\Symfony\SiteAccess\VersatileMatcher' );
$simplifiedRequest = new SimplifiedRequest(
array(
'host' => 'phoenix-rises.fm',
'scheme' => 'http'
)
);
$versatileMatcher
->expects( $this->once() )
->method( 'getRequest' )
->will( $this->returnValue( $simplifiedRequest ) );
$siteAccessRouter
->expects( $this->once() )
->method( 'matchByName' )
->with( $siteAccessName )
->will( $this->returnValue( new SiteAccess( $siteAccessName, 'foo', $versatileMatcher ) ) );

$generator = $this->getMock( 'Symfony\Component\Routing\Generator\UrlGeneratorInterface' );
$generator
->expects( $this->at( 0 ) )
->method( 'setContext' )
->with( $this->isInstanceOf( 'Symfony\Component\Routing\RequestContext' ) );
$generator
->expects( $this->at( 1 ) )
->method( 'generate' )
->with( $routeName )
->will( $this->returnValue( $urlGenerated ) );
$generator
->expects( $this->at( 2 ) )
->method( 'setContext' )
->with( $this->requestContext );

$router = new DefaultRouter( $this->container, 'foo', array(), $this->requestContext );
$router->setConfigResolver( $this->configResolver );
$router->setSiteAccess( new SiteAccess( 'test', 'test', $this->getMock( 'eZ\Publish\Core\MVC\Symfony\SiteAccess\Matcher' ) ) );
$router->setSiteAccessRouter( $siteAccessRouter );
$refRouter = new ReflectionObject( $router );
$refGenerator = $refRouter->getProperty( 'generator' );
$refGenerator->setAccessible( true );
$refGenerator->setValue( $router, $generator );

$this->assertSame(
$urlGenerated,
$router->generate( $routeName, array( 'siteaccess' => $siteAccessName ), DefaultRouter::ABSOLUTE_PATH )
);
}
}
2 changes: 0 additions & 2 deletions eZ/Bundle/EzPublishLegacyBundle/Resources/config/services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,6 @@ services:
class: %ezpublish_legacy.url_generator.class%
arguments: [@ezpublish_legacy.kernel]
parent: ezpublish.url_generator.base
calls:
- [setSiteAccess, [@ezpublish.siteaccess]]

ezpublish_legacy.siteaccess_mapper:
class: %ezpublish_legacy.siteaccess_mapper.class%
Expand Down
Loading

0 comments on commit 9805c5b

Please sign in to comment.