diff --git a/docs/releases.md b/docs/releases.md index ed728fcb..5e18edab 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1440,7 +1440,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release * Full [IPv6 support](config.md#ipv6-support) for ntfy and the official ntfy.sh server ([#519](https://github.com/binwiederhier/ntfy/issues/519)/[#1380](https://github.com/binwiederhier/ntfy/pull/1380)/[ansible#4](https://github.com/binwiederhier/ntfy-ansible/pull/4)) * Support `X-Client-IP`, `X-Real-IP`, `Forwarded` headers for [rate limiting](config.md#ip-based-rate-limiting) via `proxy-forwarded-header` and `proxy-trusted-hosts` ([#1360](https://github.com/binwiederhier/ntfy/pull/1360)/[#1252](https://github.com/binwiederhier/ntfy/pull/1252), thanks to [@pixitha](https://github.com/pixitha)) * Add STDIN support for `ntfy publish` ([#1382](https://github.com/binwiederhier/ntfy/pull/1382), thanks to [@srevn](https://github.com/srevn)) -* You can now use [Slim-Sprig](https://github.com/go-task/slim-sprig) functions in message/title templates ([#1121](https://github.com/binwiederhier/ntfy/issues/1121), thanks to [@davidatkinsondoyle](https://github.com/davidatkinsondoyle) for reporting and to [@wunter8](https://github.com/wunter8) for implementing) +* You can now use a subset of [Sprig](https://github.com/Masterminds/sprig) functions in message/title templates ([#1121](https://github.com/binwiederhier/ntfy/issues/1121), thanks to [@davidatkinsondoyle](https://github.com/davidatkinsondoyle) for reporting and to [@wunter8](https://github.com/wunter8) for implementing) **Languages** diff --git a/docs/sprig/os.md b/docs/sprig/os.md deleted file mode 100644 index e6120c03..00000000 --- a/docs/sprig/os.md +++ /dev/null @@ -1,24 +0,0 @@ -# OS Functions - -_WARNING:_ These functions can lead to information leakage if not used -appropriately. - -_WARNING:_ Some notable implementations of Sprig (such as -[Kubernetes Helm](http://helm.sh)) _do not provide these functions for security -reasons_. - -## env - -The `env` function reads an environment variable: - -``` -env "HOME" -``` - -## expandenv - -To substitute environment variables in a string, use `expandenv`: - -``` -expandenv "Your path is set to $PATH" -``` diff --git a/docs/sprig/semver.md b/docs/sprig/semver.md deleted file mode 100644 index f049613d..00000000 --- a/docs/sprig/semver.md +++ /dev/null @@ -1,151 +0,0 @@ -# Semantic Version Functions - -Some version schemes are easily parseable and comparable. Sprig provides functions -for working with [SemVer 2](http://semver.org) versions. - -## semver - -The `semver` function parses a string into a Semantic Version: - -``` -$version := semver "1.2.3-alpha.1+123" -``` - -_If the parser fails, it will cause template execution to halt with an error._ - -At this point, `$version` is a pointer to a `Version` object with the following -properties: - -- `$version.Major`: The major number (`1` above) -- `$version.Minor`: The minor number (`2` above) -- `$version.Patch`: The patch number (`3` above) -- `$version.Prerelease`: The prerelease (`alpha.1` above) -- `$version.Metadata`: The build metadata (`123` above) -- `$version.Original`: The original version as a string - -Additionally, you can compare a `Version` to another `version` using the `Compare` -function: - -``` -semver "1.4.3" | (semver "1.2.3").Compare -``` - -The above will return `-1`. - -The return values are: - -- `-1` if the given semver is greater than the semver whose `Compare` method was called -- `1` if the version who's `Compare` function was called is greater. -- `0` if they are the same version - -(Note that in SemVer, the `Metadata` field is not compared during version -comparison operations.) - -## semverCompare - -A more robust comparison function is provided as `semverCompare`. It returns `true` if -the constraint matches, or `false` if it does not match. This version supports version ranges: - -- `semverCompare "1.2.3" "1.2.3"` checks for an exact match -- `semverCompare "^1.2.0" "1.2.3"` checks that the major and minor versions match, and that the patch - number of the second version is _greater than or equal to_ the first parameter. - -The SemVer functions use the [Masterminds semver library](https://github.com/Masterminds/semver), -from the creators of Sprig. - -## Basic Comparisons - -There are two elements to the comparisons. First, a comparison string is a list -of space or comma separated AND comparisons. These are then separated by || (OR) -comparisons. For example, `">= 1.2 < 3.0.0 || >= 4.2.3"` is looking for a -comparison that's greater than or equal to 1.2 and less than 3.0.0 or is -greater than or equal to 4.2.3. - -The basic comparisons are: - -- `=`: equal (aliased to no operator) -- `!=`: not equal -- `>`: greater than -- `<`: less than -- `>=`: greater than or equal to -- `<=`: less than or equal to - -_Note, according to the Semantic Version specification pre-releases may not be -API compliant with their release counterpart. It says,_ - -## Working With Prerelease Versions - -Pre-releases, for those not familiar with them, are used for software releases -prior to stable or generally available releases. Examples of prereleases include -development, alpha, beta, and release candidate releases. A prerelease may be -a version such as `1.2.3-beta.1` while the stable release would be `1.2.3`. In the -order of precedence, prereleases come before their associated releases. In this -example `1.2.3-beta.1 < 1.2.3`. - -According to the Semantic Version specification prereleases may not be -API compliant with their release counterpart. It says, - -> A pre-release version indicates that the version is unstable and might not satisfy the intended compatibility requirements as denoted by its associated normal version. - -SemVer comparisons using constraints without a prerelease comparator will skip -prerelease versions. For example, `>=1.2.3` will skip prereleases when looking -at a list of releases while `>=1.2.3-0` will evaluate and find prereleases. - -The reason for the `0` as a pre-release version in the example comparison is -because pre-releases can only contain ASCII alphanumerics and hyphens (along with -`.` separators), per the spec. Sorting happens in ASCII sort order, again per the -spec. The lowest character is a `0` in ASCII sort order -(see an [ASCII Table](http://www.asciitable.com/)) - -Understanding ASCII sort ordering is important because A-Z comes before a-z. That -means `>=1.2.3-BETA` will return `1.2.3-alpha`. What you might expect from case -sensitivity doesn't apply here. This is due to ASCII sort ordering which is what -the spec specifies. - -## Hyphen Range Comparisons - -There are multiple methods to handle ranges and the first is hyphens ranges. -These look like: - -- `1.2 - 1.4.5` which is equivalent to `>= 1.2 <= 1.4.5` -- `2.3.4 - 4.5` which is equivalent to `>= 2.3.4 <= 4.5` - -## Wildcards In Comparisons - -The `x`, `X`, and `*` characters can be used as a wildcard character. This works -for all comparison operators. When used on the `=` operator it falls -back to the patch level comparison (see tilde below). For example, - -- `1.2.x` is equivalent to `>= 1.2.0, < 1.3.0` -- `>= 1.2.x` is equivalent to `>= 1.2.0` -- `<= 2.x` is equivalent to `< 3` -- `*` is equivalent to `>= 0.0.0` - -## Tilde Range Comparisons (Patch) - -The tilde (`~`) comparison operator is for patch level ranges when a minor -version is specified and major level changes when the minor number is missing. -For example, - -- `~1.2.3` is equivalent to `>= 1.2.3, < 1.3.0` -- `~1` is equivalent to `>= 1, < 2` -- `~2.3` is equivalent to `>= 2.3, < 2.4` -- `~1.2.x` is equivalent to `>= 1.2.0, < 1.3.0` -- `~1.x` is equivalent to `>= 1, < 2` - -## Caret Range Comparisons (Major) - -The caret (`^`) comparison operator is for major level changes once a stable -(1.0.0) release has occurred. Prior to a 1.0.0 release the minor versions acts -as the API stability level. This is useful when comparisons of API versions as a -major change is API breaking. For example, - -- `^1.2.3` is equivalent to `>= 1.2.3, < 2.0.0` -- `^1.2.x` is equivalent to `>= 1.2.0, < 2.0.0` -- `^2.3` is equivalent to `>= 2.3, < 3` -- `^2.x` is equivalent to `>= 2.0.0, < 3` -- `^0.2.3` is equivalent to `>=0.2.3 <0.3.0` -- `^0.2` is equivalent to `>=0.2.0 <0.3.0` -- `^0.0.3` is equivalent to `>=0.0.3 <0.0.4` -- `^0.0` is equivalent to `>=0.0.0 <0.1.0` -- `^0` is equivalent to `>=0.0.0 <1.0.0` diff --git a/go.mod b/go.mod index 7bddeb07..88b88463 100644 --- a/go.mod +++ b/go.mod @@ -32,9 +32,11 @@ require github.com/pkg/errors v0.9.1 // indirect require ( firebase.google.com/go/v4 v4.16.1 github.com/SherClockHolmes/webpush-go v1.4.0 + github.com/google/uuid v1.6.0 github.com/microcosm-cc/bluemonday v1.0.27 github.com/prometheus/client_golang v1.22.0 github.com/stripe/stripe-go/v74 v74.30.0 + golang.org/x/text v0.26.0 ) require ( @@ -67,7 +69,6 @@ require ( github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/s2a-go v0.1.9 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/googleapis/gax-go/v2 v2.14.2 // indirect github.com/gorilla/css v1.0.1 // indirect @@ -93,7 +94,6 @@ require ( go.opentelemetry.io/otel/trace v1.37.0 // indirect golang.org/x/net v0.41.0 // indirect golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.26.0 // indirect google.golang.org/appengine/v2 v2.0.6 // indirect google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect diff --git a/server/server.go b/server/server.go index 94461fbb..7e5fbb94 100644 --- a/server/server.go +++ b/server/server.go @@ -1133,11 +1133,7 @@ func replaceTemplate(tpl string, source string) (string, error) { if err := json.Unmarshal([]byte(source), &data); err != nil { return "", errHTTPBadRequestTemplateMessageNotJSON } - sprigFuncs := sprig.FuncMap() - // remove unsafe functions - delete(sprigFuncs, "env") - delete(sprigFuncs, "expandenv") - t, err := template.New("").Funcs(sprigFuncs).Parse(tpl) + t, err := template.New("").Funcs(sprig.FuncMap()).Parse(tpl) if err != nil { return "", errHTTPBadRequestTemplateInvalid } diff --git a/util/sprig/defaults.go b/util/sprig/defaults.go index 201b7e24..6a828a2a 100644 --- a/util/sprig/defaults.go +++ b/util/sprig/defaults.go @@ -3,16 +3,10 @@ package sprig import ( "bytes" "encoding/json" - "math/rand" "reflect" "strings" - "time" ) -func init() { - rand.Seed(time.Now().UnixNano()) -} - // dfault checks whether `given` is set, and returns default if not set. // // This returns `d` if `given` appears not to be set, and `given` otherwise. diff --git a/util/sprig/functions.go b/util/sprig/functions.go index 8549e99c..3ea46924 100644 --- a/util/sprig/functions.go +++ b/util/sprig/functions.go @@ -11,6 +11,8 @@ import ( "strings" ttemplate "text/template" "time" + + "golang.org/x/text/cases" ) // FuncMap produces the function map. @@ -107,7 +109,7 @@ var genericMap = map[string]interface{}{ "trim": strings.TrimSpace, "upper": strings.ToUpper, "lower": strings.ToLower, - "title": strings.Title, + "title": cases.Title, "substr": substring, // Switch order so that "foo" | repeat 5 "repeat": func(count int, str string) string { return strings.Repeat(str, count) }, diff --git a/util/sprig/list.go b/util/sprig/list.go index ca0fbb78..f4e95dda 100644 --- a/util/sprig/list.go +++ b/util/sprig/list.go @@ -39,7 +39,7 @@ func mustPush(list interface{}, v interface{}) ([]interface{}, error) { return append(nl, v), nil default: - return nil, fmt.Errorf("Cannot push on type %s", tp) + return nil, fmt.Errorf("cannot push on type %s", tp) } } @@ -69,7 +69,7 @@ func mustPrepend(list interface{}, v interface{}) ([]interface{}, error) { return append([]interface{}{v}, nl...), nil default: - return nil, fmt.Errorf("Cannot prepend on type %s", tp) + return nil, fmt.Errorf("cannot prepend on type %s", tp) } } @@ -113,7 +113,7 @@ func mustChunk(size int, list interface{}) ([][]interface{}, error) { return nl, nil default: - return nil, fmt.Errorf("Cannot chunk type %s", tp) + return nil, fmt.Errorf("cannot chunk type %s", tp) } } @@ -139,7 +139,7 @@ func mustLast(list interface{}) (interface{}, error) { return l2.Index(l - 1).Interface(), nil default: - return nil, fmt.Errorf("Cannot find last on type %s", tp) + return nil, fmt.Errorf("cannot find last on type %s", tp) } } @@ -165,7 +165,7 @@ func mustFirst(list interface{}) (interface{}, error) { return l2.Index(0).Interface(), nil default: - return nil, fmt.Errorf("Cannot find first on type %s", tp) + return nil, fmt.Errorf("cannot find first on type %s", tp) } } @@ -196,7 +196,7 @@ func mustRest(list interface{}) ([]interface{}, error) { return nl, nil default: - return nil, fmt.Errorf("Cannot find rest on type %s", tp) + return nil, fmt.Errorf("cannot find rest on type %s", tp) } } @@ -227,7 +227,7 @@ func mustInitial(list interface{}) ([]interface{}, error) { return nl, nil default: - return nil, fmt.Errorf("Cannot find initial on type %s", tp) + return nil, fmt.Errorf("cannot find initial on type %s", tp) } } @@ -267,7 +267,7 @@ func mustReverse(v interface{}) ([]interface{}, error) { return nl, nil default: - return nil, fmt.Errorf("Cannot find reverse on type %s", tp) + return nil, fmt.Errorf("cannot find reverse on type %s", tp) } } @@ -298,7 +298,7 @@ func mustCompact(list interface{}) ([]interface{}, error) { return nl, nil default: - return nil, fmt.Errorf("Cannot compact on type %s", tp) + return nil, fmt.Errorf("cannot compact on type %s", tp) } } @@ -329,7 +329,7 @@ func mustUniq(list interface{}) ([]interface{}, error) { return dest, nil default: - return nil, fmt.Errorf("Cannot find uniq on type %s", tp) + return nil, fmt.Errorf("cannot find uniq on type %s", tp) } } @@ -369,7 +369,7 @@ func mustWithout(list interface{}, omit ...interface{}) ([]interface{}, error) { return res, nil default: - return nil, fmt.Errorf("Cannot find without on type %s", tp) + return nil, fmt.Errorf("cannot find without on type %s", tp) } } @@ -401,7 +401,7 @@ func mustHas(needle interface{}, haystack interface{}) (bool, error) { return false, nil default: - return false, fmt.Errorf("Cannot find has on type %s", tp) + return false, fmt.Errorf("cannot find has on type %s", tp) } } @@ -457,7 +457,7 @@ func concat(lists ...interface{}) interface{} { res = append(res, l2.Index(i).Interface()) } default: - panic(fmt.Sprintf("Cannot concat type %s as list", tp)) + panic(fmt.Sprintf("cannot concat type %s as list", tp)) } } return res diff --git a/util/sprig/numeric_test.go b/util/sprig/numeric_test.go index 94e8a6d4..573873d8 100644 --- a/util/sprig/numeric_test.go +++ b/util/sprig/numeric_test.go @@ -101,7 +101,7 @@ func TestToFloat64(t *testing.T) { if target != toFloat64("102") { t.Errorf("Expected 102") } - if 0 != toFloat64("frankie") { + if toFloat64("frankie") != 0 { t.Errorf("Expected 0") } if target != toFloat64(uint16(102)) { @@ -110,10 +110,10 @@ func TestToFloat64(t *testing.T) { if target != toFloat64(uint64(102)) { t.Errorf("Expected 102") } - if 102.1234 != toFloat64(float64(102.1234)) { + if toFloat64(float64(102.1234)) != 102.1234 { t.Errorf("Expected 102.1234") } - if 1 != toFloat64(true) { + if toFloat64(true) != 1 { t.Errorf("Expected 102") } } @@ -137,7 +137,7 @@ func TestToInt64(t *testing.T) { if target != toInt64("102") { t.Errorf("Expected 102") } - if 0 != toInt64("frankie") { + if toInt64("frankie") != 0 { t.Errorf("Expected 0") } if target != toInt64(uint16(102)) { @@ -149,7 +149,7 @@ func TestToInt64(t *testing.T) { if target != toInt64(float64(102.1234)) { t.Errorf("Expected 102") } - if 1 != toInt64(true) { + if toInt64(true) != 1 { t.Errorf("Expected 102") } } @@ -174,7 +174,7 @@ func TestToInt(t *testing.T) { if target != toInt("102") { t.Errorf("Expected 102") } - if 0 != toInt("frankie") { + if toInt("frankie") != 0 { t.Errorf("Expected 0") } if target != toInt(uint16(102)) { @@ -186,7 +186,7 @@ func TestToInt(t *testing.T) { if target != toInt(float64(102.1234)) { t.Errorf("Expected 102") } - if 1 != toInt(true) { + if toInt(true) != 1 { t.Errorf("Expected 102") } }