Files
sm-sitemap-plugin/classes/rewrite.go
Jakub Zych b9804ddf17 fix(17): T-17-15 apply the frontend_url rewrite only for a safe http(s) URL
- models.AllowedFrontendURL: empty, or an absolute http(s) URL with a host
  and without whitespace, control characters, a double quote, <, > or &
- classes.Rewrite leaves the document unchanged for a refused value,
  inserts through EscapeText/EscapeAttr and keeps a loc or href that would
  not stay an http(s) URL
- TestRewriteUnsafeFrontendURL, TestAllowedFrontendURL and a stored-row
  check in TestSitemapImportSmoke
2026-10-13 00:57:02 +02:00

70 lines
2.5 KiB
Go

package classes
import (
"regexp"
"strings"
"git.golem15.com/golem15/sm-sitemap-plugin/models"
)
// The PHP routes.php patterns of the frontend rewrite.
var (
locElement = regexp.MustCompile(`<loc>(.*?)</loc>`)
xhtmlLinkRef = regexp.MustCompile(`(<xhtml:link[^>]*href=")([^"]*)(")`)
)
// Rewrite is the frontend_url rewrite of PHP routes.php: with frontendURL
// set, backendURL is replaced literally (str_replace) inside every loc
// element, inside every xhtml:link href and in the exact stylesheet href.
// Both URLs are right-trimmed of slashes first. An empty frontendURL (or
// "0", which PHP reads as false) leaves xml unchanged.
//
// PHP inserted the stored text as it was. Three rules keep that text out of
// the document unless it is a plain http(s) URL (T-17-15):
//
// - a frontendURL that models.AllowedFrontendURL refuses leaves xml
// unchanged, whatever is stored in the settings row;
// - the frontend URL is inserted through EscapeText in loc and through
// EscapeAttr in the hrefs. Both are the identity for every accepted
// value, which keeps the PHP bytes;
// - a loc or xhtml:link href whose rewritten value does not start with
// http:// or https:// keeps its value. The generator applies the same
// filter before the rewrite.
func Rewrite(xml, backendURL, frontendURL string) string {
if frontendURL == "" || frontendURL == "0" {
return xml
}
if !models.AllowedFrontendURL(frontendURL) {
return xml
}
frontend := strings.TrimRight(frontendURL, "/")
backend := strings.TrimRight(backendURL, "/")
if backend == "" {
return xml
}
frontendText, frontendAttr := EscapeText(frontend), EscapeAttr(frontend)
xml = locElement.ReplaceAllStringFunc(xml, func(m string) string {
inner := m[len("<loc>") : len(m)-len("</loc>")]
rewritten := strings.ReplaceAll(inner, backend, frontendText)
if !httpURL(rewritten) {
return m
}
return "<loc>" + rewritten + "</loc>"
})
xml = xhtmlLinkRef.ReplaceAllStringFunc(xml, func(m string) string {
sub := xhtmlLinkRef.FindStringSubmatch(m)
rewritten := strings.ReplaceAll(sub[2], backend, frontendAttr)
if !httpURL(rewritten) {
return m
}
return sub[1] + rewritten + sub[3]
})
return strings.ReplaceAll(xml, `href="`+backend+`/sitemap.xsl"`, `href="`+frontendAttr+`/sitemap.xsl"`)
}
// BackendURL is PHP rtrim(url('/'), '/') for a request whose root URL is
// root: the URL the rewrite replaces.
func BackendURL(root string) string {
return strings.TrimRight(URLTo(root, "/"), "/")
}