package search import ( "sort" "strings" "time" "unicode/utf8" ) const ( maxDomainList = 20 maxHostLen = 253 searxSiteCap = 3 ) var timeRangeLong = map[string]string{ "day": "day", "d": "day", "week": "week", "w": "week", "month": "month", "m": "month", "year": "year", "y": "year", } // NormalizeSearch checks query-vs-queries and filter fields. errMsg is // "invalid query" or "invalid search"; hint is empty or a field name. func NormalizeSearch(query string, queries []string, f Filter) (qs []string, out Filter, errMsg, hint string) { qs, errMsg = normalizeQueries(query, queries) if errMsg != "" { return nil, Filter{}, errMsg, "" } out, hint = normalizeFilter(f) if hint != "" { return nil, Filter{}, "invalid search", hint } if out.ExactMatch { for _, q := range qs { if !hasQuotedPhrase(q) { return nil, Filter{}, "invalid search", "exact_match" } } } return qs, out, "", "" } func normalizeQueries(query string, queries []string) ([]string, string) { q := strings.TrimSpace(query) qs := make([]string, 0, len(queries)) for _, s := range queries { qs = append(qs, strings.TrimSpace(s)) } hasQ := q != "" hasQS := len(qs) > 0 if hasQ && hasQS { return nil, "invalid query" } if !hasQ && !hasQS { return nil, "invalid query" } if hasQ { qs = []string{q} } if len(qs) < 1 || len(qs) > 4 { return nil, "invalid query" } for _, s := range qs { if s == "" || utf8.RuneCountInString(s) > MaxQueryRunes { return nil, "invalid query" } } return qs, "" } func normalizeFilter(f Filter) (Filter, string) { inc, hint := normalizeDomains(f.IncludeDomains, "include_domains") if hint != "" { return Filter{}, hint } exc, hint := normalizeDomains(f.ExcludeDomains, "exclude_domains") if hint != "" { return Filter{}, hint } mode := strings.TrimSpace(f.IncludeDomainsMode) switch mode { case "", "restrict", "prefer": default: return Filter{}, "include_domains_mode" } if mode != "" && len(inc) == 0 { return Filter{}, "include_domains_mode" } if len(inc) > 0 && mode == "" { mode = "restrict" } tr := strings.TrimSpace(f.TimeRange) if tr != "" { long, ok := timeRangeLong[tr] if !ok { return Filter{}, "time_range" } tr = long } start := strings.TrimSpace(f.StartDate) end := strings.TrimSpace(f.EndDate) if start != "" { if !validDay(start) { return Filter{}, "start_date" } } if end != "" { if !validDay(end) { return Filter{}, "end_date" } } if (start != "" || end != "") && tr != "" { return Filter{}, "time_range" } if start != "" && end != "" { st, _ := time.Parse("2006-01-02", start) en, _ := time.Parse("2006-01-02", end) if en.Before(st) { return Filter{}, "end_date" } } topic := strings.TrimSpace(f.Topic) switch topic { case "", "general", "news", "finance": default: return Filter{}, "topic" } ms := f.MinScore if ms < 0 || ms > 1 { return Filter{}, "min_score" } return Filter{ IncludeDomains: inc, ExcludeDomains: exc, IncludeDomainsMode: mode, TimeRange: tr, StartDate: start, EndDate: end, Topic: topic, ExactMatch: f.ExactMatch, MinScore: ms, }, "" } func normalizeDomains(in []string, hint string) ([]string, string) { if len(in) == 0 { return nil, "" } out := make([]string, 0, len(in)) seen := map[string]bool{} for _, raw := range in { s := strings.TrimSpace(raw) if s == "" { continue } d, ok := normalizeDomain(s) if !ok { return nil, hint } if seen[d] { continue } seen[d] = true out = append(out, d) } if len(out) == 0 { return nil, "" } sort.Strings(out) if len(out) > maxDomainList { return nil, hint } return out, "" } func normalizeDomain(s string) (string, bool) { s = strings.ToLower(strings.TrimSpace(s)) s = strings.TrimSuffix(s, ".") if s == "" || len(s) > maxHostLen { return "", false } if strings.ContainsAny(s, " :/@*?#[]") { return "", false } labels := strings.Split(s, ".") if len(labels) == 0 { return "", false } for _, lab := range labels { if !validLabel(lab) { return "", false } } return s, true } func validLabel(lab string) bool { if lab == "" || len(lab) > 63 { return false } if lab[0] == '-' || lab[len(lab)-1] == '-' { return false } for i := 0; i < len(lab); i++ { c := lab[i] if c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '-' { continue } return false } return true } func validDay(s string) bool { t, err := time.Parse("2006-01-02", s) return err == nil && t.Format("2006-01-02") == s } // hasQuotedPhrase reports a balanced ASCII "…" pair with ≥1 rune inside and no unpaired quotes. func hasQuotedPhrase(q string) bool { in := false n := 0 pairs := 0 for _, r := range q { if r != '"' { if in { n++ } continue } if !in { in = true n = 0 continue } if n == 0 { return false } in = false pairs++ } return !in && pairs >= 1 } func QueryRuneLen(qs []string) int { n := 0 for _, q := range qs { n += utf8.RuneCountInString(q) } return n }