mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-02 09:08:35 +02:00
fix(extensions): anchor URL handler patterns to the host
MatchesURL treated patterns as substrings of the whole URL, so "spotify.com" matched any URL embedding it in a query parameter and a hostile query string could route a link to the wrong handler. Patterns now anchor to the URL host (exact domain or subdomain, optional path prefix); "scheme:" patterns anchor to the URI front. With several matching handlers, FindURLHandler now breaks the tie via the user's metadata provider priority instead of Go's random map order.
This commit is contained in:
@@ -371,17 +371,57 @@ func (m *ExtensionManifest) HasURLHandler() bool {
|
||||
return m.URLHandler != nil && m.URLHandler.Enabled && len(m.URLHandler.Patterns) > 0
|
||||
}
|
||||
|
||||
// MatchesURL reports whether one of the handler's patterns matches the URL.
|
||||
// Web patterns are anchored to the URL's host (exact domain or subdomain,
|
||||
// optional path prefix) — never matched as a raw substring, so "spotify.com"
|
||||
// cannot match a URL that merely embeds it in a query parameter. Patterns
|
||||
// ending in ":" (e.g. "spotify:") match custom URI schemes as prefixes.
|
||||
func (m *ExtensionManifest) MatchesURL(urlStr string) bool {
|
||||
if !m.HasURLHandler() {
|
||||
return false
|
||||
}
|
||||
|
||||
urlStr = strings.ToLower(strings.TrimSpace(urlStr))
|
||||
parsed, parseErr := url.Parse(urlStr)
|
||||
|
||||
for _, pattern := range m.URLHandler.Patterns {
|
||||
pattern = strings.ToLower(strings.TrimSpace(pattern))
|
||||
if strings.Contains(urlStr, pattern) {
|
||||
return true
|
||||
if pattern == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Scheme patterns anchor to the front of the URI.
|
||||
if !strings.Contains(pattern, "/") && strings.HasSuffix(pattern, ":") {
|
||||
if strings.HasPrefix(urlStr, pattern) {
|
||||
return true
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if parseErr != nil || parsed.Host == "" {
|
||||
continue
|
||||
}
|
||||
host := parsed.Hostname()
|
||||
urlPath := parsed.Path
|
||||
if urlPath == "" {
|
||||
urlPath = "/"
|
||||
}
|
||||
|
||||
if idx := strings.Index(pattern, "://"); idx >= 0 {
|
||||
pattern = pattern[idx+3:]
|
||||
}
|
||||
patternHost, patternPath, hasPath := strings.Cut(pattern, "/")
|
||||
if patternHost == "" {
|
||||
continue
|
||||
}
|
||||
if host != patternHost && !strings.HasSuffix(host, "."+patternHost) {
|
||||
continue
|
||||
}
|
||||
if hasPath && patternPath != "" &&
|
||||
!strings.HasPrefix(urlPath, "/"+patternPath) {
|
||||
continue
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -133,16 +133,44 @@ func (m *extensionManager) SearchTracksWithMetadataProvidersForItemID(query stri
|
||||
return tracks, nil
|
||||
}
|
||||
|
||||
// FindURLHandler returns the enabled handler matching the URL. When several
|
||||
// extensions match (e.g. two Spotify handlers), the user's metadata provider
|
||||
// priority breaks the tie deterministically instead of Go's random map
|
||||
// iteration order.
|
||||
func (m *extensionManager) FindURLHandler(url string) *extensionProviderWrapper {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
matches := make([]*loadedExtension, 0, 2)
|
||||
for _, ext := range m.extensions {
|
||||
if ext.Enabled && ext.Manifest.MatchesURL(url) && ext.Error == "" {
|
||||
return newExtensionProviderWrapper(ext)
|
||||
matches = append(matches, ext)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
m.mu.RUnlock()
|
||||
|
||||
if len(matches) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(matches) > 1 {
|
||||
rank := map[string]int{}
|
||||
for i, id := range GetMetadataProviderPriority() {
|
||||
rank[strings.ToLower(strings.TrimSpace(id))] = i
|
||||
}
|
||||
sort.SliceStable(matches, func(i, j int) bool {
|
||||
ri, oki := rank[strings.ToLower(matches[i].ID)]
|
||||
rj, okj := rank[strings.ToLower(matches[j].ID)]
|
||||
switch {
|
||||
case oki && okj:
|
||||
return ri < rj
|
||||
case oki:
|
||||
return true
|
||||
case okj:
|
||||
return false
|
||||
default:
|
||||
return matches[i].ID < matches[j].ID
|
||||
}
|
||||
})
|
||||
}
|
||||
return newExtensionProviderWrapper(matches[0])
|
||||
}
|
||||
|
||||
type ExtURLHandleResultWithExtID struct {
|
||||
|
||||
@@ -777,3 +777,50 @@ func TestParseExtensionAuxiliaryResults(t *testing.T) {
|
||||
t.Fatalf("unexpected lyrics result: %+v", lyrics)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchesURLHostAnchored(t *testing.T) {
|
||||
manifest := &ExtensionManifest{
|
||||
URLHandler: &URLHandlerConfig{
|
||||
Enabled: true,
|
||||
Patterns: []string{"spotify.com", "deezer.page.link", "spotify:"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, urlStr := range []string{
|
||||
"https://open.spotify.com/track/abc",
|
||||
"https://spotify.com/track/abc",
|
||||
"HTTPS://OPEN.SPOTIFY.COM/track/ABC",
|
||||
"https://deezer.page.link/xyz",
|
||||
"spotify:track:abc123",
|
||||
} {
|
||||
if !manifest.MatchesURL(urlStr) {
|
||||
t.Fatalf("expected match for %q", urlStr)
|
||||
}
|
||||
}
|
||||
|
||||
for _, urlStr := range []string{
|
||||
// The old substring matching accepted all of these.
|
||||
"https://evil.example/?next=https://spotify.com/track/abc",
|
||||
"https://notspotify.com/track/abc",
|
||||
"https://spotify.com.evil.example/track/abc",
|
||||
"https://example.com/spotify.com",
|
||||
"not a url at all",
|
||||
} {
|
||||
if manifest.MatchesURL(urlStr) {
|
||||
t.Fatalf("expected no match for %q", urlStr)
|
||||
}
|
||||
}
|
||||
|
||||
withPath := &ExtensionManifest{
|
||||
URLHandler: &URLHandlerConfig{
|
||||
Enabled: true,
|
||||
Patterns: []string{"youtube.com/watch"},
|
||||
},
|
||||
}
|
||||
if !withPath.MatchesURL("https://www.youtube.com/watch?v=abc") {
|
||||
t.Fatal("expected host+path prefix to match")
|
||||
}
|
||||
if withPath.MatchesURL("https://www.youtube.com/playlist?list=abc") {
|
||||
t.Fatal("expected different path to not match")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user