package search import ( "errors" "net/http" "regexp" "strconv" "strings" ) var languageCodeRe = regexp.MustCompile(`^[a-z]{2,3}(-[a-zA-Z]{2})?$`) func ParseSearchRequest(r *http.Request) (SearchRequest, error) { // Supports both GET and POST and relies on form values for routing. if err := r.ParseForm(); err != nil { return SearchRequest{}, errors.New("invalid request: cannot parse form") } format := strings.ToLower(r.FormValue("format")) switch OutputFormat(format) { case FormatJSON, FormatCSV, FormatRSS, FormatHTML: // explicit format — use as-is default: // No format specified: default to HTML for browser requests, JSON for API clients. accept := r.Header.Get("Accept") if strings.Contains(accept, "text/html") { format = string(FormatHTML) } else { format = string(FormatJSON) } } q := r.FormValue("q") if strings.TrimSpace(q) == "" { return SearchRequest{}, errors.New("missing required parameter: q") } pageno := 1 if s := strings.TrimSpace(r.FormValue("pageno")); s != "" { n, err := strconv.Atoi(s) if err != nil || n < 1 { return SearchRequest{}, errors.New("invalid parameter: pageno") } pageno = n } // MVP defaults. safesearch := 0 if s := strings.TrimSpace(r.FormValue("safesearch")); s != "" { n, err := strconv.Atoi(s) if err != nil || n < 0 || n > 2 { return SearchRequest{}, errors.New("invalid parameter: safesearch") } safesearch = n } var timeRange *string if tr := strings.TrimSpace(r.FormValue("time_range")); tr != "" && tr != "None" { switch tr { case "day", "week", "month", "year": tt := tr timeRange = &tt default: return SearchRequest{}, errors.New("invalid parameter: time_range") } } var timeoutLimit *float64 if s := strings.TrimSpace(r.FormValue("timeout_limit")); s != "" && s != "None" { v, err := strconv.ParseFloat(s, 64) if err != nil || v <= 0 { return SearchRequest{}, errors.New("invalid parameter: timeout_limit") } timeoutLimit = &v } language := strings.TrimSpace(r.FormValue("language")) if language == "" { language = "auto" } switch language { case "auto", "all": // ok default: if !languageCodeRe.MatchString(language) { return SearchRequest{}, errors.New("invalid parameter: language") } } // engines is an explicit list of engine names. engines := splitCSV(strings.TrimSpace(r.FormValue("engines"))) // categories and category_ params mirror the webadapter parsing. // We don't validate against a registry here; we just preserve the requested values. catSet := map[string]bool{} if catsParam := strings.TrimSpace(r.FormValue("categories")); catsParam != "" { for _, cat := range splitCSV(catsParam) { catSet[cat] = true } } for k, v := range r.Form { if !strings.HasPrefix(k, "category_") { continue } category := strings.TrimPrefix(k, "category_") if category == "" { continue } val := "" if len(v) > 0 { val = strings.TrimSpace(v[0]) } if val == "" || val != "off" { catSet[category] = true } else { delete(catSet, category) } } categories := make([]string, 0, len(catSet)) for c := range catSet { categories = append(categories, c) } if len(categories) == 0 { categories = []string{"general"} } // Parse engine_data--= parameters. engineData := map[string]map[string]string{} for k, v := range r.Form { if !strings.HasPrefix(k, "engine_data-") { continue } parts := strings.SplitN(k, "-", 3) // engine_data-- if len(parts) != 3 { continue } engine := parts[1] key := parts[2] // For HTML forms, r.Form[k] can contain multiple values; keep first. val := "" if len(v) > 0 { val = v[0] } if _, ok := engineData[engine]; !ok { engineData[engine] = map[string]string{} } engineData[engine][key] = val } accessToken := parseAccessToken(r) return SearchRequest{ Format: OutputFormat(format), Query: q, Pageno: pageno, Safesearch: safesearch, TimeRange: timeRange, TimeoutLimit: timeoutLimit, Language: language, Engines: engines, Categories: categories, EngineData: engineData, AccessToken: accessToken, }, nil } func splitCSV(s string) []string { if s == "" { return nil } raw := strings.Split(s, ",") out := make([]string, 0, len(raw)) for _, item := range raw { item = strings.TrimSpace(item) if item == "" { continue } out = append(out, item) } return out } func parseAccessToken(r *http.Request) string { // Supported sources (first non-empty wins): // - `Authorization: Bearer ` // - `X-Search-Token` / `X-Brave-Access-Token` // - `token` form value if auth := r.Header.Get("Authorization"); auth != "" { const prefix = "Bearer " if len(auth) > len(prefix) && auth[:len(prefix)] == prefix { return strings.TrimSpace(auth[len(prefix):]) } } if v := strings.TrimSpace(r.Header.Get("X-Search-Token")); v != "" { return v } if v := strings.TrimSpace(r.Header.Get("X-Brave-Access-Token")); v != "" { return v } if v := strings.TrimSpace(r.FormValue("token")); v != "" { return v } return "" }