diff --git a/CHANGELOG.md b/CHANGELOG.md index 74bb06360..f735b680e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ 1. [](#bugfix) * Fixed some potential issues when `$grav['user']` is not set * Fixed error when calling `Media::add($name, null)` + * Fixed `url()` returning wrong path if using stream with grav root path in it, eg: `user-data://shop` when Grav is in `/shop` + * Fixed `url()` not returning a path to non-existing file (`user-data://shop` => `/user/data/shop`) if it is set to fail gracefully + * Fixed `url()` returning false on unknown streams, such as `ftp://domain.com`, they should be treated as external URL # v1.6.11 ## 06/21/2019 diff --git a/system/src/Grav/Common/Utils.php b/system/src/Grav/Common/Utils.php index d535150fc..ba7528bce 100644 --- a/system/src/Grav/Common/Utils.php +++ b/system/src/Grav/Common/Utils.php @@ -43,43 +43,80 @@ abstract class Utils } } - if (Grav::instance()['config']->get('system.absolute_urls', false)) { - $domain = true; - } + $input = (string)$input; if (Uri::isExternal($input)) { return $input; } + $grav = Grav::instance(); + /** @var Uri $uri */ - $uri = Grav::instance()['uri']; + $uri = $grav['uri']; - $root = $uri->rootUrl(); - $input = Utils::replaceFirstOccurrence($root, '', $input); - - $input = ltrim((string)$input, '/'); - - if (Utils::contains((string)$input, '://')) { + if (static::contains((string)$input, '://')) { /** @var UniformResourceLocator $locator */ - $locator = Grav::instance()['locator']; + $locator = $grav['locator']; + $parts = Uri::parseUrl($input); - if ($parts) { - try { - $resource = $locator->findResource("{$parts['scheme']}://{$parts['host']}{$parts['path']}", false); - } catch (\Exception $e) { - return $fail_gracefully ? $input : false; + if (is_array($parts)) { + // Make sure we always have scheme, host, port and path. + $scheme = $parts['scheme'] ?? ''; + $host = $parts['host'] ?? ''; + $port = $parts['port'] ?? ''; + $path = $parts['path'] ?? ''; + + if ($scheme && !$port) { + // If URL has a scheme, we need to check if it's one of Grav streams. + if (!$locator->schemeExists($scheme)) { + // If scheme does not exists as a stream, assume it's external. + return str_replace(' ', '%20', $input); + } + + // Attempt to find the resource (because of parse_url() we need to put host back to path). + $resource = $locator->findResource("{$scheme}://{$host}{$path}", false); + + if ($resource === false) { + if (!$fail_gracefully) { + return false; + } + + // Return location where the file would be if it was saved. + $resource = $locator->findResource("{$scheme}://{$host}{$path}", false, true); + } + + } elseif ($host || $port) { + // If URL doesn't have scheme but has host or port, it is external. + return str_replace(' ', '%20', $input); } - if ($resource && isset($parts['query'])) { - $resource = $resource . '?' . $parts['query']; + if (!empty($resource)) { + // Add query string back. + if (isset($parts['query'])) { + $resource .= '?' . $parts['query']; + } + + // Add fragment back. + if (isset($parts['fragment'])) { + $resource .= '#' . $parts['fragment']; + } } + } else { // Not a valid URL (can still be a stream). $resource = $locator->findResource($input, false); } } else { + $root = $uri->rootUrl(); + + if (static::startsWith($input, $root)) { + $input = static::replaceFirstOccurrence($root, '', $input); + } + + $input = ltrim($input, '/'); + $resource = $input; } @@ -87,6 +124,8 @@ abstract class Utils return false; } + $domain = $domain ?: $grav['config']->get('system.absolute_urls', false); + return rtrim($uri->rootUrl($domain), '/') . '/' . ($resource ?? ''); } diff --git a/tests/unit/Grav/Common/UtilsTest.php b/tests/unit/Grav/Common/UtilsTest.php index e5be6394b..e7862427c 100644 --- a/tests/unit/Grav/Common/UtilsTest.php +++ b/tests/unit/Grav/Common/UtilsTest.php @@ -382,30 +382,60 @@ class UtilsTest extends \Codeception\TestCase\Test // Fail hard $this->assertSame(false, Utils::url('', true)); $this->assertSame(false, Utils::url('')); - $this->assertSame(false, Utils::url('foo://bar/baz')); $this->assertSame(false, Utils::url(new stdClass())); $this->assertSame(false, Utils::url(['foo','bar','baz'])); + $this->assertSame(false, Utils::url('user://does/not/exist')); // Fail Gracefully $this->assertSame('/', Utils::url('/', false, true)); $this->assertSame('/', Utils::url('', false, true)); - $this->assertSame('foo://bar/baz', Utils::url('foo://bar/baz', false, true)); $this->assertSame('/', Utils::url(new stdClass(), false, true)); $this->assertSame('/', Utils::url(['foo','bar','baz'], false, true)); + $this->assertSame('/user/does/not/exist', Utils::url('user://does/not/exist', false, true)); + // Simple paths $this->assertSame('/', Utils::url('/')); - $this->assertSame('http://testing.dev/', Utils::url('/', true)); - $this->assertSame('http://testing.dev/path1', Utils::url('/path1', true)); $this->assertSame('/path1', Utils::url('/path1')); $this->assertSame('/path1/path2', Utils::url('/path1/path2')); + $this->assertSame('/random/path1/path2', Utils::url('/random/path1/path2')); + $this->assertSame('/foobar.jpg', Utils::url('/foobar.jpg')); + $this->assertSame('/path1/foobar.jpg', Utils::url('/path1/foobar.jpg')); + $this->assertSame('/path1/path2/foobar.jpg', Utils::url('/path1/path2/foobar.jpg')); + $this->assertSame('/random/path1/path2/foobar.jpg', Utils::url('/random/path1/path2/foobar.jpg')); + + // Simple paths with domain + $this->assertSame('http://testing.dev/', Utils::url('/', true)); + $this->assertSame('http://testing.dev/path1', Utils::url('/path1', true)); + $this->assertSame('http://testing.dev/path1/path2', Utils::url('/path1/path2', true)); + $this->assertSame('http://testing.dev/random/path1/path2', Utils::url('/random/path1/path2', true)); + $this->assertSame('http://testing.dev/foobar.jpg', Utils::url('/foobar.jpg', true)); + $this->assertSame('http://testing.dev/path1/foobar.jpg', Utils::url('/path1/foobar.jpg', true)); + $this->assertSame('http://testing.dev/path1/path2/foobar.jpg', Utils::url('/path1/path2/foobar.jpg', true)); + $this->assertSame('http://testing.dev/random/path1/path2/foobar.jpg', Utils::url('/random/path1/path2/foobar.jpg', true)); + + // Relative paths from Grav root. + $this->assertSame('/subdir', Utils::url('subdir')); + $this->assertSame('/subdir/path1', Utils::url('subdir/path1')); + $this->assertSame('/subdir/path1/path2', Utils::url('subdir/path1/path2')); + $this->assertSame('/path1', Utils::url('path1')); + $this->assertSame('/path1/path2', Utils::url('path1/path2')); + $this->assertSame('/foobar.jpg', Utils::url('foobar.jpg')); + $this->assertSame('http://testing.dev/foobar.jpg', Utils::url('foobar.jpg', true)); + + // Relative paths from Grav root with domain. $this->assertSame('http://testing.dev/foobar.jpg', Utils::url('foobar.jpg', true)); $this->assertSame('http://testing.dev/foobar.jpg', Utils::url('/foobar.jpg', true)); $this->assertSame('http://testing.dev/path1/foobar.jpg', Utils::url('/path1/foobar.jpg', true)); - $this->assertSame('/foobar.jpg', Utils::url('/foobar.jpg')); - $this->assertSame('/foobar.jpg', Utils::url('foobar.jpg')); - $this->assertSame('/path1/foobar.jpg', Utils::url('/path1/foobar.jpg')); - $this->assertSame('/path1/path2/foobar.jpg', Utils::url('/path1/path2/foobar.jpg')); + // All Non-existing streams should be treated as external URI / protocol. + $this->assertSame('http://domain.com/path', Utils::url('http://domain.com/path')); + $this->assertSame('ftp://domain.com/path', Utils::url('ftp://domain.com/path')); + $this->assertSame('sftp://domain.com/path', Utils::url('sftp://domain.com/path')); + $this->assertSame('ssh://domain.com', Utils::url('ssh://domain.com')); + $this->assertSame('pop://domain.com', Utils::url('pop://domain.com')); + $this->assertSame('foo://bar/baz', Utils::url('foo://bar/baz')); + $this->assertSame('foo://bar/baz', Utils::url('foo://bar/baz', true)); + // $this->assertSame('mailto:joe@domain.com', Utils::url('mailto:joe@domain.com', true)); // FIXME <- } public function testUrlWithRoot() @@ -415,31 +445,69 @@ class UtilsTest extends \Codeception\TestCase\Test // Fail hard $this->assertSame(false, Utils::url('', true)); $this->assertSame(false, Utils::url('')); - $this->assertSame(false, Utils::url('foo://bar/baz')); + $this->assertSame(false, Utils::url(new stdClass())); + $this->assertSame(false, Utils::url(['foo','bar','baz'])); + $this->assertSame(false, Utils::url('user://does/not/exist')); // Fail Gracefully $this->assertSame('/subdir/', Utils::url('/', false, true)); $this->assertSame('/subdir/', Utils::url('', false, true)); - $this->assertSame('foo://bar/baz', Utils::url('foo://bar/baz', false, true)); + $this->assertSame('/subdir/', Utils::url(new stdClass(), false, true)); + $this->assertSame('/subdir/', Utils::url(['foo','bar','baz'], false, true)); + $this->assertSame('/subdir/user/does/not/exist', Utils::url('user://does/not/exist', false, true)); - $this->assertSame('http://testing.dev/subdir/', Utils::url('/', true)); - $this->assertSame('http://testing.dev/subdir/path1', Utils::url('/path1', true)); - $this->assertSame('http://testing.dev/subdir/path1', Utils::url('/subdir/path1', true)); + // Simple paths $this->assertSame('/subdir/', Utils::url('/')); $this->assertSame('/subdir/path1', Utils::url('/path1')); $this->assertSame('/subdir/path1/path2', Utils::url('/path1/path2')); - $this->assertSame('/subdir/path1/path2', Utils::url('/subdir/path1/path2')); - - $this->assertSame('http://testing.dev/subdir/foobar.jpg', Utils::url('foobar.jpg', true)); - $this->assertSame('http://testing.dev/subdir/foobar.jpg', Utils::url('/foobar.jpg', true)); - $this->assertSame('http://testing.dev/subdir/foobar.jpg', Utils::url('/subdir/foobar.jpg', true)); - $this->assertSame('http://testing.dev/subdir/path1/foobar.jpg', Utils::url('/path1/foobar.jpg', true)); - $this->assertSame('http://testing.dev/subdir/path1/foobar.jpg', Utils::url('/subdir/path1/foobar.jpg', true)); + $this->assertSame('/subdir/random/path1/path2', Utils::url('/random/path1/path2')); $this->assertSame('/subdir/foobar.jpg', Utils::url('/foobar.jpg')); - $this->assertSame('/subdir/foobar.jpg', Utils::url('foobar.jpg')); - $this->assertSame('/subdir/foobar.jpg', Utils::url('/subdir/foobar.jpg')); $this->assertSame('/subdir/path1/foobar.jpg', Utils::url('/path1/foobar.jpg')); + $this->assertSame('/subdir/path1/path2/foobar.jpg', Utils::url('/path1/path2/foobar.jpg')); + $this->assertSame('/subdir/random/path1/path2/foobar.jpg', Utils::url('/random/path1/path2/foobar.jpg')); + + // Simple paths with domain + $this->assertSame('http://testing.dev/subdir/', Utils::url('/', true)); + $this->assertSame('http://testing.dev/subdir/path1', Utils::url('/path1', true)); + $this->assertSame('http://testing.dev/subdir/path1/path2', Utils::url('/path1/path2', true)); + $this->assertSame('http://testing.dev/subdir/random/path1/path2', Utils::url('/random/path1/path2', true)); + $this->assertSame('http://testing.dev/subdir/foobar.jpg', Utils::url('/foobar.jpg', true)); + $this->assertSame('http://testing.dev/subdir/path1/foobar.jpg', Utils::url('/path1/foobar.jpg', true)); + $this->assertSame('http://testing.dev/subdir/path1/path2/foobar.jpg', Utils::url('/path1/path2/foobar.jpg', true)); + $this->assertSame('http://testing.dev/subdir/random/path1/path2/foobar.jpg', Utils::url('/random/path1/path2/foobar.jpg', true)); + + // Paths including the grav base. + $this->assertSame('/subdir/', Utils::url('/subdir')); + $this->assertSame('/subdir/path1', Utils::url('/subdir/path1')); + $this->assertSame('/subdir/path1/path2', Utils::url('/subdir/path1/path2')); + $this->assertSame('/subdir/foobar.jpg', Utils::url('/subdir/foobar.jpg')); $this->assertSame('/subdir/path1/foobar.jpg', Utils::url('/subdir/path1/foobar.jpg')); + + // Relative paths from Grav root with domain. + $this->assertSame('http://testing.dev/subdir/', Utils::url('/subdir', true)); + $this->assertSame('http://testing.dev/subdir/path1', Utils::url('/subdir/path1', true)); + $this->assertSame('http://testing.dev/subdir/path1/path2', Utils::url('/subdir/path1/path2', true)); + $this->assertSame('http://testing.dev/subdir/foobar.jpg', Utils::url('/subdir/foobar.jpg', true)); + $this->assertSame('http://testing.dev/subdir/path1/foobar.jpg', Utils::url('/subdir/path1/foobar.jpg', true)); + + // Relative paths from Grav root. + $this->assertSame('/subdir/subdir', Utils::url('subdir')); + $this->assertSame('/subdir/subdir/path1', Utils::url('subdir/path1')); + $this->assertSame('/subdir/subdir/path1/path2', Utils::url('subdir/path1/path2')); + $this->assertSame('/subdir/path1', Utils::url('path1')); + $this->assertSame('/subdir/path1/path2', Utils::url('path1/path2')); + $this->assertSame('/subdir/foobar.jpg', Utils::url('foobar.jpg')); + $this->assertSame('http://testing.dev/subdir/foobar.jpg', Utils::url('foobar.jpg', true)); + + // All Non-existing streams should be treated as external URI / protocol. + $this->assertSame('http://domain.com/path', Utils::url('http://domain.com/path')); + $this->assertSame('ftp://domain.com/path', Utils::url('ftp://domain.com/path')); + $this->assertSame('sftp://domain.com/path', Utils::url('sftp://domain.com/path')); + $this->assertSame('ssh://domain.com', Utils::url('ssh://domain.com')); + $this->assertSame('pop://domain.com', Utils::url('pop://domain.com')); + $this->assertSame('foo://bar/baz', Utils::url('foo://bar/baz')); + $this->assertSame('foo://bar/baz', Utils::url('foo://bar/baz', true)); + // $this->assertSame('mailto:joe@domain.com', Utils::url('mailto:joe@domain.com', true)); // FIXME <- } public function testUrlWithStreams()