From ee27ed8376005d094a9a90e61fb175d2c79db5e7 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 5 Jan 2015 14:15:09 +0100 Subject: [PATCH 1/2] added an absolute_url() Twig function --- src/Symfony/Bridge/Twig/CHANGELOG.md | 5 ++ .../Extension/HttpFoundationExtension.php | 72 +++++++++++++++++++ .../Compiler/ExtensionPass.php | 4 ++ .../TwigBundle/Resources/config/twig.xml | 3 + 4 files changed, 84 insertions(+) create mode 100644 src/Symfony/Bridge/Twig/Extension/HttpFoundationExtension.php diff --git a/src/Symfony/Bridge/Twig/CHANGELOG.md b/src/Symfony/Bridge/Twig/CHANGELOG.md index 4be010ba20..7fba478233 100644 --- a/src/Symfony/Bridge/Twig/CHANGELOG.md +++ b/src/Symfony/Bridge/Twig/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +2.7.0 +----- + + * added an HttpFoundation extension (provides the `absolute_url` function) + 2.5.0 ----- diff --git a/src/Symfony/Bridge/Twig/Extension/HttpFoundationExtension.php b/src/Symfony/Bridge/Twig/Extension/HttpFoundationExtension.php new file mode 100644 index 0000000000..44e710f255 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Extension/HttpFoundationExtension.php @@ -0,0 +1,72 @@ + + * + * 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\Asset\Packages; + +/** + * Twig extension for the Symfony HttpFoundation component. + * + * @author Fabien Potencier + */ +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')), + ); + } + + /** + * Returns the absolute URL for the given path. + * + * This method returns the path unchanged if no request is available. + * + * @param string $path The path + * + * @return string The absolute URL + */ + public function generateAbsoluteUrl($path) + { + if (false !== strpos($path, '://') || '//' === substr($path, 0, 2)) { + return $path; + } + + if (!$request = $this->requestStack->getMasterRequest()) { + return $path; + } + + return $request->getUriForPath($path); + } + + /** + * Returns the name of the extension. + * + * @return string The extension name + */ + public function getName() + { + return 'request'; + } +} diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php index 641c5df155..c4bd1d372a 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php @@ -42,5 +42,9 @@ class ExtensionPass implements CompilerPassInterface 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'); + } } } diff --git a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml index 4ff841db1c..139befcc31 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml +++ b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml @@ -103,6 +103,9 @@ + + + From 0ec852d79fb0c89d66032784087308fc46f4a126 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 5 Jan 2015 14:25:19 +0100 Subject: [PATCH 2/2] added a relative_path Twig function --- src/Symfony/Bridge/Twig/CHANGELOG.md | 2 +- .../Extension/HttpFoundationExtension.php | 41 +++++++++- .../Extension/HttpFoundationExtensionTest.php | 74 +++++++++++++++++++ .../Component/HttpFoundation/Request.php | 55 ++++++++++++++ .../HttpFoundation/Tests/RequestTest.php | 20 +++++ 5 files changed, 189 insertions(+), 3 deletions(-) create mode 100644 src/Symfony/Bridge/Twig/Tests/Extension/HttpFoundationExtensionTest.php diff --git a/src/Symfony/Bridge/Twig/CHANGELOG.md b/src/Symfony/Bridge/Twig/CHANGELOG.md index 7fba478233..c4df599be1 100644 --- a/src/Symfony/Bridge/Twig/CHANGELOG.md +++ b/src/Symfony/Bridge/Twig/CHANGELOG.md @@ -4,7 +4,7 @@ CHANGELOG 2.7.0 ----- - * added an HttpFoundation extension (provides the `absolute_url` function) + * added an HttpFoundation extension (provides the `absolute_url` and the `relative_path` functions) 2.5.0 ----- diff --git a/src/Symfony/Bridge/Twig/Extension/HttpFoundationExtension.php b/src/Symfony/Bridge/Twig/Extension/HttpFoundationExtension.php index 44e710f255..e21a889975 100644 --- a/src/Symfony/Bridge/Twig/Extension/HttpFoundationExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/HttpFoundationExtension.php @@ -12,7 +12,7 @@ namespace Symfony\Bridge\Twig\Extension; use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\Asset\Packages; +use Symfony\Component\HttpFoundation\Request; /** * Twig extension for the Symfony HttpFoundation component. @@ -35,17 +35,20 @@ class HttpFoundationExtension extends \Twig_Extension { 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 path. + * 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() */ public function generateAbsoluteUrl($path) { @@ -57,9 +60,43 @@ class HttpFoundationExtension extends \Twig_Extension return $path; } + if (!$path || '/' !== $path[0]) { + $prefix = $request->getPathInfo(); + $last = strlen($prefix) - 1; + if ($last !== $pos = strrpos($prefix, '/')) { + $prefix = substr($prefix, 0, $pos).'/'; + } + + $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; + } + + if (!$request = $this->requestStack->getMasterRequest()) { + return $path; + } + + return $request->getRelativeUriForPath($path); + } + /** * Returns the name of the extension. * diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/HttpFoundationExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/HttpFoundationExtensionTest.php new file mode 100644 index 0000000000..228cc3fcea --- /dev/null +++ b/src/Symfony/Bridge/Twig/Tests/Extension/HttpFoundationExtensionTest.php @@ -0,0 +1,74 @@ + + * + * 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', '/'), + ); + } +} diff --git a/src/Symfony/Component/HttpFoundation/Request.php b/src/Symfony/Component/HttpFoundation/Request.php index 7bc0f1fd4d..7d77edc575 100644 --- a/src/Symfony/Component/HttpFoundation/Request.php +++ b/src/Symfony/Component/HttpFoundation/Request.php @@ -1137,6 +1137,61 @@ class Request 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) + { + // 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. * diff --git a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php index 7d7b0fff25..a6917f3e8e 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php @@ -575,6 +575,26 @@ class RequestTest extends \PHPUnit_Framework_TestCase $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 */