2024-08-13 16:31:28 +02:00
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"crypto/rand"
|
|
|
|
|
"encoding/base64"
|
2024-10-08 22:11:06 +02:00
|
|
|
|
"encoding/json"
|
2024-10-28 10:52:39 +01:00
|
|
|
|
"fmt"
|
2024-08-13 16:31:28 +02:00
|
|
|
|
"html/template"
|
2024-10-28 10:52:39 +01:00
|
|
|
|
mathrand "math/rand"
|
2024-10-08 22:11:06 +02:00
|
|
|
|
"net/http"
|
2025-04-28 20:03:33 +02:00
|
|
|
|
"net/url"
|
2024-08-13 16:31:28 +02:00
|
|
|
|
"strings"
|
2024-10-14 22:15:38 +02:00
|
|
|
|
"time"
|
2024-08-13 16:31:28 +02:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
funcs = template.FuncMap{
|
|
|
|
|
"sub": func(a, b int) int {
|
|
|
|
|
return a - b
|
|
|
|
|
},
|
|
|
|
|
"add": func(a, b int) int {
|
|
|
|
|
return a + b
|
|
|
|
|
},
|
2024-10-08 22:11:06 +02:00
|
|
|
|
"translate": Translate,
|
|
|
|
|
"toJSON": func(v interface{}) (string, error) {
|
|
|
|
|
jsonBytes, err := json.Marshal(v)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
return string(jsonBytes), nil
|
|
|
|
|
},
|
2024-08-13 16:31:28 +02:00
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
2024-10-14 22:15:38 +02:00
|
|
|
|
type SearchEngine struct {
|
|
|
|
|
Name string
|
|
|
|
|
Func func(string, string, string, int) ([]SearchResult, time.Duration, error)
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-28 20:03:33 +02:00
|
|
|
|
type LinkParts struct {
|
2025-05-07 12:54:51 +02:00
|
|
|
|
Domain template.HTML
|
|
|
|
|
Path template.HTML
|
|
|
|
|
RootURL string // used by getFaviconProxyURL()
|
2025-04-28 20:03:33 +02:00
|
|
|
|
}
|
|
|
|
|
|
2024-10-08 22:11:06 +02:00
|
|
|
|
// Helper function to render templates without elapsed time measurement
|
|
|
|
|
func renderTemplate(w http.ResponseWriter, tmplName string, data map[string]interface{}) {
|
2024-10-28 10:52:39 +01:00
|
|
|
|
// Generate icon paths for SVG and PNG, including a 1/10 chance for an alternate icon
|
|
|
|
|
iconPathSVG, iconPathPNG := GetIconPath()
|
|
|
|
|
|
|
|
|
|
// Add icon paths to data map so they are available in all templates
|
|
|
|
|
if data == nil {
|
|
|
|
|
data = make(map[string]interface{})
|
|
|
|
|
}
|
|
|
|
|
data["IconPathSVG"] = iconPathSVG
|
|
|
|
|
data["IconPathPNG"] = iconPathPNG
|
|
|
|
|
|
|
|
|
|
// Parse and execute the template with shared functions
|
2024-10-08 22:11:06 +02:00
|
|
|
|
tmpl, err := template.New(tmplName).Funcs(funcs).ParseFiles("templates/" + tmplName)
|
|
|
|
|
if err != nil {
|
|
|
|
|
printErr("Error parsing template: %v", err)
|
|
|
|
|
http.Error(w, Translate("internal_server_error"), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Execute the template
|
|
|
|
|
err = tmpl.Execute(w, data)
|
|
|
|
|
if err != nil {
|
|
|
|
|
printErr("Error executing template: %v", err)
|
|
|
|
|
http.Error(w, Translate("internal_server_error"), http.StatusInternalServerError)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-26 07:46:03 +01:00
|
|
|
|
// Randoms string generator used for auth code
|
2024-08-13 16:31:28 +02:00
|
|
|
|
func generateStrongRandomString(length int) string {
|
|
|
|
|
bytes := make([]byte, length)
|
|
|
|
|
_, err := rand.Read(bytes)
|
|
|
|
|
if err != nil {
|
|
|
|
|
printErr("Error generating random string: %v", err)
|
|
|
|
|
}
|
|
|
|
|
return base64.URLEncoding.EncodeToString(bytes)[:length]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Checks if the URL already includes a protocol
|
|
|
|
|
func hasProtocol(url string) bool {
|
|
|
|
|
return strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Checks if the domain is a local address
|
|
|
|
|
func isLocalAddress(domain string) bool {
|
|
|
|
|
return domain == "localhost" || strings.HasPrefix(domain, "127.") || strings.HasPrefix(domain, "192.168.") || strings.HasPrefix(domain, "10.")
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-26 07:46:03 +01:00
|
|
|
|
// Ensures that HTTP or HTTPS is before the address if needed
|
2024-08-13 16:31:28 +02:00
|
|
|
|
func addProtocol(domain string) string {
|
|
|
|
|
if hasProtocol(domain) {
|
|
|
|
|
return domain
|
|
|
|
|
}
|
|
|
|
|
if isLocalAddress(domain) {
|
|
|
|
|
return "http://" + domain
|
|
|
|
|
}
|
|
|
|
|
return "https://" + domain
|
|
|
|
|
}
|
2024-10-28 10:52:39 +01:00
|
|
|
|
|
|
|
|
|
// GetIconPath returns both SVG and PNG icon paths, with a 1/10 chance for a randomly generated "alt" icon.
|
|
|
|
|
func GetIconPath() (string, string) {
|
|
|
|
|
// 1 in 10 chance to select an alt icon
|
|
|
|
|
if mathrand.Intn(10) == 0 {
|
|
|
|
|
// Generate a random number between 2 and 4
|
|
|
|
|
altIconNumber := 2 + mathrand.Intn(3) // mathrand.Intn(3) generates 0, 1, or 2
|
|
|
|
|
selectedAltIcon := "icon-alt" + fmt.Sprint(altIconNumber)
|
|
|
|
|
return "/static/images/" + selectedAltIcon + ".svg", "/static/images/" + selectedAltIcon + ".png"
|
|
|
|
|
}
|
|
|
|
|
// Default paths
|
|
|
|
|
return "/static/images/icon.svg", "/static/images/icon.png"
|
|
|
|
|
}
|
2025-04-24 16:56:41 +02:00
|
|
|
|
|
|
|
|
|
// FormatElapsedTime formats elapsed time as a string,
|
|
|
|
|
// using:
|
|
|
|
|
// - "> 0.01 ms" if under 49µs
|
|
|
|
|
// - "0.xx ms" if under 1ms
|
|
|
|
|
// - "xxx ms" if under 300ms
|
|
|
|
|
// - "x.xx seconds" otherwise
|
|
|
|
|
func FormatElapsedTime(elapsed time.Duration) string {
|
|
|
|
|
if elapsed < 49*time.Microsecond {
|
|
|
|
|
return fmt.Sprintf("> 0.01 %s", Translate("milliseconds"))
|
|
|
|
|
} else if elapsed < time.Millisecond {
|
|
|
|
|
ms := float64(elapsed.Microseconds()) / 1000.0
|
|
|
|
|
return fmt.Sprintf("%.2f %s", ms, Translate("milliseconds"))
|
|
|
|
|
} else if elapsed < 300*time.Millisecond {
|
|
|
|
|
return fmt.Sprintf("%d %s", elapsed.Milliseconds(), Translate("milliseconds"))
|
|
|
|
|
}
|
|
|
|
|
return fmt.Sprintf("%.2f %s", elapsed.Seconds(), Translate("seconds"))
|
|
|
|
|
}
|
2025-05-07 12:54:51 +02:00
|
|
|
|
func FormatURLParts(rawURL string) (domain, path, rootURL string) {
|
2025-04-28 20:03:33 +02:00
|
|
|
|
parsed, err := url.Parse(rawURL)
|
2025-05-07 12:54:51 +02:00
|
|
|
|
if err != nil || parsed.Host == "" {
|
|
|
|
|
return "", "", ""
|
2025-04-28 20:03:33 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
domain = parsed.Host
|
|
|
|
|
if strings.HasPrefix(domain, "www.") {
|
|
|
|
|
domain = domain[4:]
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-07 12:54:51 +02:00
|
|
|
|
rootURL = parsed.Scheme + "://" + parsed.Host
|
|
|
|
|
|
2025-04-28 20:03:33 +02:00
|
|
|
|
path = strings.Trim(parsed.Path, "/")
|
|
|
|
|
pathSegments := strings.Split(path, "/")
|
|
|
|
|
var cleanSegments []string
|
|
|
|
|
for _, seg := range pathSegments {
|
|
|
|
|
if seg != "" {
|
|
|
|
|
cleanSegments = append(cleanSegments, seg)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
path = strings.Join(cleanSegments, "/")
|
2025-05-07 12:54:51 +02:00
|
|
|
|
return domain, path, rootURL
|
2025-04-28 20:03:33 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func FormatLinkHTML(rawURL string) LinkParts {
|
2025-05-07 12:54:51 +02:00
|
|
|
|
domain, path, root := FormatURLParts(rawURL)
|
2025-04-28 20:03:33 +02:00
|
|
|
|
|
2025-05-07 12:54:51 +02:00
|
|
|
|
lp := LinkParts{
|
|
|
|
|
RootURL: root,
|
2025-04-28 20:03:33 +02:00
|
|
|
|
}
|
|
|
|
|
|
2025-05-07 12:54:51 +02:00
|
|
|
|
lp.Domain = template.HTML(fmt.Sprintf(`<span class="result-domain">%s</span>`, template.HTMLEscapeString(domain)))
|
2025-04-28 20:03:33 +02:00
|
|
|
|
|
2025-05-07 12:54:51 +02:00
|
|
|
|
if path != "" {
|
|
|
|
|
pathDisplay := strings.ReplaceAll(path, "/", " › ")
|
|
|
|
|
lp.Path = template.HTML(fmt.Sprintf(`<span class="result-path"> › %s</span>`, template.HTMLEscapeString(pathDisplay)))
|
2025-04-28 20:03:33 +02:00
|
|
|
|
}
|
2025-05-07 12:54:51 +02:00
|
|
|
|
|
|
|
|
|
return lp
|
2025-04-28 20:03:33 +02:00
|
|
|
|
}
|