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