// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. package model import ( "net/http" "regexp" "strings" "time" ) var searchTermPuncStart = regexp.MustCompile(`^[^\pL\d\s#"]+`) var searchTermPuncEnd = regexp.MustCompile(`[^\pL\d\s*"]+$`) type SearchParams struct { Terms string ExcludedTerms string IsHashtag bool InChannels []string ExcludedChannels []string FromUsers []string ExcludedUsers []string AfterDate string ExcludedAfterDate string BeforeDate string ExcludedBeforeDate string OnDate string ExcludedDate string OrTerms bool IncludeDeletedChannels bool TimeZoneOffset int // True if this search doesn't originate from a "current user". SearchWithoutUserId bool } // Returns the epoch timestamp of the start of the day specified by SearchParams.AfterDate func (p *SearchParams) GetAfterDateMillis() int64 { date, err := time.Parse("2006-01-02", PadDateStringZeros(p.AfterDate)) if err != nil { date = time.Now() } // travel forward 1 day oneDay := time.Hour * 24 afterDate := date.Add(oneDay) return GetStartOfDayMillis(afterDate, p.TimeZoneOffset) } // Returns the epoch timestamp of the start of the day specified by SearchParams.ExcludedAfterDate func (p *SearchParams) GetExcludedAfterDateMillis() int64 { date, err := time.Parse("2006-01-02", PadDateStringZeros(p.ExcludedAfterDate)) if err != nil { date = time.Now() } // travel forward 1 day oneDay := time.Hour * 24 afterDate := date.Add(oneDay) return GetStartOfDayMillis(afterDate, p.TimeZoneOffset) } // Returns the epoch timestamp of the end of the day specified by SearchParams.BeforeDate func (p *SearchParams) GetBeforeDateMillis() int64 { date, err := time.Parse("2006-01-02", PadDateStringZeros(p.BeforeDate)) if err != nil { return 0 } // travel back 1 day oneDay := time.Hour * -24 beforeDate := date.Add(oneDay) return GetEndOfDayMillis(beforeDate, p.TimeZoneOffset) } // Returns the epoch timestamp of the end of the day specified by SearchParams.ExcludedBeforeDate func (p *SearchParams) GetExcludedBeforeDateMillis() int64 { date, err := time.Parse("2006-01-02", PadDateStringZeros(p.ExcludedBeforeDate)) if err != nil { return 0 } // travel back 1 day oneDay := time.Hour * -24 beforeDate := date.Add(oneDay) return GetEndOfDayMillis(beforeDate, p.TimeZoneOffset) } // Returns the epoch timestamps of the start and end of the day specified by SearchParams.OnDate func (p *SearchParams) GetOnDateMillis() (int64, int64) { date, err := time.Parse("2006-01-02", PadDateStringZeros(p.OnDate)) if err != nil { return 0, 0 } return GetStartOfDayMillis(date, p.TimeZoneOffset), GetEndOfDayMillis(date, p.TimeZoneOffset) } // Returns the epoch timestamps of the start and end of the day specified by SearchParams.ExcludedDate func (p *SearchParams) GetExcludedDateMillis() (int64, int64) { date, err := time.Parse("2006-01-02", PadDateStringZeros(p.ExcludedDate)) if err != nil { return 0, 0 } return GetStartOfDayMillis(date, p.TimeZoneOffset), GetEndOfDayMillis(date, p.TimeZoneOffset) } var searchFlags = [...]string{"from", "channel", "in", "before", "after", "on"} type flag struct { name string value string exclude bool } type searchWord struct { value string exclude bool } func splitWords(text string) []string { words := []string{} foundQuote := false location := 0 for i, char := range text { if char == '"' { if foundQuote { // Grab the quoted section word := text[location : i+1] words = append(words, word) foundQuote = false location = i + 1 } else { nextStart := i if i > 0 && text[i-1] == '-' { nextStart = i - 1 } words = append(words, strings.Fields(text[location:nextStart])...) foundQuote = true location = nextStart } } } words = append(words, strings.Fields(text[location:])...) return words } func parseSearchFlags(input []string) ([]searchWord, []flag) { words := []searchWord{} flags := []flag{} skipNextWord := false for i, word := range input { if skipNextWord { skipNextWord = false continue } isFlag := false if colon := strings.Index(word, ":"); colon != -1 { var flagName string var exclude bool if strings.HasPrefix(word, "-") { flagName = word[1:colon] exclude = true } else { flagName = word[:colon] exclude = false } value := word[colon+1:] for _, searchFlag := range searchFlags { // check for case insensitive equality if strings.EqualFold(flagName, searchFlag) { if value != "" { flags = append(flags, flag{ searchFlag, value, exclude, }) isFlag = true } else if i < len(input)-1 { flags = append(flags, flag{ searchFlag, input[i+1], exclude, }) skipNextWord = true isFlag = true } if isFlag { break } } } } if !isFlag { exclude := false if strings.HasPrefix(word, "-") { exclude = true } // trim off surrounding punctuation (note that we leave trailing asterisks to allow wildcards) word = searchTermPuncStart.ReplaceAllString(word, "") word = searchTermPuncEnd.ReplaceAllString(word, "") // and remove extra pound #s word = hashtagStart.ReplaceAllString(word, "#") if len(word) != 0 { words = append(words, searchWord{ word, exclude, }) } } } return words, flags } func ParseSearchParams(text string, timeZoneOffset int) []*SearchParams { words, flags := parseSearchFlags(splitWords(text)) hashtagTermList := []string{} excludedHashtagTermList := []string{} plainTermList := []string{} excludedPlainTermList := []string{} for _, word := range words { if validHashtag.MatchString(word.value) { if word.exclude { excludedHashtagTermList = append(excludedHashtagTermList, word.value) } else { hashtagTermList = append(hashtagTermList, word.value) } } else { if word.exclude { excludedPlainTermList = append(excludedPlainTermList, word.value) } else { plainTermList = append(plainTermList, word.value) } } } hashtagTerms := strings.Join(hashtagTermList, " ") excludedHashtagTerms := strings.Join(excludedHashtagTermList, " ") plainTerms := strings.Join(plainTermList, " ") excludedPlainTerms := strings.Join(excludedPlainTermList, " ") inChannels := []string{} excludedChannels := []string{} fromUsers := []string{} excludedUsers := []string{} afterDate := "" excludedAfterDate := "" beforeDate := "" excludedBeforeDate := "" onDate := "" excludedDate := "" for _, flag := range flags { if flag.name == "in" || flag.name == "channel" { if flag.exclude { excludedChannels = append(excludedChannels, flag.value) } else { inChannels = append(inChannels, flag.value) } } else if flag.name == "from" { if flag.exclude { excludedUsers = append(excludedUsers, flag.value) } else { fromUsers = append(fromUsers, flag.value) } } else if flag.name == "after" { if flag.exclude { excludedAfterDate = flag.value } else { afterDate = flag.value } } else if flag.name == "before" { if flag.exclude { excludedBeforeDate = flag.value } else { beforeDate = flag.value } } else if flag.name == "on" { if flag.exclude { excludedDate = flag.value } else { onDate = flag.value } } } paramsList := []*SearchParams{} if len(plainTerms) > 0 || len(excludedPlainTerms) > 0 { paramsList = append(paramsList, &SearchParams{ Terms: plainTerms, ExcludedTerms: excludedPlainTerms, IsHashtag: false, InChannels: inChannels, ExcludedChannels: excludedChannels, FromUsers: fromUsers, ExcludedUsers: excludedUsers, AfterDate: afterDate, ExcludedAfterDate: excludedAfterDate, BeforeDate: beforeDate, ExcludedBeforeDate: excludedBeforeDate, OnDate: onDate, ExcludedDate: excludedDate, TimeZoneOffset: timeZoneOffset, }) } if len(hashtagTerms) > 0 || len(excludedHashtagTerms) > 0 { paramsList = append(paramsList, &SearchParams{ Terms: hashtagTerms, ExcludedTerms: excludedHashtagTerms, IsHashtag: true, InChannels: inChannels, ExcludedChannels: excludedChannels, FromUsers: fromUsers, ExcludedUsers: excludedUsers, AfterDate: afterDate, ExcludedAfterDate: excludedAfterDate, BeforeDate: beforeDate, ExcludedBeforeDate: excludedBeforeDate, OnDate: onDate, ExcludedDate: excludedDate, TimeZoneOffset: timeZoneOffset, }) } // special case for when no terms are specified but we still have a filter if len(plainTerms) == 0 && len(hashtagTerms) == 0 && len(excludedPlainTerms) == 0 && len(excludedHashtagTerms) == 0 && (len(inChannels) != 0 || len(fromUsers) != 0 || len(excludedChannels) != 0 || len(excludedUsers) != 0 || len(afterDate) != 0 || len(excludedAfterDate) != 0 || len(beforeDate) != 0 || len(excludedBeforeDate) != 0 || len(onDate) != 0 || len(excludedDate) != 0) { paramsList = append(paramsList, &SearchParams{ Terms: "", ExcludedTerms: "", IsHashtag: false, InChannels: inChannels, ExcludedChannels: excludedChannels, FromUsers: fromUsers, ExcludedUsers: excludedUsers, AfterDate: afterDate, ExcludedAfterDate: excludedAfterDate, BeforeDate: beforeDate, ExcludedBeforeDate: excludedBeforeDate, OnDate: onDate, ExcludedDate: excludedDate, TimeZoneOffset: timeZoneOffset, }) } return paramsList } func IsSearchParamsListValid(paramsList []*SearchParams) *AppError { // All SearchParams should have same IncludeDeletedChannels value. for _, params := range paramsList { if params.IncludeDeletedChannels != paramsList[0].IncludeDeletedChannels { return NewAppError("IsSearchParamsListValid", "model.search_params_list.is_valid.include_deleted_channels.app_error", nil, "", http.StatusInternalServerError) } } return nil }