github.com/mattermosttest/mattermost-server/v5@v5.0.0-20200917143240-9dfa12e121f9/model/utils.go (about) 1 // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. 2 // See LICENSE.txt for license information. 3 4 package model 5 6 import ( 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 "strconv" 20 "strings" 21 "time" 22 "unicode" 23 24 goi18n "github.com/mattermost/go-i18n/i18n" 25 "github.com/pborman/uuid" 26 ) 27 28 const ( 29 LOWERCASE_LETTERS = "abcdefghijklmnopqrstuvwxyz" 30 UPPERCASE_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 31 NUMBERS = "0123456789" 32 SYMBOLS = " !\"\\#$%&'()*+,-./:;<=>?@[]^_`|~" 33 ) 34 35 type StringInterface map[string]interface{} 36 type StringMap map[string]string 37 type StringArray []string 38 39 func (sa StringArray) Equals(input StringArray) bool { 40 41 if len(sa) != len(input) { 42 return false 43 } 44 45 for index := range sa { 46 47 if sa[index] != input[index] { 48 return false 49 } 50 } 51 52 return true 53 } 54 55 var translateFunc goi18n.TranslateFunc = nil 56 57 func AppErrorInit(t goi18n.TranslateFunc) { 58 translateFunc = t 59 } 60 61 type AppError struct { 62 Id string `json:"id"` 63 Message string `json:"message"` // Message to be display to the end user without debugging information 64 DetailedError string `json:"detailed_error"` // Internal error string to help the developer 65 RequestId string `json:"request_id,omitempty"` // The RequestId that's also set in the header 66 StatusCode int `json:"status_code,omitempty"` // The http status code 67 Where string `json:"-"` // The function where it happened in the form of Struct.Func 68 IsOAuth bool `json:"is_oauth,omitempty"` // Whether the error is OAuth specific 69 params map[string]interface{} 70 } 71 72 func (er *AppError) Error() string { 73 return er.Where + ": " + er.Message + ", " + er.DetailedError 74 } 75 76 func (er *AppError) Translate(T goi18n.TranslateFunc) { 77 if T == nil { 78 er.Message = er.Id 79 return 80 } 81 82 if er.params == nil { 83 er.Message = T(er.Id) 84 } else { 85 er.Message = T(er.Id, er.params) 86 } 87 } 88 89 func (er *AppError) SystemMessage(T goi18n.TranslateFunc) string { 90 if er.params == nil { 91 return T(er.Id) 92 } else { 93 return T(er.Id, er.params) 94 } 95 } 96 97 func (er *AppError) ToJson() string { 98 b, _ := json.Marshal(er) 99 return string(b) 100 } 101 102 // AppErrorFromJson will decode the input and return an AppError 103 func AppErrorFromJson(data io.Reader) *AppError { 104 str := "" 105 bytes, rerr := ioutil.ReadAll(data) 106 if rerr != nil { 107 str = rerr.Error() 108 } else { 109 str = string(bytes) 110 } 111 112 decoder := json.NewDecoder(strings.NewReader(str)) 113 var er AppError 114 err := decoder.Decode(&er) 115 if err == nil { 116 return &er 117 } else { 118 return NewAppError("AppErrorFromJson", "model.utils.decode_json.app_error", nil, "body: "+str, http.StatusInternalServerError) 119 } 120 } 121 122 func NewAppError(where string, id string, params map[string]interface{}, details string, status int) *AppError { 123 ap := &AppError{} 124 ap.Id = id 125 ap.params = params 126 ap.Message = id 127 ap.Where = where 128 ap.DetailedError = details 129 ap.StatusCode = status 130 ap.IsOAuth = false 131 ap.Translate(translateFunc) 132 return ap 133 } 134 135 var encoding = base32.NewEncoding("ybndrfg8ejkmcpqxot1uwisza345h769") 136 137 // NewId is a globally unique identifier. It is a [A-Z0-9] string 26 138 // characters long. It is a UUID version 4 Guid that is zbased32 encoded 139 // with the padding stripped off. 140 func NewId() string { 141 var b bytes.Buffer 142 encoder := base32.NewEncoder(encoding, &b) 143 encoder.Write(uuid.NewRandom()) 144 encoder.Close() 145 b.Truncate(26) // removes the '==' padding 146 return b.String() 147 } 148 149 // NewRandomTeamName is a NewId that will be a valid team name. 150 func NewRandomTeamName() string { 151 teamName := NewId() 152 for IsReservedTeamName(teamName) { 153 teamName = NewId() 154 } 155 return teamName 156 } 157 158 // NewRandomString returns a random string of the given length. 159 // The resulting entropy will be (5 * length) bits. 160 func NewRandomString(length int) string { 161 data := make([]byte, 1+(length*5/8)) 162 rand.Read(data) 163 return encoding.EncodeToString(data)[:length] 164 } 165 166 // NewRandomBase32String returns a base32 encoded string of a random slice 167 // of bytes of the given size. The resulting entropy will be (8 * size) bits. 168 func NewRandomBase32String(size int) string { 169 data := make([]byte, size) 170 rand.Read(data) 171 return base32.StdEncoding.EncodeToString(data) 172 } 173 174 // GetMillis is a convenience method to get milliseconds since epoch. 175 func GetMillis() int64 { 176 return time.Now().UnixNano() / int64(time.Millisecond) 177 } 178 179 // GetMillisForTime is a convenience method to get milliseconds since epoch for provided Time. 180 func GetMillisForTime(thisTime time.Time) int64 { 181 return thisTime.UnixNano() / int64(time.Millisecond) 182 } 183 184 // PadDateStringZeros is a convenience method to pad 2 digit date parts with zeros to meet ISO 8601 format 185 func PadDateStringZeros(dateString string) string { 186 parts := strings.Split(dateString, "-") 187 for index, part := range parts { 188 if len(part) == 1 { 189 parts[index] = "0" + part 190 } 191 } 192 dateString = strings.Join(parts[:], "-") 193 return dateString 194 } 195 196 // GetStartOfDayMillis is a convenience method to get milliseconds since epoch for provided date's start of day 197 func GetStartOfDayMillis(thisTime time.Time, timeZoneOffset int) int64 { 198 localSearchTimeZone := time.FixedZone("Local Search Time Zone", timeZoneOffset) 199 resultTime := time.Date(thisTime.Year(), thisTime.Month(), thisTime.Day(), 0, 0, 0, 0, localSearchTimeZone) 200 return GetMillisForTime(resultTime) 201 } 202 203 // GetEndOfDayMillis is a convenience method to get milliseconds since epoch for provided date's end of day 204 func GetEndOfDayMillis(thisTime time.Time, timeZoneOffset int) int64 { 205 localSearchTimeZone := time.FixedZone("Local Search Time Zone", timeZoneOffset) 206 resultTime := time.Date(thisTime.Year(), thisTime.Month(), thisTime.Day(), 23, 59, 59, 999999999, localSearchTimeZone) 207 return GetMillisForTime(resultTime) 208 } 209 210 func CopyStringMap(originalMap map[string]string) map[string]string { 211 copyMap := make(map[string]string) 212 for k, v := range originalMap { 213 copyMap[k] = v 214 } 215 return copyMap 216 } 217 218 // MapToJson converts a map to a json string 219 func MapToJson(objmap map[string]string) string { 220 b, _ := json.Marshal(objmap) 221 return string(b) 222 } 223 224 // MapBoolToJson converts a map to a json string 225 func MapBoolToJson(objmap map[string]bool) string { 226 b, _ := json.Marshal(objmap) 227 return string(b) 228 } 229 230 // MapFromJson will decode the key/value pair map 231 func MapFromJson(data io.Reader) map[string]string { 232 decoder := json.NewDecoder(data) 233 234 var objmap map[string]string 235 if err := decoder.Decode(&objmap); err != nil { 236 return make(map[string]string) 237 } else { 238 return objmap 239 } 240 } 241 242 // MapFromJson will decode the key/value pair map 243 func MapBoolFromJson(data io.Reader) map[string]bool { 244 decoder := json.NewDecoder(data) 245 246 var objmap map[string]bool 247 if err := decoder.Decode(&objmap); err != nil { 248 return make(map[string]bool) 249 } else { 250 return objmap 251 } 252 } 253 254 func ArrayToJson(objmap []string) string { 255 b, _ := json.Marshal(objmap) 256 return string(b) 257 } 258 259 func ArrayFromJson(data io.Reader) []string { 260 decoder := json.NewDecoder(data) 261 262 var objmap []string 263 if err := decoder.Decode(&objmap); err != nil { 264 return make([]string, 0) 265 } else { 266 return objmap 267 } 268 } 269 270 func ArrayFromInterface(data interface{}) []string { 271 stringArray := []string{} 272 273 dataArray, ok := data.([]interface{}) 274 if !ok { 275 return stringArray 276 } 277 278 for _, v := range dataArray { 279 if str, ok := v.(string); ok { 280 stringArray = append(stringArray, str) 281 } 282 } 283 284 return stringArray 285 } 286 287 func StringInterfaceToJson(objmap map[string]interface{}) string { 288 b, _ := json.Marshal(objmap) 289 return string(b) 290 } 291 292 func StringInterfaceFromJson(data io.Reader) map[string]interface{} { 293 decoder := json.NewDecoder(data) 294 295 var objmap map[string]interface{} 296 if err := decoder.Decode(&objmap); err != nil { 297 return make(map[string]interface{}) 298 } else { 299 return objmap 300 } 301 } 302 303 func StringToJson(s string) string { 304 b, _ := json.Marshal(s) 305 return string(b) 306 } 307 308 func StringFromJson(data io.Reader) string { 309 decoder := json.NewDecoder(data) 310 311 var s string 312 if err := decoder.Decode(&s); err != nil { 313 return "" 314 } else { 315 return s 316 } 317 } 318 319 func GetServerIpAddress(iface string) string { 320 var addrs []net.Addr 321 if len(iface) == 0 { 322 var err error 323 addrs, err = net.InterfaceAddrs() 324 if err != nil { 325 return "" 326 } 327 } else { 328 interfaces, err := net.Interfaces() 329 if err != nil { 330 return "" 331 } 332 for _, i := range interfaces { 333 if i.Name == iface { 334 addrs, err = i.Addrs() 335 if err != nil { 336 return "" 337 } 338 break 339 } 340 } 341 } 342 343 for _, addr := range addrs { 344 345 if ip, ok := addr.(*net.IPNet); ok && !ip.IP.IsLoopback() && !ip.IP.IsLinkLocalUnicast() && !ip.IP.IsLinkLocalMulticast() { 346 if ip.IP.To4() != nil { 347 return ip.IP.String() 348 } 349 } 350 } 351 352 return "" 353 } 354 355 func IsLower(s string) bool { 356 return strings.ToLower(s) == s 357 } 358 359 func IsValidEmail(email string) bool { 360 if !IsLower(email) { 361 return false 362 } 363 364 if addr, err := mail.ParseAddress(email); err != nil { 365 return false 366 } else if addr.Name != "" { 367 // mail.ParseAddress accepts input of the form "Billy Bob <billy@example.com>" which we don't allow 368 return false 369 } 370 371 return true 372 } 373 374 var reservedName = []string{ 375 "admin", 376 "api", 377 "channel", 378 "claim", 379 "error", 380 "help", 381 "landing", 382 "login", 383 "mfa", 384 "oauth", 385 "plug", 386 "plugins", 387 "post", 388 "signup", 389 } 390 391 func IsValidChannelIdentifier(s string) bool { 392 393 if !IsValidAlphaNumHyphenUnderscore(s, true) { 394 return false 395 } 396 397 if len(s) < CHANNEL_NAME_MIN_LENGTH { 398 return false 399 } 400 401 return true 402 } 403 404 func IsValidAlphaNum(s string) bool { 405 validAlphaNum := regexp.MustCompile(`^[a-z0-9]+([a-z\-0-9]+|(__)?)[a-z0-9]+$`) 406 407 return validAlphaNum.MatchString(s) 408 } 409 410 func IsValidAlphaNumHyphenUnderscore(s string, withFormat bool) bool { 411 if withFormat { 412 validAlphaNumHyphenUnderscore := regexp.MustCompile(`^[a-z0-9]+([a-z\-\_0-9]+|(__)?)[a-z0-9]+$`) 413 return validAlphaNumHyphenUnderscore.MatchString(s) 414 } 415 416 validSimpleAlphaNumHyphenUnderscore := regexp.MustCompile(`^[a-zA-Z0-9\-_]+$`) 417 return validSimpleAlphaNumHyphenUnderscore.MatchString(s) 418 } 419 420 func Etag(parts ...interface{}) string { 421 422 etag := CurrentVersion 423 424 for _, part := range parts { 425 etag += fmt.Sprintf(".%v", part) 426 } 427 428 return etag 429 } 430 431 var validHashtag = regexp.MustCompile(`^(#\pL[\pL\d\-_.]*[\pL\d])$`) 432 var puncStart = regexp.MustCompile(`^[^\pL\d\s#]+`) 433 var hashtagStart = regexp.MustCompile(`^#{2,}`) 434 var puncEnd = regexp.MustCompile(`[^\pL\d\s]+$`) 435 436 func ParseHashtags(text string) (string, string) { 437 words := strings.Fields(text) 438 439 hashtagString := "" 440 plainString := "" 441 for _, word := range words { 442 // trim off surrounding punctuation 443 word = puncStart.ReplaceAllString(word, "") 444 word = puncEnd.ReplaceAllString(word, "") 445 446 // and remove extra pound #s 447 word = hashtagStart.ReplaceAllString(word, "#") 448 449 if validHashtag.MatchString(word) { 450 hashtagString += " " + word 451 } else { 452 plainString += " " + word 453 } 454 } 455 456 if len(hashtagString) > 1000 { 457 hashtagString = hashtagString[:999] 458 lastSpace := strings.LastIndex(hashtagString, " ") 459 if lastSpace > -1 { 460 hashtagString = hashtagString[:lastSpace] 461 } else { 462 hashtagString = "" 463 } 464 } 465 466 return strings.TrimSpace(hashtagString), strings.TrimSpace(plainString) 467 } 468 469 func IsFileExtImage(ext string) bool { 470 ext = strings.ToLower(ext) 471 for _, imgExt := range IMAGE_EXTENSIONS { 472 if ext == imgExt { 473 return true 474 } 475 } 476 return false 477 } 478 479 func GetImageMimeType(ext string) string { 480 ext = strings.ToLower(ext) 481 if len(IMAGE_MIME_TYPES[ext]) == 0 { 482 return "image" 483 } else { 484 return IMAGE_MIME_TYPES[ext] 485 } 486 } 487 488 func ClearMentionTags(post string) string { 489 post = strings.Replace(post, "<mention>", "", -1) 490 post = strings.Replace(post, "</mention>", "", -1) 491 return post 492 } 493 494 func IsValidHttpUrl(rawUrl string) bool { 495 if strings.Index(rawUrl, "http://") != 0 && strings.Index(rawUrl, "https://") != 0 { 496 return false 497 } 498 499 if _, err := url.ParseRequestURI(rawUrl); err != nil { 500 return false 501 } 502 503 return true 504 } 505 506 func IsValidTurnOrStunServer(rawUri string) bool { 507 if strings.Index(rawUri, "turn:") != 0 && strings.Index(rawUri, "stun:") != 0 { 508 return false 509 } 510 511 if _, err := url.ParseRequestURI(rawUri); err != nil { 512 return false 513 } 514 515 return true 516 } 517 518 func IsSafeLink(link *string) bool { 519 if link != nil { 520 if IsValidHttpUrl(*link) { 521 return true 522 } else if strings.HasPrefix(*link, "/") { 523 return true 524 } else { 525 return false 526 } 527 } 528 529 return true 530 } 531 532 func IsValidWebsocketUrl(rawUrl string) bool { 533 if strings.Index(rawUrl, "ws://") != 0 && strings.Index(rawUrl, "wss://") != 0 { 534 return false 535 } 536 537 if _, err := url.ParseRequestURI(rawUrl); err != nil { 538 return false 539 } 540 541 return true 542 } 543 544 func IsValidTrueOrFalseString(value string) bool { 545 return value == "true" || value == "false" 546 } 547 548 func IsValidNumberString(value string) bool { 549 if _, err := strconv.Atoi(value); err != nil { 550 return false 551 } 552 553 return true 554 } 555 556 func IsValidId(value string) bool { 557 if len(value) != 26 { 558 return false 559 } 560 561 for _, r := range value { 562 if !unicode.IsLetter(r) && !unicode.IsNumber(r) { 563 return false 564 } 565 } 566 567 return true 568 } 569 570 // Copied from https://golang.org/src/net/dnsclient.go#L119 571 func IsDomainName(s string) bool { 572 // See RFC 1035, RFC 3696. 573 // Presentation format has dots before every label except the first, and the 574 // terminal empty label is optional here because we assume fully-qualified 575 // (absolute) input. We must therefore reserve space for the first and last 576 // labels' length octets in wire format, where they are necessary and the 577 // maximum total length is 255. 578 // So our _effective_ maximum is 253, but 254 is not rejected if the last 579 // character is a dot. 580 l := len(s) 581 if l == 0 || l > 254 || l == 254 && s[l-1] != '.' { 582 return false 583 } 584 585 last := byte('.') 586 ok := false // Ok once we've seen a letter. 587 partlen := 0 588 for i := 0; i < len(s); i++ { 589 c := s[i] 590 switch { 591 default: 592 return false 593 case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '_': 594 ok = true 595 partlen++ 596 case '0' <= c && c <= '9': 597 // fine 598 partlen++ 599 case c == '-': 600 // Byte before dash cannot be dot. 601 if last == '.' { 602 return false 603 } 604 partlen++ 605 case c == '.': 606 // Byte before dot cannot be dot, dash. 607 if last == '.' || last == '-' { 608 return false 609 } 610 if partlen > 63 || partlen == 0 { 611 return false 612 } 613 partlen = 0 614 } 615 last = c 616 } 617 if last == '-' || partlen > 63 { 618 return false 619 } 620 621 return ok 622 } 623 624 func RemoveDuplicateStrings(in []string) []string { 625 out := []string{} 626 seen := make(map[string]bool, len(in)) 627 628 for _, item := range in { 629 if !seen[item] { 630 out = append(out, item) 631 632 seen[item] = true 633 } 634 } 635 636 return out 637 } 638 639 func GetPreferredTimezone(timezone StringMap) string { 640 if timezone["useAutomaticTimezone"] == "true" { 641 return timezone["automaticTimezone"] 642 } 643 644 return timezone["manualTimezone"] 645 } 646 647 // IsSamlFile checks if filename is a SAML file. 648 func IsSamlFile(saml *SamlSettings, filename string) bool { 649 return filename == *saml.PublicCertificateFile || filename == *saml.PrivateKeyFile || filename == *saml.IdpCertificateFile 650 } 651 652 func AsStringBoolMap(list []string) map[string]bool { 653 listMap := map[string]bool{} 654 for _, p := range list { 655 listMap[p] = true 656 } 657 return listMap 658 } 659 660 // SanitizeUnicode will remove undesirable Unicode characters from a string. 661 func SanitizeUnicode(s string) string { 662 return strings.Map(filterBlocklist, s) 663 } 664 665 // filterBlocklist returns `r` if it is not in the blocklist, otherwise drop (-1). 666 // Blocklist is taken from https://www.w3.org/TR/unicode-xml/#Charlist 667 func filterBlocklist(r rune) rune { 668 const drop = -1 669 switch r { 670 case '\u0340', '\u0341': // clones of grave and acute; deprecated in Unicode 671 return drop 672 case '\u17A3', '\u17D3': // obsolete characters for Khmer; deprecated in Unicode 673 return drop 674 case '\u2028', '\u2029': // line and paragraph separator 675 return drop 676 case '\u202A', '\u202B', '\u202C', '\u202D', '\u202E': // BIDI embedding controls 677 return drop 678 case '\u206A', '\u206B': // activate/inhibit symmetric swapping; deprecated in Unicode 679 return drop 680 case '\u206C', '\u206D': // activate/inhibit Arabic form shaping; deprecated in Unicode 681 return drop 682 case '\u206E', '\u206F': // activate/inhibit national digit shapes; deprecated in Unicode 683 return drop 684 case '\uFFF9', '\uFFFA', '\uFFFB': // interlinear annotation characters 685 return drop 686 case '\uFEFF': // byte order mark 687 return drop 688 case '\uFFFC': // object replacement character 689 return drop 690 } 691 692 // Scoping for musical notation 693 if r >= 0x0001D173 && r <= 0x0001D17A { 694 return drop 695 } 696 697 // Language tag code points 698 if r >= 0x000E0000 && r <= 0x000E007F { 699 return drop 700 } 701 702 return r 703 }