1// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. 2// See LICENSE.txt for license information. 3 4package model 5 6import ( 7 "bytes" 8 "crypto/rand" 9 "encoding/base32" 10 "encoding/json" 11 "fmt" 12 "io" 13 "io/ioutil" 14 "net" 15 "net/http" 16 "net/mail" 17 "net/url" 18 "regexp" 19 "sort" 20 "strings" 21 "sync" 22 "time" 23 "unicode" 24 25 "github.com/mattermost/mattermost-server/v6/shared/i18n" 26 "github.com/pborman/uuid" 27) 28 29const ( 30 LowercaseLetters = "abcdefghijklmnopqrstuvwxyz" 31 UppercaseLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 32 NUMBERS = "0123456789" 33 SYMBOLS = " !\"\\#$%&'()*+,-./:;<=>?@[]^_`|~" 34) 35 36type StringInterface map[string]interface{} 37type StringArray []string 38 39func (sa StringArray) Remove(input string) StringArray { 40 for index := range sa { 41 if sa[index] == input { 42 ret := make(StringArray, 0, len(sa)-1) 43 ret = append(ret, sa[:index]...) 44 return append(ret, sa[index+1:]...) 45 } 46 } 47 return sa 48} 49 50func (sa StringArray) Contains(input string) bool { 51 for index := range sa { 52 if sa[index] == input { 53 return true 54 } 55 } 56 57 return false 58} 59func (sa StringArray) Equals(input StringArray) bool { 60 61 if len(sa) != len(input) { 62 return false 63 } 64 65 for index := range sa { 66 67 if sa[index] != input[index] { 68 return false 69 } 70 } 71 72 return true 73} 74 75var translateFunc i18n.TranslateFunc 76var translateFuncOnce sync.Once 77 78func AppErrorInit(t i18n.TranslateFunc) { 79 translateFuncOnce.Do(func() { 80 translateFunc = t 81 }) 82} 83 84type AppError struct { 85 Id string `json:"id"` 86 Message string `json:"message"` // Message to be display to the end user without debugging information 87 DetailedError string `json:"detailed_error"` // Internal error string to help the developer 88 RequestId string `json:"request_id,omitempty"` // The RequestId that's also set in the header 89 StatusCode int `json:"status_code,omitempty"` // The http status code 90 Where string `json:"-"` // The function where it happened in the form of Struct.Func 91 IsOAuth bool `json:"is_oauth,omitempty"` // Whether the error is OAuth specific 92 params map[string]interface{} 93} 94 95func (er *AppError) Error() string { 96 return er.Where + ": " + er.Message + ", " + er.DetailedError 97} 98 99func (er *AppError) Translate(T i18n.TranslateFunc) { 100 if T == nil { 101 er.Message = er.Id 102 return 103 } 104 105 if er.params == nil { 106 er.Message = T(er.Id) 107 } else { 108 er.Message = T(er.Id, er.params) 109 } 110} 111 112func (er *AppError) SystemMessage(T i18n.TranslateFunc) string { 113 if er.params == nil { 114 return T(er.Id) 115 } 116 return T(er.Id, er.params) 117} 118 119func (er *AppError) ToJSON() string { 120 b, _ := json.Marshal(er) 121 return string(b) 122} 123 124// AppErrorFromJSON will decode the input and return an AppError 125func AppErrorFromJSON(data io.Reader) *AppError { 126 str := "" 127 bytes, rerr := ioutil.ReadAll(data) 128 if rerr != nil { 129 str = rerr.Error() 130 } else { 131 str = string(bytes) 132 } 133 134 decoder := json.NewDecoder(strings.NewReader(str)) 135 var er AppError 136 err := decoder.Decode(&er) 137 if err != nil { 138 return NewAppError("AppErrorFromJSON", "model.utils.decode_json.app_error", nil, "body: "+str, http.StatusInternalServerError) 139 } 140 return &er 141} 142 143func NewAppError(where string, id string, params map[string]interface{}, details string, status int) *AppError { 144 ap := &AppError{} 145 ap.Id = id 146 ap.params = params 147 ap.Message = id 148 ap.Where = where 149 ap.DetailedError = details 150 ap.StatusCode = status 151 ap.IsOAuth = false 152 ap.Translate(translateFunc) 153 return ap 154} 155 156var encoding = base32.NewEncoding("ybndrfg8ejkmcpqxot1uwisza345h769") 157 158// NewId is a globally unique identifier. It is a [A-Z0-9] string 26 159// characters long. It is a UUID version 4 Guid that is zbased32 encoded 160// with the padding stripped off. 161func NewId() string { 162 var b bytes.Buffer 163 encoder := base32.NewEncoder(encoding, &b) 164 encoder.Write(uuid.NewRandom()) 165 encoder.Close() 166 b.Truncate(26) // removes the '==' padding 167 return b.String() 168} 169 170// NewRandomTeamName is a NewId that will be a valid team name. 171func NewRandomTeamName() string { 172 teamName := NewId() 173 for IsReservedTeamName(teamName) { 174 teamName = NewId() 175 } 176 return teamName 177} 178 179// NewRandomString returns a random string of the given length. 180// The resulting entropy will be (5 * length) bits. 181func NewRandomString(length int) string { 182 data := make([]byte, 1+(length*5/8)) 183 rand.Read(data) 184 return encoding.EncodeToString(data)[:length] 185} 186 187// GetMillis is a convenience method to get milliseconds since epoch. 188func GetMillis() int64 { 189 return time.Now().UnixNano() / int64(time.Millisecond) 190} 191 192// GetMillisForTime is a convenience method to get milliseconds since epoch for provided Time. 193func GetMillisForTime(thisTime time.Time) int64 { 194 return thisTime.UnixNano() / int64(time.Millisecond) 195} 196 197// GetTimeForMillis is a convenience method to get time.Time for milliseconds since epoch. 198func GetTimeForMillis(millis int64) time.Time { 199 return time.Unix(0, millis*int64(time.Millisecond)) 200} 201 202// PadDateStringZeros is a convenience method to pad 2 digit date parts with zeros to meet ISO 8601 format 203func PadDateStringZeros(dateString string) string { 204 parts := strings.Split(dateString, "-") 205 for index, part := range parts { 206 if len(part) == 1 { 207 parts[index] = "0" + part 208 } 209 } 210 dateString = strings.Join(parts[:], "-") 211 return dateString 212} 213 214// GetStartOfDayMillis is a convenience method to get milliseconds since epoch for provided date's start of day 215func GetStartOfDayMillis(thisTime time.Time, timeZoneOffset int) int64 { 216 localSearchTimeZone := time.FixedZone("Local Search Time Zone", timeZoneOffset) 217 resultTime := time.Date(thisTime.Year(), thisTime.Month(), thisTime.Day(), 0, 0, 0, 0, localSearchTimeZone) 218 return GetMillisForTime(resultTime) 219} 220 221// GetEndOfDayMillis is a convenience method to get milliseconds since epoch for provided date's end of day 222func GetEndOfDayMillis(thisTime time.Time, timeZoneOffset int) int64 { 223 localSearchTimeZone := time.FixedZone("Local Search Time Zone", timeZoneOffset) 224 resultTime := time.Date(thisTime.Year(), thisTime.Month(), thisTime.Day(), 23, 59, 59, 999999999, localSearchTimeZone) 225 return GetMillisForTime(resultTime) 226} 227 228func CopyStringMap(originalMap map[string]string) map[string]string { 229 copyMap := make(map[string]string, len(originalMap)) 230 for k, v := range originalMap { 231 copyMap[k] = v 232 } 233 return copyMap 234} 235 236// MapToJSON converts a map to a json string 237func MapToJSON(objmap map[string]string) string { 238 b, _ := json.Marshal(objmap) 239 return string(b) 240} 241 242// MapBoolToJSON converts a map to a json string 243func MapBoolToJSON(objmap map[string]bool) string { 244 b, _ := json.Marshal(objmap) 245 return string(b) 246} 247 248// MapFromJSON will decode the key/value pair map 249func MapFromJSON(data io.Reader) map[string]string { 250 decoder := json.NewDecoder(data) 251 252 var objmap map[string]string 253 if err := decoder.Decode(&objmap); err != nil { 254 return make(map[string]string) 255 } 256 return objmap 257} 258 259// MapFromJSON will decode the key/value pair map 260func MapBoolFromJSON(data io.Reader) map[string]bool { 261 decoder := json.NewDecoder(data) 262 263 var objmap map[string]bool 264 if err := decoder.Decode(&objmap); err != nil { 265 return make(map[string]bool) 266 } 267 return objmap 268} 269 270func ArrayToJSON(objmap []string) string { 271 b, _ := json.Marshal(objmap) 272 return string(b) 273} 274 275func ArrayFromJSON(data io.Reader) []string { 276 decoder := json.NewDecoder(data) 277 278 var objmap []string 279 if err := decoder.Decode(&objmap); err != nil { 280 return make([]string, 0) 281 } 282 return objmap 283} 284 285func ArrayFromInterface(data interface{}) []string { 286 stringArray := []string{} 287 288 dataArray, ok := data.([]interface{}) 289 if !ok { 290 return stringArray 291 } 292 293 for _, v := range dataArray { 294 if str, ok := v.(string); ok { 295 stringArray = append(stringArray, str) 296 } 297 } 298 299 return stringArray 300} 301 302func StringInterfaceToJSON(objmap map[string]interface{}) string { 303 b, _ := json.Marshal(objmap) 304 return string(b) 305} 306 307func StringInterfaceFromJSON(data io.Reader) map[string]interface{} { 308 decoder := json.NewDecoder(data) 309 310 var objmap map[string]interface{} 311 if err := decoder.Decode(&objmap); err != nil { 312 return make(map[string]interface{}) 313 } 314 return objmap 315} 316 317// ToJSON serializes an arbitrary data type to JSON, discarding the error. 318func ToJSON(v interface{}) []byte { 319 b, _ := json.Marshal(v) 320 return b 321} 322 323func GetServerIPAddress(iface string) string { 324 var addrs []net.Addr 325 if iface == "" { 326 var err error 327 addrs, err = net.InterfaceAddrs() 328 if err != nil { 329 return "" 330 } 331 } else { 332 interfaces, err := net.Interfaces() 333 if err != nil { 334 return "" 335 } 336 for _, i := range interfaces { 337 if i.Name == iface { 338 addrs, err = i.Addrs() 339 if err != nil { 340 return "" 341 } 342 break 343 } 344 } 345 } 346 347 for _, addr := range addrs { 348 349 if ip, ok := addr.(*net.IPNet); ok && !ip.IP.IsLoopback() && !ip.IP.IsLinkLocalUnicast() && !ip.IP.IsLinkLocalMulticast() { 350 if ip.IP.To4() != nil { 351 return ip.IP.String() 352 } 353 } 354 } 355 356 return "" 357} 358 359func isLower(s string) bool { 360 return strings.ToLower(s) == s 361} 362 363func IsValidEmail(email string) bool { 364 if !isLower(email) { 365 return false 366 } 367 368 if addr, err := mail.ParseAddress(email); err != nil { 369 return false 370 } else if addr.Name != "" { 371 // mail.ParseAddress accepts input of the form "Billy Bob <billy@example.com>" which we don't allow 372 return false 373 } 374 375 return true 376} 377 378var reservedName = []string{ 379 "admin", 380 "api", 381 "channel", 382 "claim", 383 "error", 384 "files", 385 "help", 386 "landing", 387 "login", 388 "mfa", 389 "oauth", 390 "plug", 391 "plugins", 392 "post", 393 "signup", 394 "boards", 395 "playbooks", 396} 397 398func IsValidChannelIdentifier(s string) bool { 399 400 if !IsValidAlphaNumHyphenUnderscore(s, true) { 401 return false 402 } 403 404 if len(s) < ChannelNameMinLength { 405 return false 406 } 407 408 return true 409} 410 411var ( 412 validAlphaNum = regexp.MustCompile(`^[a-z0-9]+([a-z\-0-9]+|(__)?)[a-z0-9]+$`) 413 validAlphaNumHyphenUnderscore = regexp.MustCompile(`^[a-z0-9]+([a-z\-\_0-9]+|(__)?)[a-z0-9]+$`) 414 validSimpleAlphaNumHyphenUnderscore = regexp.MustCompile(`^[a-zA-Z0-9\-_]+$`) 415 validSimpleAlphaNumHyphenUnderscorePlus = regexp.MustCompile(`^[a-zA-Z0-9+_-]+$`) 416) 417 418func isValidAlphaNum(s string) bool { 419 return validAlphaNum.MatchString(s) 420} 421 422func IsValidAlphaNumHyphenUnderscore(s string, withFormat bool) bool { 423 if withFormat { 424 return validAlphaNumHyphenUnderscore.MatchString(s) 425 } 426 return validSimpleAlphaNumHyphenUnderscore.MatchString(s) 427} 428 429func IsValidAlphaNumHyphenUnderscorePlus(s string) bool { 430 return validSimpleAlphaNumHyphenUnderscorePlus.MatchString(s) 431} 432 433func Etag(parts ...interface{}) string { 434 435 etag := CurrentVersion 436 437 for _, part := range parts { 438 etag += fmt.Sprintf(".%v", part) 439 } 440 441 return etag 442} 443 444var ( 445 validHashtag = regexp.MustCompile(`^(#\pL[\pL\d\-_.]*[\pL\d])$`) 446 puncStart = regexp.MustCompile(`^[^\pL\d\s#]+`) 447 hashtagStart = regexp.MustCompile(`^#{2,}`) 448 puncEnd = regexp.MustCompile(`[^\pL\d\s]+$`) 449) 450 451func ParseHashtags(text string) (string, string) { 452 words := strings.Fields(text) 453 454 hashtagString := "" 455 plainString := "" 456 for _, word := range words { 457 // trim off surrounding punctuation 458 word = puncStart.ReplaceAllString(word, "") 459 word = puncEnd.ReplaceAllString(word, "") 460 461 // and remove extra pound #s 462 word = hashtagStart.ReplaceAllString(word, "#") 463 464 if validHashtag.MatchString(word) { 465 hashtagString += " " + word 466 } else { 467 plainString += " " + word 468 } 469 } 470 471 if len(hashtagString) > 1000 { 472 hashtagString = hashtagString[:999] 473 lastSpace := strings.LastIndex(hashtagString, " ") 474 if lastSpace > -1 { 475 hashtagString = hashtagString[:lastSpace] 476 } else { 477 hashtagString = "" 478 } 479 } 480 481 return strings.TrimSpace(hashtagString), strings.TrimSpace(plainString) 482} 483 484func ClearMentionTags(post string) string { 485 post = strings.Replace(post, "<mention>", "", -1) 486 post = strings.Replace(post, "</mention>", "", -1) 487 return post 488} 489 490func IsValidHTTPURL(rawURL string) bool { 491 if strings.Index(rawURL, "http://") != 0 && strings.Index(rawURL, "https://") != 0 { 492 return false 493 } 494 495 if u, err := url.ParseRequestURI(rawURL); err != nil || u.Scheme == "" || u.Host == "" { 496 return false 497 } 498 499 return true 500} 501 502func IsValidId(value string) bool { 503 if len(value) != 26 { 504 return false 505 } 506 507 for _, r := range value { 508 if !unicode.IsLetter(r) && !unicode.IsNumber(r) { 509 return false 510 } 511 } 512 513 return true 514} 515 516// RemoveDuplicateStrings does an in-place removal of duplicate strings 517// from the input slice. The original slice gets modified. 518func RemoveDuplicateStrings(in []string) []string { 519 // In-place de-dup. 520 // Copied from https://github.com/golang/go/wiki/SliceTricks#in-place-deduplicate-comparable 521 if len(in) == 0 { 522 return in 523 } 524 sort.Strings(in) 525 j := 0 526 for i := 1; i < len(in); i++ { 527 if in[j] == in[i] { 528 continue 529 } 530 j++ 531 in[j] = in[i] 532 } 533 return in[:j+1] 534} 535 536func GetPreferredTimezone(timezone StringMap) string { 537 if timezone["useAutomaticTimezone"] == "true" { 538 return timezone["automaticTimezone"] 539 } 540 541 return timezone["manualTimezone"] 542} 543 544// SanitizeUnicode will remove undesirable Unicode characters from a string. 545func SanitizeUnicode(s string) string { 546 return strings.Map(filterBlocklist, s) 547} 548 549// filterBlocklist returns `r` if it is not in the blocklist, otherwise drop (-1). 550// Blocklist is taken from https://www.w3.org/TR/unicode-xml/#Charlist 551func filterBlocklist(r rune) rune { 552 const drop = -1 553 switch r { 554 case '\u0340', '\u0341': // clones of grave and acute; deprecated in Unicode 555 return drop 556 case '\u17A3', '\u17D3': // obsolete characters for Khmer; deprecated in Unicode 557 return drop 558 case '\u2028', '\u2029': // line and paragraph separator 559 return drop 560 case '\u202A', '\u202B', '\u202C', '\u202D', '\u202E': // BIDI embedding controls 561 return drop 562 case '\u206A', '\u206B': // activate/inhibit symmetric swapping; deprecated in Unicode 563 return drop 564 case '\u206C', '\u206D': // activate/inhibit Arabic form shaping; deprecated in Unicode 565 return drop 566 case '\u206E', '\u206F': // activate/inhibit national digit shapes; deprecated in Unicode 567 return drop 568 case '\uFFF9', '\uFFFA', '\uFFFB': // interlinear annotation characters 569 return drop 570 case '\uFEFF': // byte order mark 571 return drop 572 case '\uFFFC': // object replacement character 573 return drop 574 } 575 576 // Scoping for musical notation 577 if r >= 0x0001D173 && r <= 0x0001D17A { 578 return drop 579 } 580 581 // Language tag code points 582 if r >= 0x000E0000 && r <= 0x000E007F { 583 return drop 584 } 585 586 return r 587} 588