diff --git a/.version b/.version index bceeec9b3..e9488e3f0 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -0.2.648 \ No newline at end of file +0.2.655 \ No newline at end of file diff --git a/runtime.go b/runtime.go index 8bda16774..ced77ae07 100644 --- a/runtime.go +++ b/runtime.go @@ -461,25 +461,6 @@ func SanitizeCSS[T ~string](property string, value T) SafeCSS { return SafeCSS(p + ":" + v + ";") } -// Hyperlink sanitization. - -// FailedSanitizationURL is returned if a URL fails sanitization checks. -const FailedSanitizationURL = SafeURL("about:invalid#TemplFailedSanitizationURL") - -// URL sanitizes the input string s and returns a SafeURL. -func URL(s string) SafeURL { - if i := strings.IndexRune(s, ':'); i >= 0 && !strings.ContainsRune(s[:i], '/') { - protocol := s[:i] - if !strings.EqualFold(protocol, "http") && !strings.EqualFold(protocol, "https") && !strings.EqualFold(protocol, "mailto") && !strings.EqualFold(protocol, "tel") && !strings.EqualFold(protocol, "ftp") && !strings.EqualFold(protocol, "ftps") { - return FailedSanitizationURL - } - } - return SafeURL(s) -} - -// SafeURL is a URL that has been sanitized. -type SafeURL string - // Attributes is an alias to map[string]any made for spread attributes. type Attributes map[string]any diff --git a/url.go b/url.go new file mode 100644 index 000000000..bf912e137 --- /dev/null +++ b/url.go @@ -0,0 +1,20 @@ +package templ + +import "strings" + +// FailedSanitizationURL is returned if a URL fails sanitization checks. +const FailedSanitizationURL = SafeURL("about:invalid#TemplFailedSanitizationURL") + +// URL sanitizes the input string s and returns a SafeURL. +func URL(s string) SafeURL { + if i := strings.IndexRune(s, ':'); i >= 0 && !strings.ContainsRune(s[:i], '/') { + protocol := s[:i] + if !strings.EqualFold(protocol, "http") && !strings.EqualFold(protocol, "https") && !strings.EqualFold(protocol, "mailto") && !strings.EqualFold(protocol, "tel") && !strings.EqualFold(protocol, "ftp") && !strings.EqualFold(protocol, "ftps") { + return FailedSanitizationURL + } + } + return SafeURL(s) +} + +// SafeURL is a URL that has been sanitized. +type SafeURL string diff --git a/url_test.go b/url_test.go new file mode 100644 index 000000000..346edf344 --- /dev/null +++ b/url_test.go @@ -0,0 +1,57 @@ +package templ + +import ( + "strings" + "testing" +) + +type urlTest struct { + url string + expectSanitized bool +} + +var urlTests = []urlTest{ + {"//example.com", false}, + {"/", false}, + {"/index", false}, + {"http://example.com", false}, + {"https://example.com", false}, + {"mailto:test@example.com", false}, + {"tel:+1234567890", false}, + {"ftp://example.com", false}, + {"ftps://example.com", false}, + {"irc://example.com", true}, + {"bitcoin://example.com", true}, +} + +func testURL(t *testing.T, url string, expectSanitized bool) { + u := URL(url) + wasSanitized := u == FailedSanitizationURL + if expectSanitized != wasSanitized { + t.Errorf("expected sanitized=%v, got %v", expectSanitized, wasSanitized) + } +} + +func TestURL(t *testing.T) { + for _, test := range urlTests { + t.Run(test.url, func(t *testing.T) { + testURL(t, test.url, test.expectSanitized) + }) + test.url = strings.ToUpper(test.url) + t.Run(strings.ToUpper(test.url), func(t *testing.T) { + testURL(t, test.url, test.expectSanitized) + }) + } +} + +func BenchmarkURL(b *testing.B) { + for i := 0; i < b.N; i++ { + for _, test := range urlTests { + u := URL(test.url) + wasSanitized := u == FailedSanitizationURL + if test.expectSanitized != wasSanitized { + b.Errorf("expected sanitized=%v, got %v", test.expectSanitized, wasSanitized) + } + } + } +}