8000 URL manipulations as a Twig extension by fabpot · Pull Request #13264 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

URL manipulations as a Twig extension #13264

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 10, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/Symfony/Bridge/Twig/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

2.7.0
-----

* added an HttpFoundation extension (provides the `absolute_url` and the `relative_path` functions)

2.5.0
-----

Expand Down
109 changes: 109 additions & 0 deletions src/Symfony/Bridge/Twig/Extension/HttpFoundationExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bridge\Twig\Extension;

use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Request;

/**
* Twig extension for the Symfony HttpFoundation component.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class HttpFoundationExtension extends \Twig_Extension
{
private $requestStack;

public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}

/**
* {@inheritdoc}
*/
public function getFunctions()
{
return array(
new \Twig_SimpleFunction('absolute_url', array($this, 'generateAbsoluteUrl')),
new \Twig_SimpleFunction('relative_path', array($this, 'generateRelativePath')),
);
}

/**
* Returns the absolute URL for the given absolute or relative path.
*
* This method returns the path unchanged if no request is available.
*
* @param string $path The path
*
* @return string The absolute URL
*
* @see Request::getUriForPath()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request should be probably added to the use statements, or full namespace should be used. We've got two Request classes in Symfony. Similar case in the next docblock.

8000
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

*/
public function generateAbsoluteUrl($path)
{
if (false !== strpos($path, '://') || '//' === substr($path, 0, 2)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The case of URLs starting with // being unchanged is wrong IMO. If it starts with //, it is not an absolute URL. It is a scheme-relative URL (and scheme relative URLs don't work in non-webmail email clients for instance, as the email is not on an HTTP or HTTPS scheme).

return $path;
}

if (!$request = $this->requestStack->getMasterRequest()) {
return $path;
}

if (!$path || '/' !== $path[0]) {
$prefix = $request->getPathInfo();
$last = strlen($prefix) - 1;
if ($last !== $pos = strrpos($prefix, '/')) {
$prefix = substr($prefix, 0, $pos).'/';
}
8000
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic above should probably be moved to Request::getUriForPath() but that would be a BC break.


$path = $prefix.$path;
}

return $request->getUriForPath($path);
}

/**
* Returns a relative path based on the current Request.
*
* This method returns the path unchanged if no request is available.
*
* @param string $path The path
*
* @return string The relative path
*
* @see Request::getRelativeUriForPath()
*/
public function generateRelativePath($path)
{
if (false !== strpos($path, '://') || '//' === substr($path, 0, 2)) {
return $path;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not relative

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be better to throw an exception?

}

if (!$request = $this->requestStack->getMasterRequest()) {
return $path;
}

return $request->getRelativeUriForPath($path);
}

