magpie://import now takes icon=<https picture>: the vendor's logo, PNG, JPEG, GIF, WebP, ICO or SVG. It is fetched once, when the user opens the confirm dialog — never at parse time — into magpie's icons folder, and falls back to the catalog's logo or a plain mark when the fetch fails. An explicit icon= wins over whatever catalog or preset gave. The fetch is guarded on the way out: https only, at most 1 MB, the type checked, and the host is resolved at connect time with loopback, private, link-local and reserved addresses refused, so a name that points back at the user's own machine (or a DNS-rebinding trick) never connects. The page and the dialog never load a remote picture themselves. IconURL is a carrier between parsing and that fetch only; Save drops it, so it never reaches providers.json.
92 lines
2.3 KiB
Go
92 lines
2.3 KiB
Go
package gui
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/yetone/magpie/internal/provider"
|
|
)
|
|
|
|
// An import link (magpie://import?…, see provider.ParseImport) is parsed
|
|
// here, parked under a random id, and the window opens on the Providers tab
|
|
// with that id; the page fetches it once and asks the user before saving
|
|
// anything.
|
|
|
|
type importJSON struct {
|
|
Provider provider.Provider `json:"provider"`
|
|
Error string `json:"error,omitempty"`
|
|
Replaces string `json:"replaces,omitempty"` // the name of the provider it would replace
|
|
}
|
|
|
|
var imports = struct {
|
|
sync.Mutex
|
|
m map[string]importJSON
|
|
}{m: map[string]importJSON{}}
|
|
|
|
func stashImport(link string) string {
|
|
var in importJSON
|
|
p, err := provider.ParseImport(link)
|
|
if err != nil {
|
|
in.Error = err.Error()
|
|
} else {
|
|
in.Provider = p
|
|
}
|
|
b := make([]byte, 8)
|
|
_, _ = rand.Read(b)
|
|
id := hex.EncodeToString(b)
|
|
imports.Lock()
|
|
imports.m[id] = in
|
|
imports.Unlock()
|
|
return id
|
|
}
|
|
|
|
// ImportLink is the link among a launch's arguments, if any.
|
|
func ImportLink(args []string) string {
|
|
for _, a := range args {
|
|
if strings.HasPrefix(strings.ToLower(a), provider.Scheme+":") {
|
|
return a
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func importRoutes(mux *http.ServeMux) {
|
|
// Read once: the key is in there, and a reload should not ask again.
|
|
mux.HandleFunc("GET /api/import/{id}", func(rw http.ResponseWriter, r *http.Request) {
|
|
imports.Lock()
|
|
in, ok := imports.m[r.PathValue("id")]
|
|
delete(imports.m, r.PathValue("id"))
|
|
imports.Unlock()
|
|
if !ok {
|
|
http.NotFound(rw, r)
|
|
return
|
|
}
|
|
if in.Error == "" {
|
|
if old, err := provider.Find(in.Provider.ID); err == nil {
|
|
in.Replaces = old.Name
|
|
}
|
|
}
|
|
writeJSON(rw, in)
|
|
})
|
|
// fetch the picture a link named, once the user has the dialog open. The
|
|
// URL is validated again in provider.FetchIcon: this handler only ever
|
|
// fetches into the icons folder and answers with the reference.
|
|
mux.HandleFunc("POST /api/import/icon", func(rw http.ResponseWriter, r *http.Request) {
|
|
var in struct{ URL string }
|
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
|
fail(rw, err)
|
|
return
|
|
}
|
|
icon, err := provider.FetchIcon(r.Context(), in.URL)
|
|
if err != nil {
|
|
fail(rw, err)
|
|
return
|
|
}
|
|
writeJSON(rw, map[string]string{"icon": icon})
|
|
})
|
|
}
|