From 5d2d19141c0c301413c61a97213f1d8d191b8edf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Goetz?= Date: Tue, 28 May 2019 21:26:27 +0200 Subject: [PATCH] Sanitize filenames for usage in urls, fixes #86 --- libs/ContentTypes/Markdown/LinkRenderer.php | 28 ++++++++++++++++--- libs/DauxHelper.php | 9 ++++-- libs/Tree/Builder.php | 1 + .../Markdown/LinkRendererTest.php | 6 ++++ tests/Tree/BuilderTest.php | 21 ++++++++++---- 5 files changed, 53 insertions(+), 12 deletions(-) diff --git a/libs/ContentTypes/Markdown/LinkRenderer.php b/libs/ContentTypes/Markdown/LinkRenderer.php index 9730850..ba7b11e 100644 --- a/libs/ContentTypes/Markdown/LinkRenderer.php +++ b/libs/ContentTypes/Markdown/LinkRenderer.php @@ -109,18 +109,38 @@ class LinkRenderer extends \League\CommonMark\Inline\Renderer\LinkRenderer $urlAndHash = explode('#', $url); $url = $urlAndHash[0]; + $foundWithHash = false; + try { $file = $this->resolveInternalFile($url); $url = DauxHelper::getRelativePath($this->daux->getCurrentPage()->getUrl(), $file->getUrl()); } catch (LinkNotFoundException $e) { - if ($this->daux->isStatic()) { - throw $e; + + + + // For some reason, the filename could contain a # and thus the link needs to resolve to that. + try { + if (strlen($urlAndHash[1]) > 0) { + $file = $this->resolveInternalFile($url . '#' . $urlAndHash[1]); + $url = DauxHelper::getRelativePath($this->daux->getCurrentPage()->getUrl(), $file->getUrl()); + $foundWithHash = true; + } + } catch (LinkNotFoundException $e2) { + // If it's still not found here, we'll only + // report on the first error as the second + // one will tell the same. } - $element->setAttribute('class', 'Link--broken'); + if (!$foundWithHash) { + if ($this->daux->isStatic()) { + throw $e; + } + + $element->setAttribute('class', 'Link--broken'); + } } - if (isset($urlAndHash[1])) { + if (!$foundWithHash && isset($urlAndHash[1])) { $url .= '#' . $urlAndHash[1]; } diff --git a/libs/DauxHelper.php b/libs/DauxHelper.php index ca5b13d..2b97f5d 100644 --- a/libs/DauxHelper.php +++ b/libs/DauxHelper.php @@ -189,7 +189,7 @@ class DauxHelper continue; } - $node = urldecode($node); + $node = DauxHelper::slug(urldecode($node)); // if the node exists in the current request tree, // change the $tree variable to reference the new @@ -241,18 +241,21 @@ class DauxHelper */ public static function slug($title) { + // Convert to ASCII foreach (static::charsArray() as $key => $value) { $title = str_replace($value, $key, $title); } + // Remove unsupported characters $title = preg_replace('/[^\x20-\x7E]/u', '', $title); $separator = '_'; // Convert all dashes into underscores $title = preg_replace('![' . preg_quote('-') . ']+!u', $separator, $title); - // Remove all characters that are not the separator, letters, numbers, or whitespace. - $title = preg_replace('![^' . preg_quote($separator) . '\pL\pN\s]+!u', '', $title); + // Remove all characters that are not valid in a URL: + // $-_.+!*'(), separator, letters, numbers, or whitespace. + $title = preg_replace('![^-' . preg_quote($separator) . '\!\'\(\),\.\+\*\$\pL\pN\s]+!u', '', $title); // Replace all separator characters and whitespace by a single separator $title = preg_replace('![' . preg_quote($separator) . '\s]+!u', $separator, $title); diff --git a/libs/Tree/Builder.php b/libs/Tree/Builder.php index b3587d5..a93b26d 100644 --- a/libs/Tree/Builder.php +++ b/libs/Tree/Builder.php @@ -115,6 +115,7 @@ class Builder } $uri = static::removeSortingInformations($name); + $uri = DauxHelper::slug($uri); if ($config->isStatic()) { $uri .= '.html'; } diff --git a/tests/ContentTypes/Markdown/LinkRendererTest.php b/tests/ContentTypes/Markdown/LinkRendererTest.php index a0e7668..f82e926 100644 --- a/tests/ContentTypes/Markdown/LinkRendererTest.php +++ b/tests/ContentTypes/Markdown/LinkRendererTest.php @@ -20,6 +20,7 @@ class LinkRendererTest extends TestCase 'Widgets' => [ 'Page.md' => 'another page', 'Button.md' => 'another page', + 'Page_with_#_hash.md' => 'page with hash', ], ]; $root = vfsStream::setup('root', null, $structure); @@ -38,6 +39,11 @@ class LinkRendererTest extends TestCase public function providerRenderLink() { return [ + // /Widgets/Page_with_#_hash + ['Link', '[Link](../Widgets/Page_with_#_hash.md)', 'Content/Page.html'], + ['Link', '[Link](!Widgets/Page_with_#_hash)', 'Content/Page.html'], + ['Link', '[Link](Page_with_#_hash.md)', 'Widgets/Page.html'], + // /Widgets/Page ['Link', '[Link](http://google.ch)', 'Widgets/Page.html'], ['Link', '[Link](#features)', 'Widgets/Page.html'], diff --git a/tests/Tree/BuilderTest.php b/tests/Tree/BuilderTest.php index d675925..a742527 100644 --- a/tests/Tree/BuilderTest.php +++ b/tests/Tree/BuilderTest.php @@ -82,16 +82,27 @@ class BuilderTest extends TestCase return new Root($config); } - public function testGetOrCreatePage() + public function providerCreatePage() + { + return [ + ['A Page.md', 'dir/A_Page.html', 'A_Page.html', 'A Page'], + ['Page#1.md', 'dir/Page1.html', 'Page1.html', 'Page#1'] + ]; + } + + /** + * @dataProvider providerCreatePage + */ + public function testGetOrCreatePage($file, $url, $uri, $title) { $directory = new Directory($this->getStaticRoot(), 'dir'); - $entry = Builder::getOrCreatePage($directory, 'A Page.md'); + $entry = Builder::getOrCreatePage($directory, $file); $this->assertSame($directory, $entry->getParent()); - $this->assertEquals('dir/A_Page.html', $entry->getUrl()); - $this->assertEquals('A_Page.html', $entry->getUri()); - $this->assertEquals('A Page', $entry->getTitle()); + $this->assertEquals($url, $entry->getUrl()); + $this->assertEquals($uri, $entry->getUri()); + $this->assertEquals($title, $entry->getTitle()); $this->assertInstanceOf('Todaymade\Daux\Tree\Content', $entry); }