/**
* Returns the name of the extension.
*
* @return string The extension name
*/
public function getName()
{
return 'request';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bridge\Twig\Tests\Extension;

use Symfony\Bridge\Twig\Extension\HttpFoundationExtension;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Request;

class HttpFoundationExtensionTest extends \PHPUnit_Framework_TestCase
{
/**
* @dataProvider getGenerateAbsoluteUrlData()
*/
public function testGenerateAbsoluteUrl($expected, $path, $pathinfo)
{
$stack = new RequestStack();
$stack->push(Request::create($pathinfo));
$extension = new HttpFoundationExtension($stack);

$this->assertEquals($expected, $extension->generateAbsoluteUrl($path));
}

public function getGenerateAbsoluteUrlData()
{
return array(
array('http://localhost/foo.png', '/foo.png', '/foo/bar.html'),
array('http://localhost/foo/foo.png', 'foo.png', '/foo/bar.html'),
array('http://localhost/foo/foo.png', 'foo.png', '/foo/bar'),
array('http://localhost/foo/bar/foo.png', 'foo.png', '/foo/bar/'),

array('http://example.com/baz', 'http://example.com/baz', '/'),
array('https://example.com/baz', 'https://example.com/baz', '/'),
array('//example.com/baz', '//example.com/baz', '/'),
);
}

/**
* @dataProvider getGenerateRelativePathData()
*/
public function testGenerateRelativePath($expected, $path, $pathinfo)
{
if (!method_exists('Symfony\Component\HttpFoundation\Request', 'getRelativeUriForPath')) {
$this->markTestSkipped('Your version of Symfony HttpFoundation is too old.');
}

$stack = new RequestStack();
$stack->push(Request::create($pathinfo));
$extension = new HttpFoundationExtension($stack);

$this->assertEquals($expected, $extension->generateRelativePath($path));
}

public function getGenerateRelativePathData()
{
return array(
array('../foo.png', '/foo.png', '/foo/bar.html'),
array('../baz/foo.png', '/baz/foo.png', '/foo/bar.html'),
array('baz/foo.png', 'baz/foo.png', '/foo/bar.html'),

array('http://example.com/baz', 'http://example.com/baz', '/'),
array('https://example.com/baz', 'https://example.com/baz', '/'),
array('//example.com/baz', '//example.com/baz', '/'),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,9 @@ public function process(ContainerBuilder $container)
if ($container->has('fragment.handler')) {
$container->getDefinition('twig.extension.httpkernel')->addTag('twig.extension');
}

if ($container->has('request_stack')) {
$container->getDefinition('twig.extension.httpfoundation')->addTag('twig.extension');
}
}
}
3 changes: 3 additions & 0 deletions src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@
<argument type="service" id="fragment.handler" />
</service>

<service id="twig.extension.httpfoundation" class="Symfony\Bridge\Twig\Extension\HttpFoundationExtension" public="false">
<argument type="service" id="request_stack" />
</service>

<service id="twig.extension.form" class="%twig.extension.form.class%" public="false">
<argument type="service" id="twig.form.renderer" />
Expand Down
55 changes: 55 additions & 0 deletions src/Symfony/Component/HttpFoundation/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -1137,6 +1137,61 @@ public function getUriForPath($path)
return $this->getSchemeAndHttpHost().$this->getBaseUrl().$path;
}

/**
* Returns the path as relative reference from the current Request path.
*
* Only the URIs path component (no schema, host etc.) is relevant and must be given.
* Both paths must be absolute and not contain relative parts.
* Relative URLs from one resource to another are useful when generating self-contained downloadable document archives.
* Furthermore, they can be used to reduce the link size in documents.
*
* Example target paths, given a base path of "/a/b/c/d":
* - "/a/b/c/d" -> ""
* - "/a/b/c/" -> "./"
* - "/a/b/" -> "../"
* - "/a/b/c/other" -> "other"
* - "/a/x/y" -> "../../x/y"
*
* @param string $path The target path
*
* @return string The relative target path
*/
public function getRelativeUriForPath($path)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is exactly the same as the code in UrlGenerator.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe instead of duplicating that code add new class (same like there is IpUtils class) i.e. UrlUtils and re-use it in places it's needed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stloyd That would be in a new component as Routing and HttpFoundation are independent. IMO, it's not worth it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fabpot From what I see Routing depends on HttpFoundation, so moving instead of duplicating code have sense IMO.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it does not. As you can see, the requirement is only for dev. Anyway, the code is now slightly different.

{
// be sure that we are dealing with an absolute path
if (!isset($path[0]) || '/' !== $path[0]) {
return $path;
}

if ($path === $basePath = $this->getPathInfo()) {
return '';
}

$sourceDirs = explode('/', isset($basePath[0]) && '/' === $basePath[0] ? substr($basePath, 1) : $basePath);
$targetDirs = explode('/', isset($path[0]) && '/' === $path[0] ? substr($path, 1) : $path);
array_pop($sourceDirs);
$targetFile = array_pop($targetDirs);

foreach ($sourceDirs as $i => $dir) {
if (isset($targetDirs[$i]) && $dir === $targetDirs[$i]) {
unset($sourceDirs[$i], $targetDirs[$i]);
} else {
break;
}
}

$targetDirs[] = $targetFile;
$path = str_repeat('../', count($sourceDirs)).implode('/', $targetDirs);

// A reference to the same base directory or an empty subdirectory must be prefixed with "./".
// This also applies to a segment with a colon character (e.g., "file:colon") that cannot be used
// as the first segment of a relative-path reference, as it would be mistaken for a scheme name
// (see http://tools.ietf.org/html/rfc3986#section-4.2).
return !isset($path[0]) || '/' === $path[0]
|| false !== ($colonPos = strpos($path, ':')) && ($colonPos < ($slashPos = strpos($path, '/')) || false === $slashPos)
? "./$path" : $path;
}

/**
* Generates the normalized query string for the Request.
*
Expand Down
20 changes: 20 additions & 0 deletions src/Symfony/Component/HttpFoundation/Tests/RequestTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,26 @@ public function testGetUriForPath()
$this->assertEquals('http://servername/some/path', $request->getUriForPath('/some/path'));
}

/**
* @dataProvider getRelativeUriForPathData()
*/
public function testGetRelativeUriForPath($expected, $pathinfo, $path)
{
$this->assertEquals($expected, Request::create($pathinfo)->getRelativeUriForPath($path));
}

public function getRelativeUriForPathData()
{
return array(
array('me.png', '/foo', '/me.png'),
array('../me.png', '/foo/bar', '/me.png'),
array('me.png', '/foo/bar', '/foo/me.png'),
array('../baz/me.png', '/foo/bar/b', '/foo/baz/me.png'),
array('../../fooz/baz/me.png', '/foo/bar/b', '/fooz/baz/me.png'),
array('baz/me.png', '/foo/bar/b', 'baz/me.png'),
);
}

/**
* @covers Symfony\Component\HttpFoundation\Request::getUserInfo
*/
Expand Down
0