* create ChannelBookmarks table * ChannelBookmark model * channel bookamrks Store layer * add GetBookmarksForAllChannelByIdSince * add channel bookmarks to test store * Add channel bookmarks to app layer * remove index for createAt in channel bookmarks migrations * remove createAt from select channel bookmark query and enable store delete bookmark test * update reponse of UpdateBookmark * rename db migration files * channel bookmarks store update sort order * channel bookmarks app layer update sort order * fix lint & tests * Fix lint and introduce util functions to insert / remove from slice * remove model etag * i18n * defer remove file info after test run * Fix tests passing the request context * fix migrations * fix TestRetry * Add bookmark permissions (#25560) * Adds channel bookmarks permissions * Fix linter * Remove unnecessary empty lines * Remove scss change as it's not necessary anymore * Fix mock store * Fix mock store and add role entry * Fix test * Adds cypress test and update permissions migration to update admin roles * Adds channel bookmarks roles to default admin roles * Adds bookmark permissions to default role permissions constant in webapp * Update mmctl test * Update permission test after normalising the roles * fix store tests * fix app layer tests * Add new bookmark endpoint (#25624) * Adds channel bookmarks api scaffold and create endpoint * Applies review comments to the API docs * Adds websocket test to create channel bookmark --------- Co-authored-by: Mattermost Build <build@mattermost.com> * MM-54426 exclude Channel Bookmarks files from data retention (#25656) * Augment channel APIs to include bookmarks (#25567) * update files docs for server 9.4 * Adds update channel bookmark endpoint (#25653) * Adds update channel bookmark sort order endpoint (#25686) * Adds update channel bookmark endpoint * Updates edit app method to return the right deleted bookmark and adds tests * Adds the update channel bookmark sort order endpoint * Fix repeated test after merge * Assign right permissions to each test * Update store and app layer to return specific errors and add tests * Adds delete channel bookmark endpoint (#25693) * Updates edit app method to return the right deleted bookmark and adds tests * Fix repeated test after merge * Updates edit app method to return the right deleted bookmark and adds tests * Adds delete channel bookmark endpoint * Adds list channel bookmarks endpoint (#25700) * Add channel moderation to bookmarks (#25716) * fix migrations index * fix getChannelsForTeamForUser * fix getChannelsForTeamForUser * fix bad merge client4 * fix file api with bookmark permission * add ChannelBookmarks feature flag * add missing translations * Set DB column for type as enum * use custom type for bookmark query using sqlx * use transaction when saving bookmark * return NewErrNotFound instead of Sql.ErrNoRows * use squirrel for IN query * add a limit of 1K for records in GetBookmarksForAllChannelByIdSince * UpdateSortOrder with one single query instead of multiple updates * fix shadow declaration * fix channel bookmarks permission string definition in admin console * fix another shadow declaration * Fix model conversion * add SplitSliceInChunks * remove include bookmarks in channels api * Cap amount of bookmarks per channel * add etag back to get channels * feedback review * update file info when replacing a bookmark file * return 501 not implemented when the license is not available * add detail message when getting channel member on bookmark api * start audit before permission check on create bookmark api * use require.Eventuallyf for testing WS events * remove unnecessary log in app layer * use require instead of assert to avoid panics * enforce limit when querying bookmarks since * prevent to create/update bookmark if file is already attached * fix lint * delete file when a bookmark is deleted * Dot allow to set a fileId and a url at the same time to a bookmark * fix query to delete a file that belongs to a bookmark * do not patch the bookmark type * Server side FeatureFlag check (#26145) * use ff in server, set ff to false * turn on FF for unit tests * defer unset FF for unit tests * turn ff on for testing * only allow attaching files that were uploaded for bookmark * Set feature flag off as default * fix lint * update email templates as PR failed * revert templates * force the assignment of ID when creating a bookmark * Fix unit tests --------- Co-authored-by: Miguel de la Cruz <miguel@mcrx.me> Co-authored-by: Mattermost Build <build@mattermost.com> Co-authored-by: Caleb Roseland <caleb@calebroseland.com> Co-authored-by: Scott Bishel <scott.bishel@mattermost.com>
286 строки
6.5 KiB
Go
286 строки
6.5 KiB
Go
// 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"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
pUtils "github.com/mattermost/mattermost/server/public/utils"
|
|
)
|
|
|
|
// 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 !pUtils.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 InsertElementToSliceAtIndex[T comparable](slice []T, element T, index int) []T {
|
|
if len(slice) == index {
|
|
return append(slice, element)
|
|
}
|
|
slice = append(slice[:index+1], slice[index:]...)
|
|
slice[index] = element
|
|
return slice
|
|
}
|
|
|
|
func RemoveElementFromSliceAtIndex[T comparable](slice []T, index int) []T {
|
|
return append(slice[:index], slice[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 && (u.Scheme == "http" || u.Scheme == "https") {
|
|
if config.ServiceSettings.SiteURL != nil {
|
|
siteURL := *config.ServiceSettings.SiteURL
|
|
return strings.Index(strings.ToLower(redirectURL), strings.ToLower(siteURL)) == 0
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
func MinInt(a, b int) int {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
func MaxInt(a, b int) int {
|
|
if a > b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
// 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 := MaxInt(0, minResolution)
|
|
if n >= -9 && n <= 9 {
|
|
if resolution == 0 {
|
|
return int64(n)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
zeroes := int(math.Log10(math.Abs(n)))
|
|
resolution = MinInt(zeroes, resolution)
|
|
tens := int64(math.Pow10(resolution))
|
|
significantDigits := int64(n) / tens
|
|
return significantDigits * tens
|
|
}
|