github.com/jfrerich/mattermost-server@v5.8.0-rc2+incompatible/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/nicksnyder/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 var translateFunc goi18n.TranslateFunc = nil 40 41 func AppErrorInit(t goi18n.TranslateFunc) { 42 translateFunc = t 43 } 44 45 type AppError struct { 46 Id string `json:"id"` 47 Message string `json:"message"` // Message to be display to the end user without debugging information 48 DetailedError string `json:"detailed_error"` // Internal error string to help the developer 49 RequestId string `json:"request_id,omitempty"` // The RequestId that's also set in the header 50 StatusCode int `json:"status_code,omitempty"` // The http status code 51 Where string `json:"-"` // The function where it happened in the form of Struct.Func 52 IsOAuth bool `json:"is_oauth,omitempty"` // Whether the error is OAuth specific 53 params map[string]interface{} 54 } 55 56 func (er *AppError) Error() string { 57 return er.Where + ": " + er.Message + ", " + er.DetailedError 58 } 59 60 func (er *AppError) Translate(T goi18n.TranslateFunc) { 61 if T == nil { 62 er.Message = er.Id 63 return 64 } 65 66 if er.params == nil { 67 er.Message = T(er.Id) 68 } else { 69 er.Message = T(er.Id, er.params) 70 } 71 } 72 73 func (er *AppError) SystemMessage(T goi18n.TranslateFunc) string { 74 if er.params == nil { 75 return T(er.Id) 76 } else { 77 return T(er.Id, er.params) 78 } 79 } 80 81 func (er *AppError) ToJson() string { 82 b, _ := json.Marshal(er) 83 return string(b) 84 } 85 86 // AppErrorFromJson will decode the input and return an AppError 87 func AppErrorFromJson(data io.Reader) *AppError { 88 str := "" 89 bytes, rerr := ioutil.ReadAll(data) 90 if rerr != nil { 91 str = rerr.Error() 92 } else { 93 str = string(bytes) 94 } 95 96 decoder := json.NewDecoder(strings.NewReader(str)) 97 var er AppError 98 err := decoder.Decode(&er) 99 if err == nil { 100 return &er 101 } else { 102 return NewAppError("AppErrorFromJson", "model.utils.decode_json.app_error", nil, "body: "+str, http.StatusInternalServerError) 103 } 104 } 105 106 func NewAppError(where string, id string, params map[string]interface{}, details string, status int) *AppError { 107 ap := &AppError{} 108 ap.Id = id 109 ap.params = params 110 ap.Message = id 111 ap.Where = where 112 ap.DetailedError = details 113 ap.StatusCode = status 114 ap.IsOAuth = false 115 ap.Translate(translateFunc) 116 return ap 117 } 118 119 var encoding = base32.NewEncoding("ybndrfg8ejkmcpqxot1uwisza345h769") 120 121 // NewId is a globally unique identifier. It is a [A-Z0-9] string 26 122 // characters long. It is a UUID version 4 Guid that is zbased32 encoded 123 // with the padding stripped off. 124 func NewId() string { 125 var b bytes.Buffer 126 encoder := base32.NewEncoder(encoding, &b) 127 encoder.Write(uuid.NewRandom()) 128 encoder.Close() 129 b.Truncate(26) // removes the '==' padding 130 return b.String() 131 } 132 133 func NewRandomString(length int) string { 134 var b bytes.Buffer 135 str := make([]byte, length+8) 136 rand.Read(str) 137 encoder := base32.NewEncoder(encoding, &b) 138 encoder.Write(str) 139 encoder.Close() 140 b.Truncate(length) // removes the '==' padding 141 return b.String() 142 } 143 144 // GetMillis is a convenience method to get milliseconds since epoch. 145 func GetMillis() int64 { 146 return time.Now().UnixNano() / int64(time.Millisecond) 147 } 148 149 // GetMillisForTime is a convenience method to get milliseconds since epoch for provided Time. 150 func GetMillisForTime(thisTime time.Time) int64 { 151 return thisTime.UnixNano() / int64(time.Millisecond) 152 } 153 154 // PadDateStringZeros is a convenience method to pad 2 digit date parts with zeros to meet ISO 8601 format 155 func PadDateStringZeros(dateString string) string { 156 parts := strings.Split(dateString, "-") 157 for index, part := range parts { 158 if len(part) == 1 { 159 parts[index] = "0" + part 160 } 161 } 162 dateString = strings.Join(parts[:], "-") 163 return dateString 164 } 165 166 // GetStartOfDayMillis is a convenience method to get milliseconds since epoch for provided date's start of day 167 func GetStartOfDayMillis(thisTime time.Time, timeZoneOffset int) int64 { 168 localSearchTimeZone := time.FixedZone("Local Search Time Zone", timeZoneOffset) 169 resultTime := time.Date(thisTime.Year(), thisTime.Month(), thisTime.Day(), 0, 0, 0, 0, localSearchTimeZone) 170 return GetMillisForTime(resultTime) 171 } 172 173 // GetEndOfDayMillis is a convenience method to get milliseconds since epoch for provided date's end of day 174 func GetEndOfDayMillis(thisTime time.Time, timeZoneOffset int) int64 { 175 localSearchTimeZone := time.FixedZone("Local Search Time Zone", timeZoneOffset) 176 resultTime := time.Date(thisTime.Year(), thisTime.Month(), thisTime.Day(), 23, 59, 59, 999999999, localSearchTimeZone) 177 return GetMillisForTime(resultTime) 178 } 179 180 func CopyStringMap(originalMap map[string]string) map[string]string { 181 copyMap := make(map[string]string) 182 for k, v := range originalMap { 183 copyMap[k] = v 184 } 185 return copyMap 186 } 187 188 // MapToJson converts a map to a json string 189 func MapToJson(objmap map[string]string) string { 190 b, _ := json.Marshal(objmap) 191 return string(b) 192 } 193 194 // MapToJson converts a map to a json string 195 func MapBoolToJson(objmap map[string]bool) string { 196 b, _ := json.Marshal(objmap) 197 return string(b) 198 } 199 200 // MapFromJson will decode the key/value pair map 201 func MapFromJson(data io.Reader) map[string]string { 202 decoder := json.NewDecoder(data) 203 204 var objmap map[string]string 205 if err := decoder.Decode(&objmap); err != nil { 206 return make(map[string]string) 207 } else { 208 return objmap 209 } 210 } 211 212 // MapFromJson will decode the key/value pair map 213 func MapBoolFromJson(data io.Reader) map[string]bool { 214 decoder := json.NewDecoder(data) 215 216 var objmap map[string]bool 217 if err := decoder.Decode(&objmap); err != nil { 218 return make(map[string]bool) 219 } else { 220 return objmap 221 } 222 } 223 224 func ArrayToJson(objmap []string) string { 225 b, _ := json.Marshal(objmap) 226 return string(b) 227 } 228 229 func ArrayFromJson(data io.Reader) []string { 230 decoder := json.NewDecoder(data) 231 232 var objmap []string 233 if err := decoder.Decode(&objmap); err != nil { 234 return make([]string, 0) 235 } else { 236 return objmap 237 } 238 } 239 240 func ArrayFromInterface(data interface{}) []string { 241 stringArray := []string{} 242 243 dataArray, ok := data.([]interface{}) 244 if !ok { 245 return stringArray 246 } 247 248 for _, v := range dataArray { 249 if str, ok := v.(string); ok { 250 stringArray = append(stringArray, str) 251 } 252 } 253 254 return stringArray 255 } 256 257 func StringInterfaceToJson(objmap map[string]interface{}) string { 258 b, _ := json.Marshal(objmap) 259 return string(b) 260 } 261 262 func StringInterfaceFromJson(data io.Reader) map[string]interface{} { 263 decoder := json.NewDecoder(data) 264 265 var objmap map[string]interface{} 266 if err := decoder.Decode(&objmap); err != nil { 267 return make(map[string]interface{}) 268 } else { 269 return objmap 270 } 271 } 272 273 func StringToJson(s string) string { 274 b, _ := json.Marshal(s) 275 return string(b) 276 } 277 278 func StringFromJson(data io.Reader) string { 279 decoder := json.NewDecoder(data) 280 281 var s string 282 if err := decoder.Decode(&s); err != nil { 283 return "" 284 } else { 285 return s 286 } 287 } 288 289 func GetServerIpAddress() string { 290 if addrs, err := net.InterfaceAddrs(); err != nil { 291 return "" 292 } else { 293 for _, addr := range addrs { 294 295 if ip, ok := addr.(*net.IPNet); ok && !ip.IP.IsLoopback() && !ip.IP.IsLinkLocalUnicast() && !ip.IP.IsLinkLocalMulticast() { 296 if ip.IP.To4() != nil { 297 return ip.IP.String() 298 } 299 } 300 } 301 } 302 303 return "" 304 } 305 306 func IsLower(s string) bool { 307 return strings.ToLower(s) == s 308 } 309 310 func IsValidEmail(email string) bool { 311 if !IsLower(email) { 312 return false 313 } 314 315 if addr, err := mail.ParseAddress(email); err != nil { 316 return false 317 } else if addr.Name != "" { 318 // mail.ParseAddress accepts input of the form "Billy Bob <billy@example.com>" which we don't allow 319 return false 320 } 321 322 return true 323 } 324 325 var reservedName = []string{ 326 "signup", 327 "login", 328 "admin", 329 "channel", 330 "post", 331 "api", 332 "oauth", 333 } 334 335 func IsValidChannelIdentifier(s string) bool { 336 337 if !IsValidAlphaNumHyphenUnderscore(s, true) { 338 return false 339 } 340 341 if len(s) < CHANNEL_NAME_MIN_LENGTH { 342 return false 343 } 344 345 return true 346 } 347 348 func IsValidAlphaNum(s string) bool { 349 validAlphaNum := regexp.MustCompile(`^[a-z0-9]+([a-z\-0-9]+|(__)?)[a-z0-9]+$`) 350 351 return validAlphaNum.MatchString(s) 352 } 353 354 func IsValidAlphaNumHyphenUnderscore(s string, withFormat bool) bool { 355 if withFormat { 356 validAlphaNumHyphenUnderscore := regexp.MustCompile(`^[a-z0-9]+([a-z\-\_0-9]+|(__)?)[a-z0-9]+$`) 357 return validAlphaNumHyphenUnderscore.MatchString(s) 358 } 359 360 validSimpleAlphaNumHyphenUnderscore := regexp.MustCompile(`^[a-zA-Z0-9\-_]+$`) 361 return validSimpleAlphaNumHyphenUnderscore.MatchString(s) 362 } 363 364 func Etag(parts ...interface{}) string { 365 366 etag := CurrentVersion 367 368 for _, part := range parts { 369 etag += fmt.Sprintf(".%v", part) 370 } 371 372 return etag 373 } 374 375 var validHashtag = regexp.MustCompile(`^(#\pL[\pL\d\-_.]*[\pL\d])$`) 376 var puncStart = regexp.MustCompile(`^[^\pL\d\s#]+`) 377 var hashtagStart = regexp.MustCompile(`^#{2,}`) 378 var puncEnd = regexp.MustCompile(`[^\pL\d\s]+$`) 379 380 func ParseHashtags(text string) (string, string) { 381 words := strings.Fields(text) 382 383 hashtagString := "" 384 plainString := "" 385 for _, word := range words { 386 // trim off surrounding punctuation 387 word = puncStart.ReplaceAllString(word, "") 388 word = puncEnd.ReplaceAllString(word, "") 389 390 // and remove extra pound #s 391 word = hashtagStart.ReplaceAllString(word, "#") 392 393 if validHashtag.MatchString(word) { 394 hashtagString += " " + word 395 } else { 396 plainString += " " + word 397 } 398 } 399 400 if len(hashtagString) > 1000 { 401 hashtagString = hashtagString[:999] 402 lastSpace := strings.LastIndex(hashtagString, " ") 403 if lastSpace > -1 { 404 hashtagString = hashtagString[:lastSpace] 405 } else { 406 hashtagString = "" 407 } 408 } 409 410 return strings.TrimSpace(hashtagString), strings.TrimSpace(plainString) 411 } 412 413 func IsFileExtImage(ext string) bool { 414 ext = strings.ToLower(ext) 415 for _, imgExt := range IMAGE_EXTENSIONS { 416 if ext == imgExt { 417 return true 418 } 419 } 420 return false 421 } 422 423 func GetImageMimeType(ext string) string { 424 ext = strings.ToLower(ext) 425 if len(IMAGE_MIME_TYPES[ext]) == 0 { 426 return "image" 427 } else { 428 return IMAGE_MIME_TYPES[ext] 429 } 430 } 431 432 func ClearMentionTags(post string) string { 433 post = strings.Replace(post, "<mention>", "", -1) 434 post = strings.Replace(post, "</mention>", "", -1) 435 return post 436 } 437 438 func IsValidHttpUrl(rawUrl string) bool { 439 if strings.Index(rawUrl, "http://") != 0 && strings.Index(rawUrl, "https://") != 0 { 440 return false 441 } 442 443 if _, err := url.ParseRequestURI(rawUrl); err != nil { 444 return false 445 } 446 447 return true 448 } 449 450 func IsValidTurnOrStunServer(rawUri string) bool { 451 if strings.Index(rawUri, "turn:") != 0 && strings.Index(rawUri, "stun:") != 0 { 452 return false 453 } 454 455 if _, err := url.ParseRequestURI(rawUri); err != nil { 456 return false 457 } 458 459 return true 460 } 461 462 func IsSafeLink(link *string) bool { 463 if link != nil { 464 if IsValidHttpUrl(*link) { 465 return true 466 } else if strings.HasPrefix(*link, "/") { 467 return true 468 } else { 469 return false 470 } 471 } 472 473 return true 474 } 475 476 func IsValidWebsocketUrl(rawUrl string) bool { 477 if strings.Index(rawUrl, "ws://") != 0 && strings.Index(rawUrl, "wss://") != 0 { 478 return false 479 } 480 481 if _, err := url.ParseRequestURI(rawUrl); err != nil { 482 return false 483 } 484 485 return true 486 } 487 488 func IsValidTrueOrFalseString(value string) bool { 489 return value == "true" || value == "false" 490 } 491 492 func IsValidNumberString(value string) bool { 493 if _, err := strconv.Atoi(value); err != nil { 494 return false 495 } 496 497 return true 498 } 499 500 func IsValidId(value string) bool { 501 if len(value) != 26 { 502 return false 503 } 504 505 for _, r := range value { 506 if !unicode.IsLetter(r) && !unicode.IsNumber(r) { 507 return false 508 } 509 } 510 511 return true 512 } 513 514 // Copied from https://golang.org/src/net/dnsclient.go#L119 515 func IsDomainName(s string) bool { 516 // See RFC 1035, RFC 3696. 517 // Presentation format has dots before every label except the first, and the 518 // terminal empty label is optional here because we assume fully-qualified 519 // (absolute) input. We must therefore reserve space for the first and last 520 // labels' length octets in wire format, where they are necessary and the 521 // maximum total length is 255. 522 // So our _effective_ maximum is 253, but 254 is not rejected if the last 523 // character is a dot. 524 l := len(s) 525 if l == 0 || l > 254 || l == 254 && s[l-1] != '.' { 526 return false 527 } 528 529 last := byte('.') 530 ok := false // Ok once we've seen a letter. 531 partlen := 0 532 for i := 0; i < len(s); i++ { 533 c := s[i] 534 switch { 535 default: 536 return false 537 case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '_': 538 ok = true 539 partlen++ 540 case '0' <= c && c <= '9': 541 // fine 542 partlen++ 543 case c == '-': 544 // Byte before dash cannot be dot. 545 if last == '.' { 546 return false 547 } 548 partlen++ 549 case c == '.': 550 // Byte before dot cannot be dot, dash. 551 if last == '.' || last == '-' { 552 return false 553 } 554 if partlen > 63 || partlen == 0 { 555 return false 556 } 557 partlen = 0 558 } 559 last = c 560 } 561 if last == '-' || partlen > 63 { 562 return false 563 } 564 565 return ok 566 } 567 568 func RemoveDuplicateStrings(in []string) []string { 569 out := []string{} 570 seen := make(map[string]bool, len(in)) 571 572 for _, item := range in { 573 if !seen[item] { 574 out = append(out, item) 575 576 seen[item] = true 577 } 578 } 579 580 return out 581 } 582 583 func GetPreferredTimezone(timezone StringMap) string { 584 if timezone["useAutomaticTimezone"] == "true" { 585 return timezone["automaticTimezone"] 586 } 587 588 return timezone["manualTimezone"] 589 }