142 lines
4.3 KiB
Go
142 lines
4.3 KiB
Go
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||
|
|
|
||
|
|
package handler
|
||
|
|
|
||
|
|
import (
|
||
|
|
"encoding/json"
|
||
|
|
"io/fs"
|
||
|
|
"net/http"
|
||
|
|
"net/url"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
"kode.naiv.no/olemd/favoritter/internal/middleware"
|
||
|
|
"kode.naiv.no/olemd/favoritter/internal/render"
|
||
|
|
"kode.naiv.no/olemd/favoritter/web"
|
||
|
|
)
|
||
|
|
|
||
|
|
// handleManifest serves the Web App Manifest with dynamic BasePath injection.
|
||
|
|
func (h *Handler) handleManifest(w http.ResponseWriter, r *http.Request) {
|
||
|
|
bp := h.deps.Config.BasePath
|
||
|
|
|
||
|
|
manifest := map[string]any{
|
||
|
|
"name": h.deps.Config.SiteName,
|
||
|
|
"short_name": h.deps.Config.SiteName,
|
||
|
|
"description": "Lagre og del dine favoritter",
|
||
|
|
"start_url": bp + "/",
|
||
|
|
"scope": bp + "/",
|
||
|
|
"display": "standalone",
|
||
|
|
"background_color": "#ffffff",
|
||
|
|
"theme_color": "#1095c1",
|
||
|
|
"icons": []map[string]any{
|
||
|
|
{"src": bp + "/static/icons/icon-192.png", "sizes": "192x192", "type": "image/png"},
|
||
|
|
{"src": bp + "/static/icons/icon-512.png", "sizes": "512x512", "type": "image/png"},
|
||
|
|
{"src": bp + "/static/icons/icon-512-maskable.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable"},
|
||
|
|
},
|
||
|
|
"share_target": map[string]any{
|
||
|
|
"action": bp + "/share",
|
||
|
|
"method": "GET",
|
||
|
|
"params": map[string]string{
|
||
|
|
"url": "url",
|
||
|
|
"text": "text",
|
||
|
|
"title": "title",
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
w.Header().Set("Content-Type", "application/manifest+json")
|
||
|
|
json.NewEncoder(w).Encode(manifest)
|
||
|
|
}
|
||
|
|
|
||
|
|
// handleServiceWorker serves the service worker JS from root scope.
|
||
|
|
// BasePath is injected via placeholder replacement so the SW can
|
||
|
|
// cache the correct static asset paths.
|
||
|
|
func (h *Handler) handleServiceWorker(w http.ResponseWriter, r *http.Request) {
|
||
|
|
staticFS, err := fs.Sub(web.StaticFS, "static")
|
||
|
|
if err != nil {
|
||
|
|
http.Error(w, "Not found", http.StatusNotFound)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
data, err := fs.ReadFile(staticFS, "sw.js")
|
||
|
|
if err != nil {
|
||
|
|
http.Error(w, "Not found", http.StatusNotFound)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
content := strings.ReplaceAll(string(data), "{{BASE_PATH}}", h.deps.Config.BasePath)
|
||
|
|
|
||
|
|
w.Header().Set("Content-Type", "application/javascript")
|
||
|
|
w.Header().Set("Cache-Control", "no-cache")
|
||
|
|
w.Write([]byte(content))
|
||
|
|
}
|
||
|
|
|
||
|
|
// handleShare receives Android share intents via the Web Share Target API
|
||
|
|
// and redirects to the new-fave form with pre-filled values.
|
||
|
|
// Uses GET to avoid CSRF issues (Android cannot provide CSRF tokens).
|
||
|
|
func (h *Handler) handleShare(w http.ResponseWriter, r *http.Request) {
|
||
|
|
user := middleware.UserFromContext(r.Context())
|
||
|
|
if user == nil {
|
||
|
|
http.Redirect(w, r, h.deps.Config.BasePath+"/login", http.StatusSeeOther)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
sharedURL := r.URL.Query().Get("url")
|
||
|
|
sharedTitle := r.URL.Query().Get("title")
|
||
|
|
sharedText := r.URL.Query().Get("text")
|
||
|
|
|
||
|
|
// Many Android apps send the URL in the "text" field instead of "url".
|
||
|
|
if sharedURL == "" && sharedText != "" {
|
||
|
|
sharedURL = extractURL(sharedText)
|
||
|
|
if sharedURL != "" {
|
||
|
|
// Remove the URL from text so it's not duplicated.
|
||
|
|
sharedText = strings.TrimSpace(strings.Replace(sharedText, sharedURL, "", 1))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
description := sharedTitle
|
||
|
|
if description == "" && sharedText != "" {
|
||
|
|
description = sharedText
|
||
|
|
sharedText = "" // Don't duplicate into notes.
|
||
|
|
}
|
||
|
|
|
||
|
|
target := h.deps.Config.BasePath + "/faves/new?"
|
||
|
|
params := url.Values{}
|
||
|
|
if sharedURL != "" {
|
||
|
|
params.Set("url", sharedURL)
|
||
|
|
}
|
||
|
|
if description != "" {
|
||
|
|
params.Set("description", description)
|
||
|
|
}
|
||
|
|
if sharedText != "" {
|
||
|
|
params.Set("notes", sharedText)
|
||
|
|
}
|
||
|
|
|
||
|
|
http.Redirect(w, r, target+params.Encode(), http.StatusSeeOther)
|
||
|
|
}
|
||
|
|
|
||
|
|
// handleFaveNewWithPreFill shows the new fave form, optionally pre-filled
|
||
|
|
// from query parameters (used by share target and bookmarklets).
|
||
|
|
func (h *Handler) handleFaveNewPreFill(w http.ResponseWriter, r *http.Request) {
|
||
|
|
user := middleware.UserFromContext(r.Context())
|
||
|
|
|
||
|
|
h.deps.Renderer.Page(w, r, "fave_form", render.PageData{
|
||
|
|
Title: "Ny favoritt",
|
||
|
|
Data: map[string]any{
|
||
|
|
"IsNew": true,
|
||
|
|
"DefaultPrivacy": user.DefaultFavePrivacy,
|
||
|
|
"Description": r.URL.Query().Get("description"),
|
||
|
|
"URL": r.URL.Query().Get("url"),
|
||
|
|
"Notes": r.URL.Query().Get("notes"),
|
||
|
|
},
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
// extractURL finds the first http:// or https:// URL in a string.
|
||
|
|
func extractURL(s string) string {
|
||
|
|
for _, word := range strings.Fields(s) {
|
||
|
|
if strings.HasPrefix(word, "http://") || strings.HasPrefix(word, "https://") {
|
||
|
|
return word
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return ""
|
||
|
|
}
|