// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. package utils import ( "io" "math" "net" "net/http" "net/url" "slices" "strings" "github.com/pkg/errors" "github.com/mattermost/mattermost/server/public/model" ) // RemoveStringFromSlice removes the first occurrence of a from slice. func RemoveStringFromSlice(a string, slice []string) []string { for i, str := range slice { if str == a { return append(slice[:i], slice[i+1:]...) } } return slice } // RemoveStringsFromSlice removes all occurrences of strings from slice. func RemoveStringsFromSlice(slice []string, strings ...string) []string { newSlice := []string{} for _, item := range slice { if !slices.Contains(strings, item) { newSlice = append(newSlice, item) } } return newSlice } func StringArrayIntersection(arr1, arr2 []string) []string { arrMap := map[string]bool{} result := []string{} for _, value := range arr1 { arrMap[value] = true } for _, value := range arr2 { if arrMap[value] { result = append(result, value) } } return result } func RemoveDuplicatesFromStringArray(arr []string) []string { result := make([]string, 0, len(arr)) seen := make(map[string]bool) for _, item := range arr { if !seen[item] { result = append(result, item) seen[item] = true } } return result } func StringSliceDiff(a, b []string) []string { m := make(map[string]bool) result := []string{} for _, item := range b { m[item] = true } for _, item := range a { if !m[item] { result = append(result, item) } } return result } func RemoveElementFromSliceAtIndex[S ~[]E, E any](slice S, index int) S { return slices.Delete(slice, index, index+1) } func GetIPAddress(r *http.Request, trustedProxyIPHeader []string) string { address := "" for _, proxyHeader := range trustedProxyIPHeader { header := r.Header.Get(proxyHeader) if header != "" { addresses := strings.Split(header, ",") if len(addresses) > 0 { address = strings.TrimSpace(addresses[0]) } } if address != "" && net.ParseIP(address) != nil { return address } } host, _, _ := net.SplitHostPort(r.RemoteAddr) return host } func GetHostnameFromSiteURL(siteURL string) string { u, err := url.Parse(siteURL) if err != nil { return "" } return u.Hostname() } type RequestCache struct { Data []byte Date string Key string } // Fetch JSON data from the notices server // if skip is passed, does a fetch without touching the cache func GetURLWithCache(url string, cache *RequestCache, skip bool) ([]byte, error) { // Build a GET Request, including optional If-None-Match header. req, err := http.NewRequest("GET", url, nil) if err != nil { cache.Data = nil return nil, err } if !skip && cache.Data != nil { req.Header.Add("If-None-Match", cache.Key) req.Header.Add("If-Modified-Since", cache.Date) } client := &http.Client{} resp, err := client.Do(req) if err != nil { cache.Data = nil return nil, err } defer resp.Body.Close() // No change from latest known Etag? if resp.StatusCode == http.StatusNotModified { return cache.Data, nil } if resp.StatusCode != 200 { cache.Data = nil return nil, errors.Errorf("Fetching notices failed with status code %d", resp.StatusCode) } cache.Data, err = io.ReadAll(resp.Body) if err != nil { cache.Data = nil return nil, err } // If etags headers are missing, ignore. cache.Key = resp.Header.Get("ETag") cache.Date = resp.Header.Get("Date") return cache.Data, err } // Append tokens to passed baseURL as query params func AppendQueryParamsToURL(baseURL string, params map[string]string) string { u, err := url.Parse(baseURL) if err != nil { return "" } q, err := url.ParseQuery(u.RawQuery) if err != nil { return "" } for key, value := range params { q.Add(key, value) } u.RawQuery = q.Encode() return u.String() } // Validates RedirectURL passed during OAuth or SAML func IsValidWebAuthRedirectURL(config *model.Config, redirectURL string) bool { u, err := url.Parse(redirectURL) if err != nil || config.ServiceSettings.SiteURL == nil { return false } siteURL, err := url.Parse(*config.ServiceSettings.SiteURL) if err != nil { return false } if u.Scheme == siteURL.Scheme && u.Host == siteURL.Host { return true } return false } // Validates Mobile Custom URL Scheme passed during OAuth or SAML func IsValidMobileAuthRedirectURL(config *model.Config, redirectURL string) bool { for _, URLScheme := range config.NativeAppSettings.AppCustomURLSchemes { if strings.Index(strings.ToLower(redirectURL), strings.ToLower(URLScheme)) == 0 { return true } } return false } // This will only return TRUE if the request comes from a mobile running the Mobile App. // If the request comes from a mobile using the browser, it will return FALSE. func IsMobileRequest(r *http.Request) bool { userAgent := r.UserAgent() if userAgent == "" { return false } // Check if the User-Agent contain keywords found in mobile devices running the mobile App mobileKeywords := []string{"Mobile", "Android", "iOS", "iPhone", "iPad"} for _, keyword := range mobileKeywords { if strings.Contains(userAgent, keyword) { return true } } return false } // RoundOffToZeroes converts all digits to 0 except the 1st one. // Special case: If there is only 1 digit, then returns 0. func RoundOffToZeroes(n float64) int64 { if n >= -9 && n <= 9 { return 0 } zeroes := int(math.Log10(math.Abs(n))) tens := int64(math.Pow10(zeroes)) firstDigit := int64(n) / tens return firstDigit * tens } // RoundOffToZeroesResolution truncates off at most minResolution zero places. // It implicitly sets the lowest minResolution to 0. // e.g. 0 reports 1s, 1 reports 10s, 2 reports 100s, 3 reports 1000s func RoundOffToZeroesResolution(n float64, minResolution int) int64 { resolution := max(0, minResolution) if n >= -9 && n <= 9 { if resolution == 0 { return int64(n) } return 0 } zeroes := int(math.Log10(math.Abs(n))) resolution = min(zeroes, resolution) tens := int64(math.Pow10(resolution)) significantDigits := int64(n) / tens return significantDigits * tens }