Doug Lauder
2023-03-22 17:22:27 -04:00
коммит произвёл GitHub
родитель b61c096497
Коммит c943ed6859
13276 изменённых файлов: 1695615 добавлений и 223189 удалений

177
server/channels/utils/api.go Обычный файл
Просмотреть файл

@@ -0,0 +1,177 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"crypto"
"crypto/rand"
"encoding/base64"
"fmt"
"html/template"
"net/http"
"net/url"
"path"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
func CheckOrigin(r *http.Request, allowedOrigins string) bool {
origin := r.Header.Get("Origin")
if origin == "" {
return true
}
if allowedOrigins == "*" {
return true
}
for _, allowed := range strings.Split(allowedOrigins, " ") {
if allowed == origin {
return true
}
}
return false
}
func OriginChecker(allowedOrigins string) func(*http.Request) bool {
return func(r *http.Request) bool {
return CheckOrigin(r, allowedOrigins)
}
}
func RenderWebAppError(config *model.Config, w http.ResponseWriter, r *http.Request, err *model.AppError, s crypto.Signer) {
RenderWebError(config, w, r, err.StatusCode, url.Values{
"message": []string{err.Message},
}, s)
}
func RenderWebError(config *model.Config, w http.ResponseWriter, r *http.Request, status int, params url.Values, s crypto.Signer) {
queryString := params.Encode()
subpath, _ := GetSubpathFromConfig(config)
h := crypto.SHA256
sum := h.New()
sum.Write([]byte(path.Join(subpath, "error") + "?" + queryString))
signature, err := s.Sign(rand.Reader, sum.Sum(nil), h)
if err != nil {
http.Error(w, "", http.StatusInternalServerError)
return
}
destination := path.Join(subpath, "error") + "?" + queryString + "&s=" + base64.URLEncoding.EncodeToString(signature)
if status >= 300 && status < 400 {
http.Redirect(w, r, destination, status)
return
}
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(status)
fmt.Fprintln(w, `<!DOCTYPE html><html><head></head>`)
fmt.Fprintln(w, `<body onload="window.location = '`+template.HTMLEscapeString(template.JSEscapeString(destination))+`'">`)
fmt.Fprintln(w, `<noscript><meta http-equiv="refresh" content="0; url=`+template.HTMLEscapeString(destination)+`"></noscript>`)
fmt.Fprintln(w, `<!-- web error message -->`)
fmt.Fprintln(w, `<a href="`+template.HTMLEscapeString(destination)+`" style="color: #c0c0c0;">...</a>`)
fmt.Fprintln(w, `</body></html>`)
}
func RenderMobileAuthComplete(w http.ResponseWriter, redirectURL string) {
var link = template.HTMLEscapeString(redirectURL)
RenderMobileMessage(w, `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" style="width: 64px; height: 64px; fill: #3c763d">
<!-- Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) -->
<path stroke="green" d="M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z"/>
</svg>
<h2> `+i18n.T("api.oauth.auth_complete")+` </h2>
<p id="redirecting-message"> `+i18n.T("api.oauth.redirecting_back")+` </p>
<p id="close-tab-message" style="display: none"> `+i18n.T("api.oauth.close_browser")+` </p>
<p> `+i18n.T("api.oauth.click_redirect", model.StringInterface{"Link": link})+` </p>
<meta http-equiv="refresh" content="2; url=`+link+`">
<script>
window.onload = function() {
setTimeout(function() {
document.getElementById('redirecting-message').style.display = 'none';
document.getElementById('close-tab-message').style.display = 'block';
}, 2000);
}
</script>
`)
}
func RenderMobileError(config *model.Config, w http.ResponseWriter, err *model.AppError, redirectURL string) {
var link = template.HTMLEscapeString(redirectURL)
var invalidSchemes = map[string]bool{
"data": true,
"javascript": true,
"vbscript": true,
}
u, redirectErr := url.Parse(redirectURL)
if redirectErr != nil || invalidSchemes[u.Scheme] {
link = *config.ServiceSettings.SiteURL
}
RenderMobileMessage(w, `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" style="width: 64px; height: 64px; fill: #ccc">
<!-- Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) -->
<path d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"/>
</svg>
<h2> `+i18n.T("error")+` </h2>
<p> `+err.Message+` </p>
<a href="`+link+`">
`+i18n.T("api.back_to_app", map[string]any{"SiteName": config.TeamSettings.SiteName})+`
</a>
`)
}
func RenderMobileMessage(w http.ResponseWriter, message string) {
w.Header().Set("Content-Type", "text/html")
fmt.Fprintln(w, `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, user-scalable=yes, viewport-fit=cover">
<style>
body {
color: #333;
background-color: #fff;
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
font-size: 14px;
line-height: 1.42857143;
}
a {
color: #337ab7;
text-decoration: none;
}
a:focus, a:hover {
color: #23527c;
text-decoration: underline;
}
h2 {
font-size: 30px;
margin: 20px 0 10px 0;
font-weight: 500;
line-height: 1.1
}
p {
margin: 0 0 10px;
}
.message-container {
color: #555;
display: table-cell;
padding: 5em 0;
text-align: left;
vertical-align: top;
}
</style>
</head>
<body>
<!-- mobile app message -->
<div class="message-container">
`+message+`
</div>
</body>
</html>
`)
}

51
server/channels/utils/api_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,51 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"encoding/asn1"
"encoding/base64"
"math/big"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
)
func TestRenderWebError(t *testing.T) {
r := httptest.NewRequest("GET", "http://foo", nil)
w := httptest.NewRecorder()
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
RenderWebError(&model.Config{}, w, r, http.StatusTemporaryRedirect, url.Values{
"foo": []string{"bar"},
}, key)
resp := w.Result()
location, err := url.Parse(resp.Header.Get("Location"))
require.NoError(t, err)
require.NotEmpty(t, location.Query().Get("s"))
type ecdsaSignature struct {
R, S *big.Int
}
var rs ecdsaSignature
s, err := base64.URLEncoding.DecodeString(location.Query().Get("s"))
require.NoError(t, err)
_, err = asn1.Unmarshal(s, &rs)
require.NoError(t, err)
assert.Equal(t, "bar", location.Query().Get("foo"))
h := sha256.Sum256([]byte("/error?foo=bar"))
assert.True(t, ecdsa.Verify(&key.PublicKey, h[:], rs.R, rs.S))
}

69
server/channels/utils/archive.go Обычный файл
Просмотреть файл

@@ -0,0 +1,69 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"archive/zip"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
func sanitizePath(p string) string {
dir := strings.ReplaceAll(filepath.Dir(filepath.Clean(p)), "..", "")
base := filepath.Base(p)
if strings.Count(base, ".") == len(base) {
return ""
}
return filepath.Join(dir, base)
}
// UnzipToPath extracts a given zip archive into a given path.
// It returns a list of extracted paths.
func UnzipToPath(zipFile io.ReaderAt, size int64, outPath string) ([]string, error) {
rd, err := zip.NewReader(zipFile, size)
if err != nil {
return nil, fmt.Errorf("failed to create reader: %w", err)
}
paths := make([]string, len(rd.File))
for i, f := range rd.File {
filePath := sanitizePath(f.Name)
if filePath == "" {
return nil, fmt.Errorf("invalid filepath `%s`", f.Name)
}
path := filepath.Join(outPath, filePath)
paths[i] = path
if f.FileInfo().IsDir() {
if err := os.Mkdir(path, 0700); err != nil {
return nil, fmt.Errorf("failed to create directory: %w", err)
}
continue
}
if _, err := os.Stat(filepath.Dir(path)); os.IsNotExist(err) {
if err = os.MkdirAll(filepath.Dir(path), 0700); err != nil {
return nil, fmt.Errorf("failed to create directory: %w", err)
}
}
outFile, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0600)
if err != nil {
return nil, fmt.Errorf("failed to create file: %w", err)
}
defer outFile.Close()
file, err := f.Open()
if err != nil {
return nil, fmt.Errorf("failed to open file: %w", err)
}
defer file.Close()
if _, err := io.Copy(outFile, file); err != nil {
return nil, fmt.Errorf("failed to write to file: %w", err)
}
}
return paths, nil
}

148
server/channels/utils/archive_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,148 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"archive/zip"
"errors"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/fileutils"
)
func TestSanitizePath(t *testing.T) {
cases := []struct {
input string
expected string
}{
{
".",
"",
},
{
"../",
"",
},
{
"...",
"",
},
{
"..//.",
"",
},
{
"/../",
"",
},
{
"/path/...../to/file",
"/path/to/file",
},
{
"/path/to/file...",
"/path/to/file...",
},
{
"/path/to/../../../file",
"/file",
},
{
"../../../../file",
"/file",
},
{
"/path/to/file..ext",
"/path/to/file..ext",
},
{
"/path/to/...file..ext",
"/path/to/...file..ext",
},
{
"./path/to/...file..ext",
"path/to/...file..ext",
},
{
"./...file",
"...file",
},
{
"path/",
"path",
},
}
for _, c := range cases {
t.Run(c.input, func(t *testing.T) {
require.Equal(t, c.expected, sanitizePath(c.input))
})
}
}
func TestUnzipToPath(t *testing.T) {
testDir, _ := fileutils.FindDir("tests")
require.NotEmpty(t, testDir)
dir, err := os.MkdirTemp("", "unzip")
require.NoError(t, err)
defer os.RemoveAll(dir)
t.Run("invalid archive", func(t *testing.T) {
file, err := os.Open(testDir + "/testplugin.tar.gz")
require.NoError(t, err)
defer file.Close()
info, err := file.Stat()
require.NoError(t, err)
paths, err := UnzipToPath(file, info.Size(), dir)
require.Error(t, err)
require.True(t, errors.Is(err, zip.ErrFormat))
require.Nil(t, paths)
})
t.Run("valid archive", func(t *testing.T) {
file, err := os.Open(testDir + "/testarchive.zip")
require.NoError(t, err)
defer file.Close()
info, err := file.Stat()
require.NoError(t, err)
paths, err := UnzipToPath(file, info.Size(), dir)
require.NoError(t, err)
require.NotEmpty(t, paths)
expectedFiles := map[string]int64{
dir + "/testfile.txt": 446,
dir + "/testdir/testfile2.txt": 866,
dir + "/testdir2/testfile3.txt": 845,
}
expectedDirs := []string{
dir + "/testdir",
dir + "/testdir2",
}
err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
require.NoError(t, err)
if path == dir {
return nil
}
require.Contains(t, paths, path)
if info.IsDir() {
require.Contains(t, expectedDirs, path)
} else {
require.Equal(t, expectedFiles[path], info.Size())
}
return nil
})
require.NoError(t, err)
})
}

26
server/channels/utils/backoff.go Обычный файл
Просмотреть файл

@@ -0,0 +1,26 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"time"
)
var backoffTimeouts = []time.Duration{50 * time.Millisecond, 100 * time.Millisecond, 200 * time.Millisecond, 200 * time.Millisecond, 400 * time.Millisecond, 400 * time.Millisecond}
// ProgressiveRetry executes a BackoffOperation and waits an increasing time before retrying the operation.
func ProgressiveRetry(operation func() error) error {
var err error
for attempts := 0; attempts < len(backoffTimeouts); attempts++ {
err = operation()
if err == nil {
return nil
}
time.Sleep(backoffTimeouts[attempts])
}
return err
}

65
server/channels/utils/backoff_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,65 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"testing"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestProgressiveRetry(t *testing.T) {
var retries int
type args struct {
operation func() error
}
tests := []struct {
name string
args args
wantErr bool
expectedRetries int
}{
{
name: "Should fail and return error",
args: args{
operation: func() error {
retries++
return errors.New("Operation Failed")
},
},
wantErr: true,
expectedRetries: 6,
},
{
name: "Should succeed after two retries",
args: args{
operation: func() error {
retries++
if retries == 2 {
return nil
}
return errors.New("Operation Failed")
},
},
wantErr: false,
expectedRetries: 2,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
retries = 0
err := ProgressiveRetry(tt.args.operation)
if !tt.wantErr {
require.NoError(t, err)
}
assert.Equal(t, tt.expectedRetries, retries)
})
}
}

60
server/channels/utils/emoji.go Обычный файл
Просмотреть файл

@@ -0,0 +1,60 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"bytes"
"image"
"image/color"
"image/gif"
"image/jpeg"
"image/png"
"testing"
"github.com/stretchr/testify/require"
)
func CreateTestGif(t *testing.T, width int, height int) []byte {
var buffer bytes.Buffer
err := gif.Encode(&buffer, image.NewRGBA(image.Rect(0, 0, width, height)), nil)
require.NoErrorf(t, err, "failed to create gif: %v", err)
return buffer.Bytes()
}
func CreateTestAnimatedGif(t *testing.T, width int, height int, frames int) []byte {
var buffer bytes.Buffer
img := gif.GIF{
Image: make([]*image.Paletted, frames),
Delay: make([]int, frames),
}
for i := 0; i < frames; i++ {
img.Image[i] = image.NewPaletted(image.Rect(0, 0, width, height), color.Palette{color.Black})
img.Delay[i] = 0
}
err := gif.EncodeAll(&buffer, &img)
require.NoErrorf(t, err, "failed to create animated gif: %v", err)
return buffer.Bytes()
}
func CreateTestJpeg(t *testing.T, width int, height int) []byte {
var buffer bytes.Buffer
err := jpeg.Encode(&buffer, image.NewRGBA(image.Rect(0, 0, width, height)), nil)
require.NoErrorf(t, err, "failed to create jpeg: %v", err)
return buffer.Bytes()
}
func CreateTestPng(t *testing.T, width int, height int) []byte {
var buffer bytes.Buffer
err := png.Encode(&buffer, image.NewRGBA(image.Rect(0, 0, width, height)))
require.NoErrorf(t, err, "failed to create png: %v", err)
return buffer.Bytes()
}

139
server/channels/utils/file.go Обычный файл
Просмотреть файл

@@ -0,0 +1,139 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
)
// CopyFile will copy a file from src path to dst path.
// Overwrites any existing files at dst.
// Permissions are copied from file at src to the new file at dst.
func CopyFile(src, dst string) (err error) {
in, err := os.Open(src)
if err != nil {
return
}
defer in.Close()
if err = os.MkdirAll(filepath.Dir(dst), os.ModePerm); err != nil {
return
}
out, err := os.Create(dst)
if err != nil {
return
}
defer func() {
if e := out.Close(); e != nil {
err = e
}
}()
_, err = io.Copy(out, in)
if err != nil {
return
}
err = out.Sync()
if err != nil {
return
}
stat, err := os.Stat(src)
if err != nil {
return
}
err = os.Chmod(dst, stat.Mode())
if err != nil {
return
}
return
}
// CopyDir will copy a directory and all contained files and directories.
// src must exist and dst must not exist.
// Permissions are preserved when possible. Symlinks are skipped.
func CopyDir(src string, dst string) (err error) {
src = filepath.Clean(src)
dst = filepath.Clean(dst)
stat, err := os.Stat(src)
if err != nil {
return
}
if !stat.IsDir() {
return fmt.Errorf("source must be a directory")
}
_, err = os.Stat(dst)
if err != nil && !os.IsNotExist(err) {
return
}
if err == nil {
return fmt.Errorf("destination already exists")
}
err = os.MkdirAll(dst, stat.Mode())
if err != nil {
return
}
items, err := os.ReadDir(src)
if err != nil {
return
}
for _, item := range items {
srcPath := filepath.Join(src, item.Name())
dstPath := filepath.Join(dst, item.Name())
if item.IsDir() {
err = CopyDir(srcPath, dstPath)
if err != nil {
return
}
} else {
info, ierr := item.Info()
if ierr != nil {
continue
}
if info.Mode()&os.ModeSymlink != 0 {
continue
}
err = CopyFile(srcPath, dstPath)
if err != nil {
return
}
}
}
return
}
var SizeLimitExceeded = errors.New("Size limit exceeded")
type LimitedReaderWithError struct {
limitedReader *io.LimitedReader
}
func NewLimitedReaderWithError(reader io.Reader, maxBytes int64) *LimitedReaderWithError {
return &LimitedReaderWithError{
limitedReader: &io.LimitedReader{R: reader, N: maxBytes + 1},
}
}
func (l *LimitedReaderWithError) Read(p []byte) (int, error) {
n, err := l.limitedReader.Read(p)
if l.limitedReader.N <= 0 && err == io.EOF {
return n, SizeLimitExceeded
}
return n, err
}

128
server/channels/utils/file_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,128 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"bytes"
"crypto/rand"
"io"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCopyDir(t *testing.T) {
srcDir, err := os.MkdirTemp("", "src")
require.NoError(t, err)
defer os.RemoveAll(srcDir)
dstParentDir, err := os.MkdirTemp("", "dstparent")
require.NoError(t, err)
defer os.RemoveAll(dstParentDir)
dstDir := filepath.Join(dstParentDir, "dst")
tempFile := "temp.txt"
err = os.WriteFile(filepath.Join(srcDir, tempFile), []byte("test file"), 0655)
require.NoError(t, err)
childDir := "child"
err = os.Mkdir(filepath.Join(srcDir, childDir), 0777)
require.NoError(t, err)
childTempFile := "childtemp.txt"
err = os.WriteFile(filepath.Join(srcDir, childDir, childTempFile), []byte("test file"), 0755)
require.NoError(t, err)
err = CopyDir(srcDir, dstDir)
assert.NoError(t, err)
stat, err := os.Stat(filepath.Join(dstDir, tempFile))
assert.NoError(t, err)
assert.Equal(t, uint32(0655), uint32(stat.Mode()))
assert.False(t, stat.IsDir())
data, err := os.ReadFile(filepath.Join(dstDir, tempFile))
assert.NoError(t, err)
assert.Equal(t, "test file", string(data))
stat, err = os.Stat(filepath.Join(dstDir, childDir))
assert.NoError(t, err)
assert.True(t, stat.IsDir())
stat, err = os.Stat(filepath.Join(dstDir, childDir, childTempFile))
assert.NoError(t, err)
assert.Equal(t, uint32(0755), uint32(stat.Mode()))
assert.False(t, stat.IsDir())
data, err = os.ReadFile(filepath.Join(dstDir, childDir, childTempFile))
assert.NoError(t, err)
assert.Equal(t, "test file", string(data))
err = CopyDir(srcDir, dstDir)
assert.Error(t, err)
}
func TestLimitedReaderWithError(t *testing.T) {
t.Run("read less than max size", func(t *testing.T) {
maxBytes := 10
randomBytes := make([]byte, maxBytes)
n, err := rand.Read(randomBytes)
require.NoError(t, err)
require.Equal(t, n, maxBytes)
lr := NewLimitedReaderWithError(bytes.NewReader(randomBytes), int64(maxBytes))
smallerBuf := make([]byte, maxBytes-3)
_, err = io.ReadFull(lr, smallerBuf)
require.NoError(t, err)
})
t.Run("read equal to max size", func(t *testing.T) {
maxBytes := 10
randomBytes := make([]byte, maxBytes)
n, err := rand.Read(randomBytes)
require.NoError(t, err)
require.Equal(t, n, maxBytes)
lr := NewLimitedReaderWithError(bytes.NewReader(randomBytes), int64(maxBytes))
buf := make([]byte, maxBytes)
_, err = io.ReadFull(lr, buf)
require.Truef(t, err == nil || err == io.EOF, "err must be nil or %v, got %v", io.EOF, err)
})
t.Run("single read, larger than max size", func(t *testing.T) {
maxBytes := 5
moreThanMaxBytes := maxBytes + 10
randomBytes := make([]byte, moreThanMaxBytes)
n, err := rand.Read(randomBytes)
require.NoError(t, err)
require.Equal(t, moreThanMaxBytes, n)
lr := NewLimitedReaderWithError(bytes.NewReader(randomBytes), int64(maxBytes))
buf := make([]byte, moreThanMaxBytes)
_, err = io.ReadFull(lr, buf)
require.Error(t, err)
require.Equal(t, SizeLimitExceeded, err)
})
t.Run("multiple small reads, total larger than max size", func(t *testing.T) {
maxBytes := 10
lessThanMaxBytes := maxBytes - 4
randomBytesLen := maxBytes * 2
randomBytes := make([]byte, randomBytesLen)
n, err := rand.Read(randomBytes)
require.NoError(t, err)
require.Equal(t, randomBytesLen, n)
lr := NewLimitedReaderWithError(bytes.NewReader(randomBytes), int64(maxBytes))
buf := make([]byte, lessThanMaxBytes)
_, err = io.ReadFull(lr, buf)
require.NoError(t, err)
// lets do it again
_, err = io.ReadFull(lr, buf)
require.Error(t, err)
require.Equal(t, SizeLimitExceeded, err)
})
}

118
server/channels/utils/fileutils/fileutils.go Обычный файл
Просмотреть файл

@@ -0,0 +1,118 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package fileutils
import (
"os"
"path/filepath"
)
func CommonBaseSearchPaths() []string {
paths := []string{
".",
"..",
"../..",
"../../..",
"../../../..",
}
// this enables the server to be used in tests from a different repository
if mmPath := os.Getenv("MM_SERVER_PATH"); mmPath != "" {
paths = append(paths, mmPath)
}
return paths
}
func findPath(path string, baseSearchPaths []string, workingDirFirst bool, filter func(os.FileInfo) bool) string {
if filepath.IsAbs(path) {
if _, err := os.Stat(path); err == nil {
return path
}
return ""
}
searchPaths := []string{}
if workingDirFirst {
searchPaths = append(searchPaths, baseSearchPaths...)
}
// Attempt to search relative to the location of the running binary either before
// or after searching relative to the working directory, depending on `workingDirFirst`.
var binaryDir string
if exe, err := os.Executable(); err == nil {
if exe, err = filepath.EvalSymlinks(exe); err == nil {
if exe, err = filepath.Abs(exe); err == nil {
binaryDir = filepath.Dir(exe)
}
}
}
if binaryDir != "" {
for _, baseSearchPath := range baseSearchPaths {
searchPaths = append(
searchPaths,
filepath.Join(binaryDir, baseSearchPath),
)
}
}
if !workingDirFirst {
searchPaths = append(searchPaths, baseSearchPaths...)
}
for _, parent := range searchPaths {
found, err := filepath.Abs(filepath.Join(parent, path))
if err != nil {
continue
} else if fileInfo, err := os.Stat(found); err == nil {
if filter != nil {
if filter(fileInfo) {
return found
}
} else {
return found
}
}
}
return ""
}
func FindPath(path string, baseSearchPaths []string, filter func(os.FileInfo) bool) string {
return findPath(path, baseSearchPaths, true, filter)
}
// FindFile looks for the given file in nearby ancestors relative to the current working
// directory as well as the directory of the executable.
func FindFile(path string) string {
return FindPath(path, CommonBaseSearchPaths(), func(fileInfo os.FileInfo) bool {
return !fileInfo.IsDir()
})
}
// fileutils.FindDir looks for the given directory in nearby ancestors relative to the current working
// directory as well as the directory of the executable, falling back to `./` if not found.
func FindDir(dir string) (string, bool) {
found := FindPath(dir, CommonBaseSearchPaths(), func(fileInfo os.FileInfo) bool {
return fileInfo.IsDir()
})
if found == "" {
return "./", false
}
return found, true
}
// FindDirRelBinary looks for the given directory in nearby ancestors relative to the
// directory of the executable, then relative to the working directory, falling back to `./` if not found.
func FindDirRelBinary(dir string) (string, bool) {
found := findPath(dir, CommonBaseSearchPaths(), false, func(fileInfo os.FileInfo) bool {
return fileInfo.IsDir()
})
if found == "" {
return "./", false
}
return found, true
}

Просмотреть файл

@@ -0,0 +1,123 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package fileutils
import (
"fmt"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestFindFile(t *testing.T) {
t.Run("files from various paths", func(t *testing.T) {
// Create the following directory structure:
// tmpDir1/
// file1.json
// file2.xml
// other.txt
// tmpDir2/
// other.txt/ [directory]
// tmpDir3/
// tmpDir4/
// tmpDir5/
tmpDir1, err := os.MkdirTemp("", "")
require.NoError(t, err)
defer os.RemoveAll(tmpDir1)
tmpDir2, err := os.MkdirTemp(tmpDir1, "")
require.NoError(t, err)
err = os.Mkdir(filepath.Join(tmpDir2, "other.txt"), 0700)
require.NoError(t, err)
tmpDir3, err := os.MkdirTemp(tmpDir2, "")
require.NoError(t, err)
tmpDir4, err := os.MkdirTemp(tmpDir3, "")
require.NoError(t, err)
tmpDir5, err := os.MkdirTemp(tmpDir4, "")
require.NoError(t, err)
type testCase struct {
Description string
Cwd *string
FileName string
Expected string
}
testCases := []testCase{}
for _, fileName := range []string{"file1.json", "file2.xml", "other.txt"} {
filePath := filepath.Join(tmpDir1, fileName)
require.NoError(t, os.WriteFile(filePath, []byte("{}"), 0600))
// Relative paths end up getting symlinks fully resolved, so use this below as necessary.
filePathResolved, err := filepath.EvalSymlinks(filePath)
require.NoError(t, err)
testCases = append(testCases, []testCase{
{
fmt.Sprintf("absolute path to %s", fileName),
nil,
filePath,
filePath,
},
{
fmt.Sprintf("absolute path to %s from containing directory", fileName),
&tmpDir1,
filePath,
filePath,
},
{
fmt.Sprintf("relative path to %s from containing directory", fileName),
&tmpDir1,
fileName,
filePathResolved,
},
{
fmt.Sprintf("%s: subdirectory of containing directory", fileName),
&tmpDir2,
fileName,
filePathResolved,
},
{
fmt.Sprintf("%s: twice-nested subdirectory of containing directory", fileName),
&tmpDir3,
fileName,
filePathResolved,
},
{
fmt.Sprintf("%s: thrice-nested subdirectory of containing directory", fileName),
&tmpDir4,
fileName,
filePathResolved,
},
{
fmt.Sprintf("%s: quadruple-nested subdirectory of containing directory", fileName),
&tmpDir5,
fileName,
filePath,
},
}...)
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
if testCase.Cwd != nil {
prevDir, err := os.Getwd()
require.NoError(t, err)
defer os.Chdir(prevDir)
os.Chdir(*testCase.Cwd)
}
assert.Equal(t, testCase.Expected, FindFile(testCase.FileName))
})
}
})
}

16
server/channels/utils/hash.go Обычный файл
Просмотреть файл

@@ -0,0 +1,16 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"crypto/sha256"
"fmt"
)
func HashSha256(text string) string {
hash := sha256.New()
hash.Write([]byte(text))
return fmt.Sprintf("%x", hash.Sum(nil))
}

29
server/channels/utils/i18n.go Обычный файл
Просмотреть файл

@@ -0,0 +1,29 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"fmt"
"os"
"path/filepath"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/fileutils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
// this functions loads translations from filesystem if they are not
// loaded already and assigns english while loading server config
func TranslationsPreInit() error {
translationsDir := "i18n"
if mattermostPath := os.Getenv("MM_SERVER_PATH"); mattermostPath != "" {
translationsDir = filepath.Join(mattermostPath, "i18n")
}
i18nDirectory, found := fileutils.FindDirRelBinary(translationsDir)
if !found {
return fmt.Errorf("unable to find i18n directory at %q", translationsDir)
}
return i18n.TranslationsPreInit(i18nDirectory)
}

505
server/channels/utils/imgutils/gif.go Обычный файл
Просмотреть файл

@@ -0,0 +1,505 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// This is a modified version, the original copyright was: Copyright (c) 2011
// The Go Authors.
package imgutils
// This contains a portion of Go's image/go library, modified to count the number of frames in a gif without loading
// the entire image into memory.
import (
"bufio"
"compress/lzw"
"encoding/binary"
"errors"
"fmt"
"io"
)
var (
errNotEnough = errors.New("gif: not enough image data")
errTooMuch = errors.New("gif: too much image data")
)
// If the io.Reader does not also have ReadByte, then decode will introduce its own buffering.
type reader interface {
io.Reader
io.ByteReader
}
// Masks etc.
const (
// Fields.
fColorTable = 1 << 7
fColorTableBitsMask = 7
// Graphic control flags.
gcTransparentColorSet = 1 << 0
gcDisposalMethodMask = 7 << 2
)
// Disposal Methods.
const (
DisposalNone = 0x01
DisposalBackground = 0x02
DisposalPrevious = 0x03
)
// Section indicators.
const (
sExtension = 0x21
sImageDescriptor = 0x2C
sTrailer = 0x3B
)
// Extensions.
const (
eText = 0x01 // Plain Text
eGraphicControl = 0xF9 // Graphic Control
eComment = 0xFE // Comment
eApplication = 0xFF // Application
)
func readFull(r io.Reader, b []byte) error {
_, err := io.ReadFull(r, b)
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
return err
}
func readByte(r io.ByteReader) (byte, error) {
b, err := r.ReadByte()
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
return b, err
}
// decoder is the type used to decode a GIF file.
type decoder struct {
r reader
// From header.
vers string
width int
height int
loopCount int
delayTime int
backgroundIndex byte
disposalMethod byte
// From image descriptor.
imageFields byte
// From graphics control.
transparentIndex byte
hasTransparentIndex bool
// Computed.
hasGlobalColorTable bool
// Used when decoding.
imageCount int
tmp [1024]byte // must be at least 768 so we can read color table
}
// blockReader parses the block structure of GIF image data, which comprises
// (n, (n bytes)) blocks, with 1 <= n <= 255. It is the reader given to the
// LZW decoder, which is thus immune to the blocking. After the LZW decoder
// completes, there will be a 0-byte block remaining (0, ()), which is
// consumed when checking that the blockReader is exhausted.
//
// To avoid the allocation of a bufio.Reader for the lzw Reader, blockReader
// implements io.ReadByte and buffers blocks into the decoder's "tmp" buffer.
type blockReader struct {
d *decoder
i, j uint8 // d.tmp[i:j] contains the buffered bytes
err error
}
func (b *blockReader) fill() {
if b.err != nil {
return
}
b.j, b.err = readByte(b.d.r)
if b.j == 0 && b.err == nil {
b.err = io.EOF
}
if b.err != nil {
return
}
b.i = 0
b.err = readFull(b.d.r, b.d.tmp[:b.j])
if b.err != nil {
b.j = 0
}
}
func (b *blockReader) ReadByte() (byte, error) {
if b.i == b.j {
b.fill()
if b.err != nil {
return 0, b.err
}
}
c := b.d.tmp[b.i]
b.i++
return c, nil
}
// blockReader must implement io.Reader, but its Read shouldn't ever actually
// be called in practice. The compress/lzw package will only call ReadByte.
func (b *blockReader) Read(p []byte) (int, error) {
if len(p) == 0 || b.err != nil {
return 0, b.err
}
if b.i == b.j {
b.fill()
if b.err != nil {
return 0, b.err
}
}
n := copy(p, b.d.tmp[b.i:b.j])
b.i += uint8(n)
return n, nil
}
// close primarily detects whether or not a block terminator was encountered
// after reading a sequence of data sub-blocks. It allows at most one trailing
// sub-block worth of data. I.e., if some number of bytes exist in one sub-block
// following the end of LZW data, the very next sub-block must be the block
// terminator. If the very end of LZW data happened to fill one sub-block, at
// most one more sub-block of length 1 may exist before the block-terminator.
// These accommodations allow us to support GIFs created by less strict encoders.
// See https://golang.org/issue/16146.
func (b *blockReader) close() error {
if b.err == io.EOF {
// A clean block-sequence terminator was encountered while reading.
return nil
} else if b.err != nil {
// Some other error was encountered while reading.
return b.err
}
if b.i == b.j {
// We reached the end of a sub block reading LZW data. We'll allow at
// most one more sub block of data with a length of 1 byte.
b.fill()
if b.err == io.EOF {
return nil
} else if b.err != nil {
return b.err
} else if b.j > 1 {
return errTooMuch
}
}
// Part of a sub-block remains buffered. We expect that the next attempt to
// buffer a sub-block will reach the block terminator.
b.fill()
if b.err == io.EOF {
return nil
} else if b.err != nil {
return b.err
}
return errTooMuch
}
// decode reads a GIF image from r and stores the result in d.
func (d *decoder) decode(r io.Reader, configOnly bool) error {
// Add buffering if r does not provide ReadByte.
if rr, ok := r.(reader); ok {
d.r = rr
} else {
d.r = bufio.NewReader(r)
}
d.loopCount = -1
err := d.readHeaderAndScreenDescriptor()
if err != nil {
return err
}
if configOnly {
return nil
}
for {
c, err := readByte(d.r)
if err != nil {
return fmt.Errorf("gif: reading frames: %v", err)
}
switch c {
case sExtension:
if err = d.readExtension(); err != nil {
return err
}
case sImageDescriptor:
if err = d.readImageDescriptor(); err != nil {
return err
}
case sTrailer:
if d.imageCount == 0 {
return fmt.Errorf("gif: missing image data")
}
return nil
default:
return fmt.Errorf("gif: unknown block type: 0x%.2x", c)
}
}
}
func (d *decoder) readHeaderAndScreenDescriptor() error {
err := readFull(d.r, d.tmp[:13])
if err != nil {
return fmt.Errorf("gif: reading header: %v", err)
}
d.vers = string(d.tmp[:6])
if d.vers != "GIF87a" && d.vers != "GIF89a" {
return fmt.Errorf("gif: can't recognize format %q", d.vers)
}
d.width = int(d.tmp[6]) + int(d.tmp[7])<<8
d.height = int(d.tmp[8]) + int(d.tmp[9])<<8
if fields := d.tmp[10]; fields&fColorTable != 0 {
d.backgroundIndex = d.tmp[11]
// readColorTable overwrites the contents of d.tmp, but that's OK.
if err = d.readColorTable(fields); err != nil {
return err
}
d.hasGlobalColorTable = true
}
// d.tmp[12] is the Pixel Aspect Ratio, which is ignored.
return nil
}
func (d *decoder) readColorTable(fields byte) error {
n := 1 << (1 + uint(fields&fColorTableBitsMask))
err := readFull(d.r, d.tmp[:3*n])
if err != nil {
return fmt.Errorf("gif: reading color table: %s", err)
}
return nil
}
func (d *decoder) readExtension() error {
extension, err := readByte(d.r)
if err != nil {
return fmt.Errorf("gif: reading extension: %v", err)
}
size := 0
switch extension {
case eText:
size = 13
case eGraphicControl:
return d.readGraphicControl()
case eComment:
// nothing to do but read the data.
case eApplication:
b, err := readByte(d.r)
if err != nil {
return fmt.Errorf("gif: reading extension: %v", err)
}
// The spec requires size be 11, but Adobe sometimes uses 10.
size = int(b)
default:
return fmt.Errorf("gif: unknown extension 0x%.2x", extension)
}
if size > 0 {
if err := readFull(d.r, d.tmp[:size]); err != nil {
return fmt.Errorf("gif: reading extension: %v", err)
}
}
// Application Extension with "NETSCAPE2.0" as string and 1 in data means
// this extension defines a loop count.
if extension == eApplication && string(d.tmp[:size]) == "NETSCAPE2.0" {
n, err := d.readBlock()
if err != nil {
return fmt.Errorf("gif: reading extension: %v", err)
}
if n == 0 {
return nil
}
if n == 3 && d.tmp[0] == 1 {
d.loopCount = int(d.tmp[1]) | int(d.tmp[2])<<8
}
}
for {
n, err := d.readBlock()
if err != nil {
return fmt.Errorf("gif: reading extension: %v", err)
}
if n == 0 {
return nil
}
}
}
func (d *decoder) readGraphicControl() error {
if err := readFull(d.r, d.tmp[:6]); err != nil {
return fmt.Errorf("gif: can't read graphic control: %s", err)
}
if d.tmp[0] != 4 {
return fmt.Errorf("gif: invalid graphic control extension block size: %d", d.tmp[0])
}
flags := d.tmp[1]
d.disposalMethod = (flags & gcDisposalMethodMask) >> 2
d.delayTime = int(d.tmp[2]) | int(d.tmp[3])<<8
if flags&gcTransparentColorSet != 0 {
d.transparentIndex = d.tmp[4]
d.hasTransparentIndex = true
}
if d.tmp[5] != 0 {
return fmt.Errorf("gif: invalid graphic control extension block terminator: %d", d.tmp[5])
}
return nil
}
func (d *decoder) readImageDescriptor() error {
err := d.checkImageFromDescriptor()
if err != nil {
return err
}
useLocalColorTable := d.imageFields&fColorTable != 0
if useLocalColorTable {
if err = d.readColorTable(d.imageFields); err != nil {
return err
}
} else if !d.hasGlobalColorTable {
return errors.New("gif: no color table")
}
litWidth, err := readByte(d.r)
if err != nil {
return fmt.Errorf("gif: reading image data: %v", err)
}
if litWidth < 2 || litWidth > 8 {
return fmt.Errorf("gif: pixel size in decode out of range: %d", litWidth)
}
// A wonderfully Go-like piece of magic.
br := &blockReader{d: d}
lzwr := lzw.NewReader(br, lzw.LSB, int(litWidth))
defer lzwr.Close()
if _, err := io.Copy(io.Discard, lzwr); err != nil {
if err != io.ErrUnexpectedEOF {
return fmt.Errorf("gif: reading image data: %v", err)
}
return errNotEnough
}
// In theory, both lzwr and br should be exhausted. Reading from them
// should yield (0, io.EOF).
//
// The spec (Appendix F - Compression), says that "An End of
// Information code... must be the last code output by the encoder
// for an image". In practice, though, giflib (a widely used C
// library) does not enforce this, so we also accept lzwr returning
// io.ErrUnexpectedEOF (meaning that the encoded stream hit io.EOF
// before the LZW decoder saw an explicit end code), provided that
// the io.ReadFull call above successfully read len(m.Pix) bytes.
// See https://golang.org/issue/9856 for an example GIF.
if n, err := lzwr.Read(d.tmp[256:257]); n != 0 || (err != io.EOF && err != io.ErrUnexpectedEOF) {
if err != nil {
return fmt.Errorf("gif: reading image data: %v", err)
}
return errTooMuch
}
// In practice, some GIFs have an extra byte in the data sub-block
// stream, which we ignore. See https://golang.org/issue/16146.
if err := br.close(); err == errTooMuch {
return errTooMuch
} else if err != nil {
return fmt.Errorf("gif: reading image data: %v", err)
}
d.imageCount += 1
return nil
}
func (d *decoder) checkImageFromDescriptor() error {
if err := readFull(d.r, d.tmp[:9]); err != nil {
return fmt.Errorf("gif: can't read image descriptor: %s", err)
}
left := int(d.tmp[0]) + int(d.tmp[1])<<8
top := int(d.tmp[2]) + int(d.tmp[3])<<8
width := int(d.tmp[4]) + int(d.tmp[5])<<8
height := int(d.tmp[6]) + int(d.tmp[7])<<8
d.imageFields = d.tmp[8]
// The GIF89a spec, Section 20 (Image Descriptor) says: "Each image must
// fit within the boundaries of the Logical Screen, as defined in the
// Logical Screen Descriptor."
//
// This is conceptually similar to testing
// frameBounds := image.Rect(left, top, left+width, top+height)
// imageBounds := image.Rect(0, 0, d.width, d.height)
// if !frameBounds.In(imageBounds) { etc }
// but the semantics of the Go image.Rectangle type is that r.In(s) is true
// whenever r is an empty rectangle, even if r.Min.X > s.Max.X. Here, we
// want something stricter.
//
// Note that, by construction, left >= 0 && top >= 0, so we only have to
// explicitly compare frameBounds.Max (left+width, top+height) against
// imageBounds.Max (d.width, d.height) and not frameBounds.Min (left, top)
// against imageBounds.Min (0, 0).
if left+width > d.width || top+height > d.height {
return errors.New("gif: frame bounds larger than image bounds")
}
return nil
}
func (d *decoder) readBlock() (int, error) {
n, err := readByte(d.r)
if n == 0 || err != nil {
return 0, err
}
if err := readFull(d.r, d.tmp[:n]); err != nil {
return 0, err
}
return int(n), nil
}
func CountGIFFrames(r io.Reader) (int, error) {
var d decoder
if err := d.decode(r, false); err != nil {
return -1, err
}
return d.imageCount, nil
}
func GenGIFData(width, height uint16, nFrames int) []byte {
header := []byte{
'G', 'I', 'F', '8', '9', 'a', // header
0, 0, 0, 0, // width and height
128, 0, 0, // other header information
0, 0, 0, 1, 1, 1, // color table
}
binary.LittleEndian.PutUint16(header[6:], width)
binary.LittleEndian.PutUint16(header[8:], height)
frame := []byte{
0x2c, // block introducer
0, 0, 0, 0, 1, 0, 1, 0, // position and dimensions of the frame
0, // other frame information
0x2, 0x2, 0x4c, 0x1, 0, // encoded pixel data
}
trailer := []byte{0x3b}
gifData := header
for i := 0; i < nFrames; i++ {
gifData = append(gifData, frame...)
}
gifData = append(gifData, trailer...)
return gifData
}

89
server/channels/utils/imgutils/gif_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,89 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package imgutils
import (
"bytes"
"image"
_ "image/gif"
"io"
"os"
"path/filepath"
"testing"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/fileutils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func readTestFile(t *testing.T, name string) ([]byte, error) {
t.Helper()
path, _ := fileutils.FindDir("tests")
file, err := os.Open(filepath.Join(path, name))
if err != nil {
return nil, err
}
defer file.Close()
data := &bytes.Buffer{}
if _, err := io.Copy(data, file); err != nil {
return nil, err
}
return data.Bytes(), nil
}
func TestGenGIFData(t *testing.T) {
data := GenGIFData(600, 400, 1)
img, format, err := image.DecodeConfig(bytes.NewReader(data))
require.NoError(t, err)
require.Equal(t, 600, img.Width)
require.Equal(t, 400, img.Height)
require.Equal(t, "gif", format)
}
func TestCountGIFFrames(t *testing.T) {
t.Run("should count the frames of a static gif", func(t *testing.T) {
gifData := GenGIFData(400, 400, 1)
count, err := CountGIFFrames(bytes.NewReader(gifData))
assert.NoError(t, err)
assert.Equal(t, 1, count)
})
t.Run("should count the frames of an animated gif", func(t *testing.T) {
gifData := GenGIFData(400, 400, 100)
count, err := CountGIFFrames(bytes.NewReader(gifData))
assert.NoError(t, err)
assert.Equal(t, 100, count)
})
t.Run("should count the frames of an actual animated gif", func(t *testing.T) {
b, err := readTestFile(t, "testgif.gif")
require.NoError(t, err)
count, err := CountGIFFrames(bytes.NewReader(b))
assert.NoError(t, err)
assert.Equal(t, 4, count)
})
t.Run("should return an error for a non-gif image", func(t *testing.T) {
b, err := readTestFile(t, "test.png")
require.NoError(t, err)
_, err = CountGIFFrames(bytes.NewReader(b))
assert.Error(t, err)
})
t.Run("should return an error for garbage data", func(t *testing.T) {
_, err := CountGIFFrames(bytes.NewReader([]byte("garbage data")))
assert.Error(t, err)
})
}

56
server/channels/utils/jsonutils/json.go Обычный файл
Просмотреть файл

@@ -0,0 +1,56 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package jsonutils
import (
"bytes"
"encoding/json"
"github.com/pkg/errors"
)
type HumanizedJSONError struct {
Err error
Line int
Character int
}
func (e *HumanizedJSONError) Error() string {
return e.Err.Error()
}
// HumanizeJSONError extracts error offsets and annotates the error with useful context
func HumanizeJSONError(err error, data []byte) error {
if syntaxError, ok := err.(*json.SyntaxError); ok {
return NewHumanizedJSONError(syntaxError, data, syntaxError.Offset)
} else if unmarshalError, ok := err.(*json.UnmarshalTypeError); ok {
return NewHumanizedJSONError(unmarshalError, data, unmarshalError.Offset)
} else {
return err
}
}
func NewHumanizedJSONError(err error, data []byte, offset int64) *HumanizedJSONError {
if err == nil {
return nil
}
if offset < 0 || offset > int64(len(data)) {
return &HumanizedJSONError{
Err: errors.Wrapf(err, "invalid offset %d", offset),
}
}
lineSep := []byte{'\n'}
line := bytes.Count(data[:offset], lineSep) + 1
lastLineOffset := bytes.LastIndex(data[:offset], lineSep)
character := int(offset) - (lastLineOffset + 1) + 1
return &HumanizedJSONError{
Line: line,
Character: character,
Err: errors.Wrapf(err, "parsing error at line %d, character %d", line, character),
}
}

235
server/channels/utils/jsonutils/json_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,235 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package jsonutils_test
import (
"encoding/json"
"reflect"
"testing"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/jsonutils"
)
func TestHumanizeJsonError(t *testing.T) {
t.Parallel()
type testType struct{}
testCases := []struct {
Description string
Data []byte
Err error
ExpectedErr string
}{
{
"nil error",
[]byte{},
nil,
"",
},
{
"non-special error",
[]byte{},
errors.New("test"),
"test",
},
{
"syntax error, offset 17, middle of line 3",
[]byte("line 1\nline 2\nline 3"),
&json.SyntaxError{
// msg can't be set
Offset: 17,
},
"parsing error at line 3, character 4: ",
},
{
"unmarshal type error, offset 17, middle of line 3",
[]byte("line 1\nline 2\nline 3"),
&json.UnmarshalTypeError{
Value: "bool",
Type: reflect.TypeOf(testType{}),
Offset: 17,
Struct: "struct",
Field: "field",
},
"parsing error at line 3, character 4: json: cannot unmarshal bool into Go struct field struct.field of type jsonutils_test.testType",
},
}
for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.Description, func(t *testing.T) {
actual := jsonutils.HumanizeJSONError(testCase.Err, testCase.Data)
if testCase.ExpectedErr == "" {
assert.NoError(t, actual)
} else {
assert.EqualError(t, actual, testCase.ExpectedErr)
}
})
}
}
func TestNewHumanizedJSONError(t *testing.T) {
t.Parallel()
testCases := []struct {
Description string
Data []byte
Offset int64
Err error
Expected *jsonutils.HumanizedJSONError
}{
{
"nil error",
[]byte{},
0,
nil,
nil,
},
{
"offset -1, before start of string",
[]byte("line 1\nline 2\nline 3"),
-1,
errors.New("message"),
&jsonutils.HumanizedJSONError{
Err: errors.Wrap(errors.New("message"), "invalid offset -1"),
},
},
{
"offset 0, start of string",
[]byte("line 1\nline 2\nline 3"),
0,
errors.New("message"),
&jsonutils.HumanizedJSONError{
Err: errors.Wrap(errors.New("message"), "parsing error at line 1, character 1"),
Line: 1,
Character: 1,
},
},
{
"offset 5, end of line 1",
[]byte("line 1\nline 2\nline 3"),
5,
errors.New("message"),
&jsonutils.HumanizedJSONError{
Err: errors.Wrap(errors.New("message"), "parsing error at line 1, character 6"),
Line: 1,
Character: 6,
},
},
{
"offset 6, new line at end end of line 1",
[]byte("line 1\nline 2\nline 3"),
6,
errors.New("message"),
&jsonutils.HumanizedJSONError{
Err: errors.Wrap(errors.New("message"), "parsing error at line 1, character 7"),
Line: 1,
Character: 7,
},
},
{
"offset 7, start of line 2",
[]byte("line 1\nline 2\nline 3"),
7,
errors.New("message"),
&jsonutils.HumanizedJSONError{
Err: errors.Wrap(errors.New("message"), "parsing error at line 2, character 1"),
Line: 2,
Character: 1,
},
},
{
"offset 12, end of line 2",
[]byte("line 1\nline 2\nline 3"),
12,
errors.New("message"),
&jsonutils.HumanizedJSONError{
Err: errors.Wrap(errors.New("message"), "parsing error at line 2, character 6"),
Line: 2,
Character: 6,
},
},
{
"offset 13, newline at end of line 2",
[]byte("line 1\nline 2\nline 3"),
13,
errors.New("message"),
&jsonutils.HumanizedJSONError{
Err: errors.Wrap(errors.New("message"), "parsing error at line 2, character 7"),
Line: 2,
Character: 7,
},
},
{
"offset 17, middle of line 3",
[]byte("line 1\nline 2\nline 3"),
17,
errors.New("message"),
&jsonutils.HumanizedJSONError{
Err: errors.Wrap(errors.New("message"), "parsing error at line 3, character 4"),
Line: 3,
Character: 4,
},
},
{
"offset 19, end of string",
[]byte("line 1\nline 2\nline 3"),
19,
errors.New("message"),
&jsonutils.HumanizedJSONError{
Err: errors.Wrap(errors.New("message"), "parsing error at line 3, character 6"),
Line: 3,
Character: 6,
},
},
{
"offset 20, offset = length of string",
[]byte("line 1\nline 2\nline 3"),
20,
errors.New("message"),
&jsonutils.HumanizedJSONError{
Err: errors.Wrap(errors.New("message"), "parsing error at line 3, character 7"),
Line: 3,
Character: 7,
},
},
{
"offset 21, offset = length of string, after newline",
[]byte("line 1\nline 2\nline 3\n"),
21,
errors.New("message"),
&jsonutils.HumanizedJSONError{
Err: errors.Wrap(errors.New("message"), "parsing error at line 4, character 1"),
Line: 4,
Character: 1,
},
},
{
"offset 21, offset > length of string",
[]byte("line 1\nline 2\nline 3"),
21,
errors.New("message"),
&jsonutils.HumanizedJSONError{
Err: errors.Wrap(errors.New("message"), "invalid offset 21"),
},
},
}
for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.Description, func(t *testing.T) {
actual := jsonutils.NewHumanizedJSONError(testCase.Err, testCase.Data, testCase.Offset)
if testCase.Expected != nil && actual.Err != nil {
if assert.EqualValues(t, testCase.Expected.Err.Error(), actual.Err.Error()) {
actual.Err = testCase.Expected.Err
}
}
assert.Equal(t, testCase.Expected, actual)
})
}
}

Просмотреть файл

@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyZmShlU8Z8HdG0IWSZ8r
tSyzyxrXkJjsFUf0Ke7bm/TLtIggRdqOcUF3XEWqQk5RGD5vuq7Rlg1zZqMEBk8N
EZeRhkxyaZW8pLjxwuBUOnXfJew31+gsTNdKZzRjrvPumKr3EtkleuoxNdoatu4E
HrKmR/4Yi71EqAvkhk7ZjQFuF0osSWJMEEGGCSUYQnTEqUzcZSh1BhVpkIkeu8Kk
1wCtptODixvEujgqVe+SrE3UlZjBmPjC/CL+3cYmufpSNgcEJm2mwsdaXp2OPpfn
a0v85XL6i9ote2P+fLZ3wX9EoioHzgdgB7arOxY50QRJO7OyCqpKFKv6lRWTXuSt
hwIDAQAB
-----END PUBLIC KEY-----

Просмотреть файл

@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyZmShlU8Z8HdG0IWSZ8r
tSyzyxrXkJjsFUf0Ke7bm/TLtIggRdqOcUF3XEWqQk5RGD5vuq7Rlg1zZqMEBk8N
EZeRhkxyaZW8pLjxwuBUOnXfJew31+gsTNdKZzRjrvPumKr3EtkleuoxNdoatu4E
HrKmR/4Yi71EqAvkhk7ZjQFuF0osSWJMEEGGCSUYQnTEqUzcZSh1BhVpkIkeu8Kk
1wCtptODixvEujgqVe+SrE3UlZjBmPjC/CL+3cYmufpSNgcEJm2mwsdaXp2OPpfn
a0v85XL6i9ote2P+fLZ3wX9EoioHzgdgB7arOxY50QRJO7OyCqpKFKv6lRWTXuSt
hwIDAQAB
-----END PUBLIC KEY-----

216
server/channels/utils/license.go Обычный файл
Просмотреть файл

@@ -0,0 +1,216 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"crypto"
"crypto/rsa"
"crypto/sha512"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"io"
"net/http"
"os"
"path/filepath"
"strconv"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/fileutils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var LicenseValidator LicenseValidatorIface
func init() {
if LicenseValidator == nil {
LicenseValidator = &LicenseValidatorImpl{}
}
}
type LicenseValidatorIface interface {
LicenseFromBytes(licenseBytes []byte) (*model.License, *model.AppError)
ValidateLicense(signed []byte) (bool, string)
}
type LicenseValidatorImpl struct {
}
func (l *LicenseValidatorImpl) LicenseFromBytes(licenseBytes []byte) (*model.License, *model.AppError) {
success, licenseStr := l.ValidateLicense(licenseBytes)
if !success {
return nil, model.NewAppError("LicenseFromBytes", model.InvalidLicenseError, nil, "", http.StatusBadRequest)
}
var license model.License
if jsonErr := json.Unmarshal([]byte(licenseStr), &license); jsonErr != nil {
return nil, model.NewAppError("LicenseFromBytes", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
return &license, nil
}
func (l *LicenseValidatorImpl) ValidateLicense(signed []byte) (bool, string) {
decoded := make([]byte, base64.StdEncoding.DecodedLen(len(signed)))
_, err := base64.StdEncoding.Decode(decoded, signed)
if err != nil {
mlog.Error("Encountered error decoding license", mlog.Err(err))
return false, ""
}
// remove null terminator
for len(decoded) > 0 && decoded[len(decoded)-1] == byte(0) {
decoded = decoded[:len(decoded)-1]
}
if len(decoded) <= 256 {
mlog.Error("Signed license not long enough")
return false, ""
}
plaintext := decoded[:len(decoded)-256]
signature := decoded[len(decoded)-256:]
block, _ := pem.Decode(publicKey)
public, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
mlog.Error("Encountered error signing license", mlog.Err(err))
return false, ""
}
rsaPublic := public.(*rsa.PublicKey)
h := sha512.New()
h.Write(plaintext)
d := h.Sum(nil)
err = rsa.VerifyPKCS1v15(rsaPublic, crypto.SHA512, d, signature)
if err != nil {
mlog.Error("Invalid signature", mlog.Err(err))
return false, ""
}
return true, string(plaintext)
}
func GetAndValidateLicenseFileFromDisk(location string) (*model.License, []byte) {
fileName := GetLicenseFileLocation(location)
if _, err := os.Stat(fileName); err != nil {
mlog.Debug("We could not find the license key in the database or on disk at", mlog.String("filename", fileName))
return nil, nil
}
mlog.Info("License key has not been uploaded. Loading license key from disk at", mlog.String("filename", fileName))
licenseBytes := GetLicenseFileFromDisk(fileName)
success, licenseStr := LicenseValidator.ValidateLicense(licenseBytes)
if !success {
mlog.Error("Found license key at %v but it appears to be invalid.", mlog.String("filename", fileName))
return nil, nil
}
var license model.License
if jsonErr := json.Unmarshal([]byte(licenseStr), &license); jsonErr != nil {
mlog.Error("Failed to decode license from JSON", mlog.Err(jsonErr))
return nil, nil
}
return &license, licenseBytes
}
func GetLicenseFileFromDisk(fileName string) []byte {
file, err := os.Open(fileName)
if err != nil {
mlog.Error("Failed to open license key from disk at", mlog.String("filename", fileName), mlog.Err(err))
return nil
}
defer file.Close()
licenseBytes, err := io.ReadAll(file)
if err != nil {
mlog.Error("Failed to read license key from disk at", mlog.String("filename", fileName), mlog.Err(err))
return nil
}
return licenseBytes
}
func GetLicenseFileLocation(fileLocation string) string {
if fileLocation == "" {
configDir, _ := fileutils.FindDir("config")
return filepath.Join(configDir, "mattermost.mattermost-license")
}
return fileLocation
}
func GetClientLicense(l *model.License) map[string]string {
props := make(map[string]string)
props["IsLicensed"] = strconv.FormatBool(l != nil)
if l != nil {
props["Id"] = l.Id
props["SkuName"] = l.SkuName
props["SkuShortName"] = l.SkuShortName
props["Users"] = strconv.Itoa(*l.Features.Users)
props["LDAP"] = strconv.FormatBool(*l.Features.LDAP)
props["LDAPGroups"] = strconv.FormatBool(*l.Features.LDAPGroups)
props["MFA"] = strconv.FormatBool(*l.Features.MFA)
props["SAML"] = strconv.FormatBool(*l.Features.SAML)
props["Cluster"] = strconv.FormatBool(*l.Features.Cluster)
props["Metrics"] = strconv.FormatBool(*l.Features.Metrics)
props["GoogleOAuth"] = strconv.FormatBool(*l.Features.GoogleOAuth)
props["Office365OAuth"] = strconv.FormatBool(*l.Features.Office365OAuth)
props["OpenId"] = strconv.FormatBool(*l.Features.OpenId)
props["Compliance"] = strconv.FormatBool(*l.Features.Compliance)
props["MHPNS"] = strconv.FormatBool(*l.Features.MHPNS)
props["Announcement"] = strconv.FormatBool(*l.Features.Announcement)
props["Elasticsearch"] = strconv.FormatBool(*l.Features.Elasticsearch)
props["DataRetention"] = strconv.FormatBool(*l.Features.DataRetention)
props["IDLoadedPushNotifications"] = strconv.FormatBool(*l.Features.IDLoadedPushNotifications)
props["IssuedAt"] = strconv.FormatInt(l.IssuedAt, 10)
props["StartsAt"] = strconv.FormatInt(l.StartsAt, 10)
props["ExpiresAt"] = strconv.FormatInt(l.ExpiresAt, 10)
props["Name"] = l.Customer.Name
props["Email"] = l.Customer.Email
props["Company"] = l.Customer.Company
props["EmailNotificationContents"] = strconv.FormatBool(*l.Features.EmailNotificationContents)
props["MessageExport"] = strconv.FormatBool(*l.Features.MessageExport)
props["CustomPermissionsSchemes"] = strconv.FormatBool(*l.Features.CustomPermissionsSchemes)
props["GuestAccounts"] = strconv.FormatBool(*l.Features.GuestAccounts)
props["GuestAccountsPermissions"] = strconv.FormatBool(*l.Features.GuestAccountsPermissions)
props["CustomTermsOfService"] = strconv.FormatBool(*l.Features.CustomTermsOfService)
props["LockTeammateNameDisplay"] = strconv.FormatBool(*l.Features.LockTeammateNameDisplay)
props["Cloud"] = strconv.FormatBool(*l.Features.Cloud)
props["SharedChannels"] = strconv.FormatBool(*l.Features.SharedChannels)
props["RemoteClusterService"] = strconv.FormatBool(*l.Features.RemoteClusterService)
props["IsTrial"] = strconv.FormatBool(l.IsTrial)
props["IsGovSku"] = strconv.FormatBool(l.IsGovSku)
}
return props
}
func GetSanitizedClientLicense(l map[string]string) map[string]string {
sanitizedLicense := make(map[string]string)
for k, v := range l {
sanitizedLicense[k] = v
}
delete(sanitizedLicense, "Id")
delete(sanitizedLicense, "Name")
delete(sanitizedLicense, "Email")
delete(sanitizedLicense, "IssuedAt")
delete(sanitizedLicense, "StartsAt")
delete(sanitizedLicense, "ExpiresAt")
delete(sanitizedLicense, "SkuName")
delete(sanitizedLicense, "SkuShortName")
return sanitizedLicense
}

10
server/channels/utils/license_public_key.go Обычный файл
Просмотреть файл

@@ -0,0 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
//go:build !testlicensekey
package utils
import _ "embed"
//go:embed license-public-key.txt
var publicKey []byte

Просмотреть файл

@@ -0,0 +1,13 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
//go:build testlicensekey
package utils
import _ "embed"
// TODO: license-public-key-test.txt currently has the contents of the prod public key.
// Change to the test public key when ready for dev images to use test license key.
//go:embed license-public-key-test.txt
var publicKey []byte

93
server/channels/utils/license_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,93 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"bytes"
"encoding/base64"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestValidateLicense(t *testing.T) {
t.Run("should fail with junk data", func(t *testing.T) {
b1 := []byte("junk")
ok, _ := LicenseValidator.ValidateLicense(b1)
require.False(t, ok, "should have failed - bad license")
b2 := []byte("junkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunk")
ok, _ = LicenseValidator.ValidateLicense(b2)
require.False(t, ok, "should have failed - bad license")
})
t.Run("should not panic on shorted than expected input", func(t *testing.T) {
var licenseData bytes.Buffer
var inputData []byte
for i := 0; i < 255; i++ {
inputData = append(inputData, 'A')
}
inputData = append(inputData, 0x00)
encoder := base64.NewEncoder(base64.StdEncoding, &licenseData)
_, err := encoder.Write(inputData)
require.NoError(t, err)
err = encoder.Close()
require.NoError(t, err)
ok, str := LicenseValidator.ValidateLicense(licenseData.Bytes())
require.False(t, ok)
require.Empty(t, str)
})
t.Run("should not panic with input filled of null terminators", func(t *testing.T) {
var licenseData bytes.Buffer
var inputData []byte
for i := 0; i < 256; i++ {
inputData = append(inputData, 0x00)
}
encoder := base64.NewEncoder(base64.StdEncoding, &licenseData)
_, err := encoder.Write(inputData)
require.NoError(t, err)
err = encoder.Close()
require.NoError(t, err)
ok, str := LicenseValidator.ValidateLicense(licenseData.Bytes())
require.False(t, ok)
require.Empty(t, str)
})
}
func TestGetLicenseFileLocation(t *testing.T) {
fileName := GetLicenseFileLocation("")
require.NotEmpty(t, fileName, "invalid default file name")
fileName = GetLicenseFileLocation("mattermost.mattermost-license")
require.Equal(t, fileName, "mattermost.mattermost-license", "invalid file name")
}
func TestGetLicenseFileFromDisk(t *testing.T) {
t.Run("missing file", func(t *testing.T) {
fileBytes := GetLicenseFileFromDisk("thisfileshouldnotexist.mattermost-license")
assert.Empty(t, fileBytes, "invalid bytes")
})
t.Run("not a license file", func(t *testing.T) {
f, err := os.CreateTemp("", "TestGetLicenseFileFromDisk")
require.NoError(t, err)
defer os.Remove(f.Name())
os.WriteFile(f.Name(), []byte("not a license"), 0777)
fileBytes := GetLicenseFileFromDisk(f.Name())
require.NotEmpty(t, fileBytes, "should have read the file")
success, _ := LicenseValidator.ValidateLicense(fileBytes)
assert.False(t, success, "should have been an invalid file")
})
}

172
server/channels/utils/markdown.go Обычный файл
Просмотреть файл

@@ -0,0 +1,172 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"html"
"regexp"
"strings"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/extension"
astExt "github.com/yuin/goldmark/extension/ast"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/util"
)
// StripMarkdown remove some markdown syntax
func StripMarkdown(markdown string) (string, error) {
md := goldmark.New(
goldmark.WithExtensions(extension.Strikethrough),
goldmark.WithRenderer(
renderer.NewRenderer(renderer.WithNodeRenderers(
util.Prioritized(newNotificationRenderer(), 500),
)),
),
)
var buf strings.Builder
if err := md.Convert([]byte(markdown), &buf); err != nil {
return "", err
}
return strings.TrimSpace(buf.String()), nil
}
var relLinkReg = regexp.MustCompile(`\[(.*)]\((/.*)\)`)
var blockquoteReg = regexp.MustCompile(`^|\n(&gt;)`)
// MarkdownToHTML takes a string containing Markdown and returns a string with HTML tagged version
func MarkdownToHTML(markdown, siteURL string) (string, error) {
// Turn relative links into absolute links
absLinkMarkdown := relLinkReg.ReplaceAllStringFunc(markdown, func(s string) string {
return relLinkReg.ReplaceAllString(s, "[$1]("+siteURL+"$2)")
})
// Unescape any blockquote text to be parsed by the markdown parser.
markdownClean := blockquoteReg.ReplaceAllStringFunc(absLinkMarkdown, func(s string) string {
return html.UnescapeString(s)
})
md := goldmark.New(
goldmark.WithExtensions(extension.GFM),
)
var b strings.Builder
err := md.Convert([]byte(markdownClean), &b)
if err != nil {
return "", err
}
return b.String(), nil
}
type notificationRenderer struct {
}
func newNotificationRenderer() *notificationRenderer {
return &notificationRenderer{}
}
func (r *notificationRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
// block
reg.Register(ast.KindDocument, r.renderDefault)
reg.Register(ast.KindHeading, r.renderItem)
reg.Register(ast.KindBlockquote, r.renderDefault)
reg.Register(ast.KindCodeBlock, r.renderCodeBlock)
reg.Register(ast.KindFencedCodeBlock, r.renderFencedCodeBlock)
reg.Register(ast.KindHTMLBlock, r.renderDefault)
reg.Register(ast.KindList, r.renderDefault)
reg.Register(ast.KindListItem, r.renderItem)
reg.Register(ast.KindParagraph, r.renderItem)
reg.Register(ast.KindTextBlock, r.renderTextBlock)
reg.Register(ast.KindThematicBreak, r.renderDefault)
// inlines
reg.Register(ast.KindAutoLink, r.renderDefault)
reg.Register(ast.KindCodeSpan, r.renderDefault)
reg.Register(ast.KindEmphasis, r.renderDefault)
reg.Register(ast.KindImage, r.renderDefault)
reg.Register(ast.KindLink, r.renderDefault)
reg.Register(ast.KindRawHTML, r.renderDefault)
reg.Register(ast.KindText, r.renderText)
reg.Register(ast.KindString, r.renderString)
// strikethrough
reg.Register(astExt.KindStrikethrough, r.renderDefault)
}
// renderDefault renderer function to renderDefault without changes
func (r *notificationRenderer) renderDefault(_ util.BufWriter, _ []byte, _ ast.Node, _ bool) (ast.WalkStatus, error) {
return ast.WalkContinue, nil
}
func (r *notificationRenderer) renderItem(w util.BufWriter, _ []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
if node.NextSibling() != nil {
_ = w.WriteByte(' ')
}
}
return ast.WalkContinue, nil
}
func (r *notificationRenderer) renderCodeBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
n := node.(*ast.CodeBlock)
if entering {
r.writeLines(w, source, n)
}
return ast.WalkContinue, nil
}
func (r *notificationRenderer) renderFencedCodeBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
n := node.(*ast.FencedCodeBlock)
if entering {
r.writeLines(w, source, n)
}
return ast.WalkContinue, nil
}
func (r *notificationRenderer) renderText(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
n := node.(*ast.Text)
segment := n.Segment
_, _ = w.Write(segment.Value(source))
if !n.IsRaw() {
if n.HardLineBreak() || n.SoftLineBreak() {
_ = w.WriteByte('\n')
}
}
return ast.WalkContinue, nil
}
func (r *notificationRenderer) renderTextBlock(w util.BufWriter, _ []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
if node.NextSibling() != nil && node.FirstChild() != nil {
_ = w.WriteByte(' ')
}
}
return ast.WalkContinue, nil
}
func (r *notificationRenderer) renderString(w util.BufWriter, _ []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
n := node.(*ast.String)
_, _ = w.Write(n.Value)
return ast.WalkContinue, nil
}
func (r *notificationRenderer) writeLines(w util.BufWriter, source []byte, n ast.Node) {
for i := 0; i < n.Lines().Len(); i++ {
line := n.Lines().At(i)
value := line.Value(source)
_, _ = w.Write(value)
}
}

316
server/channels/utils/markdown_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,316 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestStripMarkdown(t *testing.T) {
tests := []struct {
name string
args string
want string
}{
{
name: "emoji: same",
args: "Hey :smile: :+1: :)",
want: "Hey :smile: :+1: :)",
},
{
name: "at-mention: same",
args: "Hey @user and @test",
want: "Hey @user and @test",
},
{
name: "channel-link: same",
args: "join ~channelname",
want: "join ~channelname",
},
{
name: "codespan: single backtick",
args: "`single backtick`",
want: "single backtick",
},
{
name: "codespan: double backtick",
args: "``double backtick``",
want: "double backtick",
},
{
name: "codespan: triple backtick",
args: "```triple backtick```",
want: "triple backtick",
},
{
name: "codespan: inline code",
args: "Inline `code` has ``double backtick`` and ```triple backtick``` around it.",
want: "Inline code has double backtick and triple backtick around it.",
},
{
name: "code block: single line code block",
args: "Code block\n```\nline\n```",
want: "Code block line",
},
{
name: "code block: multiline code block 2",
args: "Multiline\n```\nfunction(number) {\n return number + 1;\n}\n```",
want: "Multiline function(number) {\n return number + 1;\n}",
},
{
name: "code block: language highlighting",
args: "```javascript\nvar s = \"JavaScript syntax highlighting\";\nalert(s);\n```",
want: "var s = \"JavaScript syntax highlighting\";\nalert(s);",
},
{
name: "blockquote:",
args: "> Hey quote",
want: "Hey quote",
},
{
name: "blockquote: multiline",
args: "> Hey quote.\n> Hello quote.",
want: "Hey quote.\nHello quote.",
},
{
name: "heading: # H1 header",
args: "# H1 header",
want: "H1 header",
},
{
name: "heading: heading with @user",
args: "# H1 @user",
want: "H1 @user",
},
{
name: "heading: ## H2 header",
args: "## H2 header",
want: "H2 header",
},
{
name: "heading: ### H3 header",
args: "### H3 header",
want: "H3 header",
},
{
name: "heading: #### H4 header",
args: "#### H4 header",
want: "H4 header",
},
{
name: "heading: ##### H5 header",
args: "##### H5 header",
want: "H5 header",
},
{
name: "heading: ###### H6 header",
args: "###### H6 header",
want: "H6 header",
},
{
name: "heading: multiline with header and paragraph",
args: "###### H6 header\nThis is next line.\nAnother line.",
want: "H6 header This is next line.\nAnother line.",
},
{
name: "heading: multiline with header and list items",
args: "###### H6 header\n- list item 1\n- list item 2",
want: "H6 header list item 1 list item 2",
},
{
name: "heading: multiline with header and links",
args: "###### H6 header\n[link 1](https://mattermost.com) - [link 2](https://mattermost.com)",
want: "H6 header link 1 - link 2",
},
{
name: "list: 1. First ordered list item",
args: "1. First ordered list item",
want: "First ordered list item",
},
{
name: "list: 2. Another item",
args: "1. 2. Another item",
want: "Another item",
},
{
name: "list: * Unordered sub-list.",
args: "* Unordered sub-list.",
want: "Unordered sub-list.",
},
{
name: "list: - Or minuses",
args: "- Or minuses",
want: "Or minuses",
},
{
name: "list: + Or pluses",
args: "+ Or pluses",
want: "Or pluses",
},
{
name: "list: multiline",
args: "1. First ordered list item\n2. Another item",
want: "First ordered list item Another item",
},
{
name: "tablerow:)",
args: "Markdown | Less | Pretty\n" +
"--- | --- | ---\n" +
"*Still* | `renders` | **nicely**\n" +
"1 | 2 | 3\n",
want: "Markdown | Less | Pretty\n" +
"--- | --- | ---\n" +
"Still | renders | nicely\n" +
"1 | 2 | 3",
},
{
name: "table:",
args: "| Tables | Are | Cool |\n" +
"| ------------- |:-------------:| -----:|\n" +
"| col 3 is | right-aligned | $1600 |\n" +
"| col 2 is | centered | $12 |\n" +
"| zebra stripes | are neat | $1 |\n",
want: "| Tables | Are | Cool |\n" +
"| ------------- |:-------------:| -----:|\n" +
"| col 3 is | right-aligned | $1600 |\n" +
"| col 2 is | centered | $12 |\n" +
"| zebra stripes | are neat | $1 |",
},
{
name: "strong: Bold with **asterisks** or __underscores__.",
args: "Bold with **asterisks** or __underscores__.",
want: "Bold with asterisks or underscores.",
},
{
name: "strong & em: Bold and italics with **asterisks and _underscores_**.",
args: "Bold and italics with **asterisks and _underscores_**.",
want: "Bold and italics with asterisks and underscores.",
},
{
name: "em: Italics with *asterisks* or _underscores_.",
args: "Italics with *asterisks* or _underscores_.",
want: "Italics with asterisks or underscores.",
},
{
name: "del: Strikethrough ~~strike this.~~",
args: "Strikethrough ~~strike this.~~",
want: "Strikethrough strike this.",
},
{
name: "links: [inline-style link](http://localhost:8065)",
args: "[inline-style link](http://localhost:8065)",
want: "inline-style link",
},
{
name: "image: ![image link](http://localhost:8065/image)",
args: "![image link](http://localhost:8065/image)",
want: "image link",
},
{
name: "text: plain",
args: "This is plain text.",
want: "This is plain text.",
},
{
name: "text: multiline",
args: "This is multiline text.\nHere is the next line.\n",
want: "This is multiline text.\nHere is the next line.",
},
{
name: "text: multiline with blockquote",
args: "This is multiline text.\n> With quote",
want: "This is multiline text. With quote",
},
{
name: "text: multiline with list items",
args: "This is multiline text.\n * List item ",
want: "This is multiline text. List item",
},
{
name: "text: &amp; entity",
args: "you & me",
want: "you & me",
},
{
name: "text: &lt; entity",
args: "1<2",
want: "1<2",
},
{
name: "text: &gt; entity",
args: "2>1",
want: "2>1",
},
{
name: "text: &#39; entity",
args: "he's out",
want: "he's out",
},
{
name: "text: &quot; entity",
args: `That is "unique"`,
want: `That is "unique"`,
},
{
name: "text: multiple entities",
args: "&<>'",
want: "&<>'",
},
{
name: "text: multiple entities",
args: "'><&",
want: "'><&",
},
{
name: "text: empty string",
args: "",
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := StripMarkdown(tt.args)
if err != nil {
t.Fatalf("error: %v", err)
}
assert.Equal(t, tt.want, got)
})
}
}
func TestMarkdownToHTML(t *testing.T) {
siteURL := "https://example.com"
tests := []struct {
name string
markdown string
want string
}{
{
name: "absolute url not changed",
markdown: "[Link](https://example.com)",
want: "<p><a href=\"https://example.com\">Link</a></p>\n",
},
{
name: "relative url changed to absolute url",
markdown: "[Link](/foo)",
want: "<p><a href=\"https://example.com/foo\">Link</a></p>\n",
},
{
name: "relative url with query params changed to absolute url",
markdown: "[Link](/foo?bar=true)",
want: "<p><a href=\"https://example.com/foo?bar=true\">Link</a></p>\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := MarkdownToHTML(tt.markdown, siteURL)
if err != nil {
t.Fatalf("error: %v", err)
}
assert.Equal(t, tt.want, got)
})
}
}

175
server/channels/utils/merge.go Обычный файл
Просмотреть файл

@@ -0,0 +1,175 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"fmt"
"reflect"
)
// StructFieldFilter defines a callback function used to decide if a patch value should be applied.
type StructFieldFilter func(structField reflect.StructField, base reflect.Value, patch reflect.Value) bool
// MergeConfig allows for optional merge customizations.
type MergeConfig struct {
StructFieldFilter StructFieldFilter
}
// Merge will return a new value of the same type as base and patch, recursively merging non-nil values from patch on top of base.
//
// Restrictions/guarantees:
// - base and patch must be the same type
// - base and patch will never be modified
// - values from patch are always selected when non-nil
// - structs are merged recursively
// - maps and slices are treated as pointers, and merged as a single value
//
// Note that callers need to cast the returned interface back into the original type:
//
// func mergeTestStruct(base, patch *testStruct) (*testStruct, error) {
// ret, err := merge(base, patch)
// if err != nil {
// return nil, err
// }
//
// retTS := ret.(testStruct)
// return &retTS, nil
// }
func Merge(base any, patch any, mergeConfig *MergeConfig) (any, error) {
if reflect.TypeOf(base) != reflect.TypeOf(patch) {
return nil, fmt.Errorf(
"cannot merge different types. base type: %s, patch type: %s",
reflect.TypeOf(base),
reflect.TypeOf(patch),
)
}
commonType := reflect.TypeOf(base)
baseVal := reflect.ValueOf(base)
patchVal := reflect.ValueOf(patch)
if commonType.Kind() == reflect.Ptr {
commonType = commonType.Elem()
baseVal = baseVal.Elem()
patchVal = patchVal.Elem()
}
ret := reflect.New(commonType)
val, ok := merge(baseVal, patchVal, mergeConfig)
if ok {
ret.Elem().Set(val)
}
return ret.Elem().Interface(), nil
}
// merge recursively merges patch into base and returns the new struct, ptr, slice/map, or value
func merge(base, patch reflect.Value, mergeConfig *MergeConfig) (reflect.Value, bool) {
commonType := base.Type()
switch commonType.Kind() {
case reflect.Struct:
merged := reflect.New(commonType).Elem()
for i := 0; i < base.NumField(); i++ {
if !merged.Field(i).CanSet() {
continue
}
if mergeConfig != nil && mergeConfig.StructFieldFilter != nil {
if !mergeConfig.StructFieldFilter(commonType.Field(i), base.Field(i), patch.Field(i)) {
merged.Field(i).Set(base.Field(i))
continue
}
}
val, ok := merge(base.Field(i), patch.Field(i), mergeConfig)
if ok {
merged.Field(i).Set(val)
}
}
return merged, true
case reflect.Ptr:
mergedPtr := reflect.New(commonType.Elem())
if base.IsNil() && patch.IsNil() {
return mergedPtr, false
}
// clone reference values (if any)
if base.IsNil() {
val, _ := merge(patch.Elem(), patch.Elem(), mergeConfig)
mergedPtr.Elem().Set(val)
} else if patch.IsNil() {
val, _ := merge(base.Elem(), base.Elem(), mergeConfig)
mergedPtr.Elem().Set(val)
} else {
val, _ := merge(base.Elem(), patch.Elem(), mergeConfig)
mergedPtr.Elem().Set(val)
}
return mergedPtr, true
case reflect.Slice:
if base.IsNil() && patch.IsNil() {
return reflect.Zero(commonType), false
}
if !patch.IsNil() {
// use patch
merged := reflect.MakeSlice(commonType, 0, patch.Len())
for i := 0; i < patch.Len(); i++ {
// recursively merge patch with itself. This will clone reference values.
val, _ := merge(patch.Index(i), patch.Index(i), mergeConfig)
merged = reflect.Append(merged, val)
}
return merged, true
}
// use base
merged := reflect.MakeSlice(commonType, 0, base.Len())
for i := 0; i < base.Len(); i++ {
// recursively merge base with itself. This will clone reference values.
val, _ := merge(base.Index(i), base.Index(i), mergeConfig)
merged = reflect.Append(merged, val)
}
return merged, true
case reflect.Map:
// maps are merged according to these rules:
// - if patch is not nil, replace the base map completely
// - otherwise, keep the base map
// - reference values (eg. slice/ptr/map) will be cloned
if base.IsNil() && patch.IsNil() {
return reflect.Zero(commonType), false
}
merged := reflect.MakeMap(commonType)
mapPtr := base
if !patch.IsNil() {
mapPtr = patch
}
for _, key := range mapPtr.MapKeys() {
// clone reference values
val, ok := merge(mapPtr.MapIndex(key), mapPtr.MapIndex(key), mergeConfig)
if !ok {
val = reflect.New(mapPtr.MapIndex(key).Type()).Elem()
}
merged.SetMapIndex(key, val)
}
return merged, true
case reflect.Interface:
var val reflect.Value
if base.IsNil() && patch.IsNil() {
return reflect.Zero(commonType), false
}
// clone reference values (if any)
if base.IsNil() {
val, _ = merge(patch.Elem(), patch.Elem(), mergeConfig)
} else if patch.IsNil() {
val, _ = merge(base.Elem(), base.Elem(), mergeConfig)
} else {
val, _ = merge(base.Elem(), patch.Elem(), mergeConfig)
}
return val, true
default:
return patch, true
}
}

1695
server/channels/utils/merge_test.go Обычный файл

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@@ -0,0 +1,61 @@
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make misc-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// LicenseValidatorIface is an autogenerated mock type for the LicenseValidatorIface type
type LicenseValidatorIface struct {
mock.Mock
}
// LicenseFromBytes provides a mock function with given fields: licenseBytes
func (_m *LicenseValidatorIface) LicenseFromBytes(licenseBytes []byte) (*model.License, *model.AppError) {
ret := _m.Called(licenseBytes)
var r0 *model.License
if rf, ok := ret.Get(0).(func([]byte) *model.License); ok {
r0 = rf(licenseBytes)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.License)
}
}
var r1 *model.AppError
if rf, ok := ret.Get(1).(func([]byte) *model.AppError); ok {
r1 = rf(licenseBytes)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// ValidateLicense provides a mock function with given fields: signed
func (_m *LicenseValidatorIface) ValidateLicense(signed []byte) (bool, string) {
ret := _m.Called(signed)
var r0 bool
if rf, ok := ret.Get(0).(func([]byte) bool); ok {
r0 = rf(signed)
} else {
r0 = ret.Get(0).(bool)
}
var r1 string
if rf, ok := ret.Get(1).(func([]byte) string); ok {
r1 = rf(signed)
} else {
r1 = ret.Get(1).(string)
}
return r0, r1
}

Просмотреть файл

@@ -0,0 +1,542 @@
{
"restrictTeamInvite": {
"all": [
{
"roleName": "team_user",
"permission": "invite_user",
"shouldHave": true
}
],
"team_admin": [
{
"roleName": "team_user",
"permission": "invite_user",
"shouldHave": false
},
{
"roleName": "team_admin",
"permission": "invite_user",
"shouldHave": true
}
],
"system_admin": [
{
"roleName": "team_user",
"permission": "invite_user",
"shouldHave": false
},
{
"roleName": "team_admin",
"permission": "invite_user",
"shouldHave": false
}
]
},
"restrictPublicChannelCreation": {
"all": [
{
"roleName": "team_user",
"permission": "create_public_channel",
"shouldHave": true
}
],
"team_admin": [
{
"roleName": "team_user",
"permission": "create_public_channel",
"shouldHave": false
},
{
"roleName": "team_admin",
"permission": "create_public_channel",
"shouldHave": true
}
],
"system_admin": [
{
"roleName": "team_user",
"permission": "create_public_channel",
"shouldHave": false
},
{
"roleName": "team_admin",
"permission": "create_public_channel",
"shouldHave": false
}
]
},
"restrictPrivateChannelCreation": {
"all": [
{
"roleName": "team_user",
"permission": "create_private_channel",
"shouldHave": true
}
],
"team_admin": [
{
"roleName": "team_user",
"permission": "create_private_channel",
"shouldHave": false
},
{
"roleName": "team_admin",
"permission": "create_private_channel",
"shouldHave": true
}
],
"system_admin": [
{
"roleName": "team_user",
"permission": "create_private_channel",
"shouldHave": false
},
{
"roleName": "team_admin",
"permission": "create_private_channel",
"shouldHave": false
}
]
},
"restrictPublicChannelManagement": {
"all": [
{
"roleName": "channel_user",
"permission": "manage_public_channel_properties",
"shouldHave": true
}
],
"channel_admin": [
{
"roleName": "channel_user",
"permission": "manage_public_channel_properties",
"shouldHave": false
},
{
"roleName": "channel_admin",
"permission": "manage_public_channel_properties",
"shouldHave": true
},
{
"roleName": "team_admin",
"permission": "manage_public_channel_properties",
"shouldHave": true
}
],
"team_admin": [
{
"roleName": "channel_user",
"permission": "manage_public_channel_properties",
"shouldHave": false
},
{
"roleName": "channel_admin",
"permission": "manage_public_channel_properties",
"shouldHave": false
},
{
"roleName": "team_admin",
"permission": "manage_public_channel_properties",
"shouldHave": true
}
],
"system_admin": [
{
"roleName": "channel_user",
"permission": "manage_public_channel_properties",
"shouldHave": false
},
{
"roleName": "channel_admin",
"permission": "manage_public_channel_properties",
"shouldHave": false
},
{
"roleName": "team_admin",
"permission": "manage_public_channel_properties",
"shouldHave": false
}
]
},
"restrictPublicChannelDeletion": {
"all": [
{
"roleName": "channel_user",
"permission": "delete_public_channel",
"shouldHave": true
}
],
"channel_admin": [
{
"roleName": "channel_user",
"permission": "delete_public_channel",
"shouldHave": false
},
{
"roleName": "channel_admin",
"permission": "delete_public_channel",
"shouldHave": true
},
{
"roleName": "team_admin",
"permission": "delete_public_channel",
"shouldHave": true
}
],
"team_admin": [
{
"roleName": "channel_user",
"permission": "delete_public_channel",
"shouldHave": false
},
{
"roleName": "channel_admin",
"permission": "delete_public_channel",
"shouldHave": false
},
{
"roleName": "team_admin",
"permission": "delete_public_channel",
"shouldHave": true
}
],
"system_admin": [
{
"roleName": "channel_user",
"permission": "delete_public_channel",
"shouldHave": false
},
{
"roleName": "channel_admin",
"permission": "delete_public_channel",
"shouldHave": false
},
{
"roleName": "team_admin",
"permission": "delete_public_channel",
"shouldHave": false
}
]
},
"restrictPrivateChannelManagement": {
"all": [
{
"roleName": "channel_user",
"permission": "manage_private_channel_properties",
"shouldHave": true
}
],
"channel_admin": [
{
"roleName": "channel_user",
"permission": "manage_private_channel_properties",
"shouldHave": false
},
{
"roleName": "channel_admin",
"permission": "manage_private_channel_properties",
"shouldHave": true
},
{
"roleName": "team_admin",
"permission": "manage_private_channel_properties",
"shouldHave": true
}
],
"team_admin": [
{
"roleName": "channel_user",
"permission": "manage_private_channel_properties",
"shouldHave": false
},
{
"roleName": "channel_admin",
"permission": "manage_private_channel_properties",
"shouldHave": false
},
{
"roleName": "team_admin",
"permission": "manage_private_channel_properties",
"shouldHave": true
}
],
"system_admin": [
{
"roleName": "channel_user",
"permission": "manage_private_channel_properties",
"shouldHave": false
},
{
"roleName": "channel_admin",
"permission": "manage_private_channel_properties",
"shouldHave": false
},
{
"roleName": "team_admin",
"permission": "manage_private_channel_properties",
"shouldHave": false
}
]
},
"restrictPrivateChannelManageMembers": {
"all": [
{
"roleName": "channel_user",
"permission": "manage_private_channel_members",
"shouldHave": true
}
],
"channel_admin": [
{
"roleName": "channel_user",
"permission": "manage_private_channel_members",
"shouldHave": false
},
{
"roleName": "channel_admin",
"permission": "manage_private_channel_members",
"shouldHave": true
},
{
"roleName": "team_admin",
"permission": "manage_private_channel_members",
"shouldHave": true
}
],
"team_admin": [
{
"roleName": "channel_user",
"permission": "manage_private_channel_members",
"shouldHave": false
},
{
"roleName": "channel_admin",
"permission": "manage_private_channel_members",
"shouldHave": false
},
{
"roleName": "team_admin",
"permission": "manage_private_channel_members",
"shouldHave": true
}
],
"system_admin": [
{
"roleName": "channel_user",
"permission": "manage_private_channel_members",
"shouldHave": false
},
{
"roleName": "channel_admin",
"permission": "manage_private_channel_members",
"shouldHave": false
},
{
"roleName": "team_admin",
"permission": "manage_private_channel_members",
"shouldHave": false
}
]
},
"restrictPrivateChannelDeletion": {
"all": [
{
"roleName": "channel_user",
"permission": "delete_private_channel",
"shouldHave": true
}
],
"channel_admin": [
{
"roleName": "channel_user",
"permission": "delete_private_channel",
"shouldHave": false
},
{
"roleName": "channel_admin",
"permission": "delete_private_channel",
"shouldHave": true
},
{
"roleName": "team_admin",
"permission": "delete_private_channel",
"shouldHave": true
}
],
"team_admin": [
{
"roleName": "channel_user",
"permission": "delete_private_channel",
"shouldHave": false
},
{
"roleName": "channel_admin",
"permission": "delete_private_channel",
"shouldHave": false
},
{
"roleName": "team_admin",
"permission": "delete_private_channel",
"shouldHave": true
}
],
"system_admin": [
{
"roleName": "channel_user",
"permission": "delete_private_channel",
"shouldHave": false
},
{
"roleName": "channel_admin",
"permission": "delete_private_channel",
"shouldHave": false
},
{
"roleName": "team_admin",
"permission": "delete_private_channel",
"shouldHave": false
}
]
},
"allowEditPost": {
"always": [
{
"roleName": "channel_user",
"permission": "edit_post",
"shouldHave": true
},
{
"roleName": "system_admin",
"permission": "edit_post",
"shouldHave": true
}
],
"never": [
{
"roleName": "channel_user",
"permission": "edit_post",
"shouldHave": false
},
{
"roleName": "system_admin",
"permission": "edit_post",
"shouldHave": true
}
]
},
"restrictPostDelete": {
"all": [
{
"roleName": "channel_user",
"permission": "delete_post",
"shouldHave": true
},
{
"roleName": "team_admin",
"permission": "delete_post",
"shouldHave": true
},
{
"roleName": "team_admin",
"permission": "delete_others_posts",
"shouldHave": true
}
],
"team_admin": [
{
"roleName": "channel_user",
"permission": "delete_post",
"shouldHave": false
},
{
"roleName": "team_admin",
"permission": "delete_post",
"shouldHave": true
},
{
"roleName": "team_admin",
"permission": "delete_others_posts",
"shouldHave": true
}
],
"system_admin": [
{
"roleName": "channel_user",
"permission": "delete_post",
"shouldHave": false
},
{
"roleName": "team_admin",
"permission": "delete_post",
"shouldHave": false
},
{
"roleName": "team_admin",
"permission": "delete_others_posts",
"shouldHave": false
}
]
},
"enableTeamCreation": {
"true": [
{
"roleName": "system_user",
"permission": "create_team",
"shouldHave": true
}
],
"false": [
{
"roleName": "system_user",
"permission": "create_team",
"shouldHave": false
}
]
},
"enableOnlyAdminIntegrations": {
"true": [
{
"roleName": "team_user",
"permission": "manage_incoming_webhooks",
"shouldHave": false
},
{
"roleName": "team_user",
"permission": "manage_outgoing_webhooks",
"shouldHave": false
},
{
"roleName": "team_user",
"permission": "manage_slash_commands",
"shouldHave": false
},
{
"roleName": "system_user",
"permission": "manage_oauth",
"shouldHave": false
}
],
"false": [
{
"roleName": "team_user",
"permission": "manage_incoming_webhooks",
"shouldHave": true
},
{
"roleName": "team_user",
"permission": "manage_outgoing_webhooks",
"shouldHave": true
},
{
"roleName": "team_user",
"permission": "manage_slash_commands",
"shouldHave": true
},
{
"roleName": "system_user",
"permission": "manage_oauth",
"shouldHave": true
}
]
}
}

20
server/channels/utils/random.go Обычный файл
Просмотреть файл

@@ -0,0 +1,20 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"math/rand"
)
type Range struct {
Begin int
End int
}
func RandIntFromRange(r Range) int {
if r.End-r.Begin <= 0 {
return r.Begin
}
return rand.Intn((r.End-r.Begin)+1) + r.Begin
}

187
server/channels/utils/subpath.go Обычный файл
Просмотреть файл

@@ -0,0 +1,187 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"crypto/sha256"
"encoding/base64"
"fmt"
"net/url"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/fileutils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// getSubpathScript renders the inline script that defines window.publicPath to change how webpack loads assets.
func getSubpathScript(subpath string) string {
if subpath == "" {
subpath = "/"
}
newPath := path.Join(subpath, "static") + "/"
return fmt.Sprintf("window.publicPath='%s'", newPath)
}
// GetSubpathScriptHash computes the script-src addition required for the subpath script to bypass CSP protections.
func GetSubpathScriptHash(subpath string) string {
// No hash is required for the default subpath.
if subpath == "" || subpath == "/" {
return ""
}
scriptHash := sha256.Sum256([]byte(getSubpathScript(subpath)))
return fmt.Sprintf(" 'sha256-%s'", base64.StdEncoding.EncodeToString(scriptHash[:]))
}
// UpdateAssetsSubpathInDir rewrites assets in the given directory to assume the application is
// hosted at the given subpath instead of at the root. No changes are written unless necessary.
func UpdateAssetsSubpathInDir(subpath, directory string) error {
if subpath == "" {
subpath = "/"
}
staticDir, found := fileutils.FindDir(directory)
if !found {
return errors.New("failed to find client dir")
}
staticDir, err := filepath.EvalSymlinks(staticDir)
if err != nil {
return errors.Wrapf(err, "failed to resolve symlinks to %s", staticDir)
}
rootHTMLPath := filepath.Join(staticDir, "root.html")
oldRootHTML, err := os.ReadFile(rootHTMLPath)
if err != nil {
return errors.Wrap(err, "failed to open root.html")
}
oldSubpath := "/"
// Determine if a previous subpath had already been rewritten into the assets.
reWebpackPublicPathScript := regexp.MustCompile("window.publicPath='([^']+/)static/'")
alreadyRewritten := false
if matches := reWebpackPublicPathScript.FindStringSubmatch(string(oldRootHTML)); matches != nil {
oldSubpath = matches[1]
alreadyRewritten = true
}
pathToReplace := path.Join(oldSubpath, "static") + "/"
newPath := path.Join(subpath, "static") + "/"
mlog.Debug("Rewriting static assets", mlog.String("from_subpath", oldSubpath), mlog.String("to_subpath", subpath))
newRootHTML := string(oldRootHTML)
reCSP := regexp.MustCompile(`<meta http-equiv="Content-Security-Policy" content="script-src 'self' cdn.rudderlabs.com/ js.stripe.com/v3([^"]*)">`)
if results := reCSP.FindAllString(newRootHTML, -1); len(results) == 0 {
return fmt.Errorf("failed to find 'Content-Security-Policy' meta tag to rewrite")
}
newRootHTML = reCSP.ReplaceAllLiteralString(newRootHTML, fmt.Sprintf(
`<meta http-equiv="Content-Security-Policy" content="script-src 'self' cdn.rudderlabs.com/ js.stripe.com/v3%s">`,
GetSubpathScriptHash(subpath),
))
// Rewrite the root.html references to `/static/*` to include the given subpath.
// This potentially includes a previously injected inline script that needs to
// be updated (and isn't covered by the cases above).
newRootHTML = strings.Replace(newRootHTML, pathToReplace, newPath, -1)
if alreadyRewritten && subpath == "/" {
// Remove the injected script since no longer required. Note that the rewrite above
// will have affected the script, so look for the new subpath, not the old one.
oldScript := getSubpathScript(subpath)
newRootHTML = strings.Replace(newRootHTML, fmt.Sprintf("</style><script>%s</script>", oldScript), "</style>", 1)
} else if !alreadyRewritten && subpath != "/" {
// Otherwise, inject the script to define `window.publicPath`.
script := getSubpathScript(subpath)
newRootHTML = strings.Replace(newRootHTML, "</style>", fmt.Sprintf("</style><script>%s</script>", script), 1)
}
// Write out the updated root.html.
if err = os.WriteFile(rootHTMLPath, []byte(newRootHTML), 0); err != nil {
return errors.Wrapf(err, "failed to update root.html with subpath %s", subpath)
}
// Rewrite the manifest.json and *.css references to `/static/*` (or a previously rewritten subpath).
err = filepath.Walk(staticDir, func(walkPath string, info os.FileInfo, err error) error {
if filepath.Base(walkPath) == "manifest.json" || filepath.Ext(walkPath) == ".css" {
old, err := os.ReadFile(walkPath)
if err != nil {
return errors.Wrapf(err, "failed to open %s", walkPath)
}
new := strings.Replace(string(old), pathToReplace, newPath, -1)
if err = os.WriteFile(walkPath, []byte(new), 0); err != nil {
return errors.Wrapf(err, "failed to update %s with subpath %s", walkPath, subpath)
}
}
return nil
})
if err != nil {
return errors.Wrapf(err, "error walking %s", staticDir)
}
return nil
}
// UpdateAssetsSubpath rewrites assets in the /client directory to assume the application is hosted
// at the given subpath instead of at the root. No changes are written unless necessary.
func UpdateAssetsSubpath(subpath string) error {
return UpdateAssetsSubpathInDir(subpath, model.ClientDir)
}
// UpdateAssetsSubpathFromConfig uses UpdateAssetsSubpath and any path defined in the SiteURL.
func UpdateAssetsSubpathFromConfig(config *model.Config) error {
// Don't rewrite in development environments, since webpack in developer mode constantly
// updates the assets and must be configured separately.
if model.BuildNumber == "dev" {
mlog.Debug("Skipping update to assets subpath since dev build")
return nil
}
// Similarly, don't rewrite during a CI build, when the assets may not even be present.
if os.Getenv("IS_CI") == "true" {
mlog.Debug("Skipping update to assets subpath since CI build")
return nil
}
subpath, err := GetSubpathFromConfig(config)
if err != nil {
return err
}
return UpdateAssetsSubpath(subpath)
}
func GetSubpathFromConfig(config *model.Config) (string, error) {
if config == nil {
return "", errors.New("no config provided")
} else if config.ServiceSettings.SiteURL == nil {
return "/", nil
}
u, err := url.Parse(*config.ServiceSettings.SiteURL)
if err != nil {
return "", errors.Wrap(err, "failed to parse SiteURL from config")
}
if u.Path == "" {
return "/", nil
}
return path.Clean(u.Path), nil
}

488
server/channels/utils/subpath_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,488 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils_test
import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
func TestUpdateAssetsSubpathFromConfig(t *testing.T) {
t.Run("dev build", func(t *testing.T) {
var oldBuildNumber = model.BuildNumber
model.BuildNumber = "dev"
defer func() {
model.BuildNumber = oldBuildNumber
}()
err := utils.UpdateAssetsSubpathFromConfig(nil)
require.NoError(t, err)
})
t.Run("IS_CI=true", func(t *testing.T) {
err := os.Setenv("IS_CI", "true")
require.NoError(t, err)
defer func() {
os.Unsetenv("IS_CI")
}()
err = utils.UpdateAssetsSubpathFromConfig(nil)
require.NoError(t, err)
})
t.Run("no config", func(t *testing.T) {
tempDir, err := os.MkdirTemp("", "test_update_assets_subpath")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
os.Chdir(tempDir)
err = utils.UpdateAssetsSubpathFromConfig(nil)
require.Error(t, err)
})
}
func TestUpdateAssetsSubpath(t *testing.T) {
t.Run("no client dir", func(t *testing.T) {
tempDir, err := os.MkdirTemp("", "test_update_assets_subpath")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
os.Chdir(tempDir)
err = utils.UpdateAssetsSubpath("/")
require.Error(t, err)
})
t.Run("valid", func(t *testing.T) {
tempDir, err := os.MkdirTemp("", "test_update_assets_subpath")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
os.Chdir(tempDir)
err = os.Mkdir(model.ClientDir, 0700)
require.NoError(t, err)
testCases := []struct {
Description string
RootHTML string
MainCSS string
ManifestJSON string
Subpath string
ExpectedError error
ExpectedRootHTML string
ExpectedMainCSS string
ExpectedManifestJSON string
}{
{
"no changes required, empty subpath provided",
baseRootHTML,
baseCSS,
baseManifestJSON,
"",
nil,
baseRootHTML,
baseCSS,
baseManifestJSON,
},
{
"no changes required",
baseRootHTML,
baseCSS,
baseManifestJSON,
"/",
nil,
baseRootHTML,
baseCSS,
baseManifestJSON,
},
{
"content security policy not found (missing quotes)",
contentSecurityPolicyNotFoundHTML,
baseCSS,
baseManifestJSON,
"/subpath",
fmt.Errorf("failed to find 'Content-Security-Policy' meta tag to rewrite"),
contentSecurityPolicyNotFoundHTML,
baseCSS,
baseManifestJSON,
},
{
"content security policy not found (missing unsafe-eval)",
contentSecurityPolicyNotFound2HTML,
baseCSS,
baseManifestJSON,
"/subpath",
fmt.Errorf("failed to find 'Content-Security-Policy' meta tag to rewrite"),
contentSecurityPolicyNotFound2HTML,
baseCSS,
baseManifestJSON,
},
{
"subpath",
baseRootHTML,
baseCSS,
baseManifestJSON,
"/subpath",
nil,
subpathRootHTML,
subpathCSS,
subpathManifestJSON,
},
{
"new subpath from old",
subpathRootHTML,
subpathCSS,
subpathManifestJSON,
"/nested/subpath",
nil,
newSubpathRootHTML,
newSubpathCSS,
newSubpathManifestJSON,
},
{
"resetting to /",
subpathRootHTML,
subpathCSS,
baseManifestJSON,
"/",
nil,
baseRootHTML,
baseCSS,
baseManifestJSON,
},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
os.WriteFile(filepath.Join(tempDir, model.ClientDir, "root.html"), []byte(testCase.RootHTML), 0700)
os.WriteFile(filepath.Join(tempDir, model.ClientDir, "main.css"), []byte(testCase.MainCSS), 0700)
os.WriteFile(filepath.Join(tempDir, model.ClientDir, "manifest.json"), []byte(testCase.ManifestJSON), 0700)
err := utils.UpdateAssetsSubpath(testCase.Subpath)
if testCase.ExpectedError != nil {
require.Equal(t, testCase.ExpectedError, err)
} else {
require.NoError(t, err)
}
contents, err := os.ReadFile(filepath.Join(tempDir, model.ClientDir, "root.html"))
require.NoError(t, err)
// Rewrite the expected and contents for simpler diffs when failed.
expectedRootHTML := strings.Replace(testCase.ExpectedRootHTML, ">", ">\n", -1)
contentsStr := strings.Replace(string(contents), ">", ">\n", -1)
require.Equal(t, expectedRootHTML, contentsStr)
contents, err = os.ReadFile(filepath.Join(tempDir, model.ClientDir, "main.css"))
require.NoError(t, err)
require.Equal(t, testCase.ExpectedMainCSS, string(contents))
contents, err = os.ReadFile(filepath.Join(tempDir, model.ClientDir, "manifest.json"))
require.NoError(t, err)
require.Equal(t, testCase.ExpectedManifestJSON, string(contents))
})
}
})
}
func TestGetSubpathFromConfig(t *testing.T) {
testCases := []struct {
Description string
SiteURL *string
ExpectedError bool
ExpectedSubpath string
}{
{
"empty SiteURL",
sToP(""),
false,
"/",
},
{
"invalid SiteURL",
sToP("cache_object:foo/bar"),
true,
"",
},
{
"nil SiteURL",
nil,
false,
"/",
},
{
"no trailing slash",
sToP("http://localhost:8065"),
false,
"/",
},
{
"trailing slash",
sToP("http://localhost:8065/"),
false,
"/",
},
{
"subpath, no trailing slash",
sToP("http://localhost:8065/subpath"),
false,
"/subpath",
},
{
"trailing slash",
sToP("http://localhost:8065/subpath/"),
false,
"/subpath",
},
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
config := &model.Config{
ServiceSettings: model.ServiceSettings{
SiteURL: testCase.SiteURL,
},
}
subpath, err := utils.GetSubpathFromConfig(config)
if testCase.ExpectedError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
require.Equal(t, testCase.ExpectedSubpath, subpath)
})
}
}
func sToP(s string) *string {
return &s
}
const contentSecurityPolicyNotFoundHTML = `<!DOCTYPE html> <html lang=en> <head> <meta charset=utf-8> <meta http-equiv=Content-Security-Policy content="script-src 'self' cdn.rudderlabs.com/ js.stripe.com/v3"> <meta http-equiv=X-UA-Compatible content="IE=edge"> <meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0"> <meta name=robots content="noindex, nofollow"> <meta name=referrer content=no-referrer> <title>Mattermost</title> <meta name=apple-mobile-web-app-capable content=yes> <meta name=apple-mobile-web-app-status-bar-style content=default> <meta name=mobile-web-app-capable content=yes> <meta name=apple-mobile-web-app-title content=Mattermost> <meta name=application-name content=Mattermost> <meta name=format-detection content="telephone=no"> <link rel=apple-touch-icon sizes=57x57 href=/static/files/78b7e73b41b8731ce2c41c870ecc8886.png> <link rel=apple-touch-icon sizes=60x60 href=/static/files/51d00ffd13afb6d74fd8f6dfdeef768a.png> <link rel=apple-touch-icon sizes=72x72 href=/static/files/23645596f8f78f017bd4d457abb855c4.png> <link rel=apple-touch-icon sizes=76x76 href=/static/files/26e9d72f472663a00b4b206149459fab.png> <link rel=apple-touch-icon sizes=144x144 href=/static/files/7bd91659bf3fc8c68fcd45fc1db9c630.png> <link rel=apple-touch-icon sizes=120x120 href=/static/files/fa69ffe11eb334aaef5aece8d848ca62.png> <link rel=apple-touch-icon sizes=152x152 href=/static/files/f046777feb6ab12fc43b8f9908b1db35.png> <link rel=icon type=image/png sizes=16x16 href=/static/files/02b96247d275680adaaabf01c71c571d.png> <link rel=icon type=image/png sizes=32x32 href=/static/files/1d9020f201a6762421cab8d30624fdd8.png> <link rel=icon type=image/png sizes=96x96 href=/static/files/fe23af39ae98d77dc26ae8586565970f.png> <link rel=icon type=image/png sizes=192x192 href=/static/files/d7ff68a7675f84337cc154c3d4abe713.png> <link rel=manifest href=/static/files/a985ad72552ad069537d6eea81e719c7.json> <link rel=stylesheet class=code_theme> <style>.error-screen{font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding-top:50px;max-width:750px;font-size:14px;color:#333;margin:auto;display:none;line-height:1.5}.error-screen h2{font-size:30px;font-weight:400;line-height:1.2}.error-screen ul{padding-left:15px;line-height:1.7;margin-top:0;margin-bottom:10px}.error-screen hr{color:#ddd;margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.error-screen-visible{display:block}</style> <link href="/static/main.364fd054d7a6d741efc6.css" rel="stylesheet"><script type="text/javascript" src="/static/main.e49599ac425584ffead5.js"></script></head> <body class=font--open_sans> <div id=root> <div class=error-screen> <h2>Cannot connect to Mattermost</h2> <hr/> <p>We're having trouble connecting to Mattermost. If refreshing this page (Ctrl+R or Command+R) does not work, please verify that your computer is connected to the internet.</p> <br/> </div> <div class=loading-screen style=position:relative> <div class=loading__content> <div class="round round-1"></div> <div class="round round-2"></div> <div class="round round-3"></div> </div> </div> </div> <noscript> To use Mattermost, please enable JavaScript. </noscript> </body> </html>`
const contentSecurityPolicyNotFound2HTML = `<!DOCTYPE html> <html lang=en> <head> <meta charset=utf-8> <meta http-equiv=Content-Security-Policy content="script-src 'self' cdn.rudderlabs.com/ js.stripe.com/v3 'unsafe-eval'"> <meta http-equiv=X-UA-Compatible content="IE=edge"> <meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0"> <meta name=robots content="noindex, nofollow"> <meta name=referrer content=no-referrer> <title>Mattermost</title> <meta name=apple-mobile-web-app-capable content=yes> <meta name=apple-mobile-web-app-status-bar-style content=default> <meta name=mobile-web-app-capable content=yes> <meta name=apple-mobile-web-app-title content=Mattermost> <meta name=application-name content=Mattermost> <meta name=format-detection content="telephone=no"> <link rel=apple-touch-icon sizes=57x57 href=/static/files/78b7e73b41b8731ce2c41c870ecc8886.png> <link rel=apple-touch-icon sizes=60x60 href=/static/files/51d00ffd13afb6d74fd8f6dfdeef768a.png> <link rel=apple-touch-icon sizes=72x72 href=/static/files/23645596f8f78f017bd4d457abb855c4.png> <link rel=apple-touch-icon sizes=76x76 href=/static/files/26e9d72f472663a00b4b206149459fab.png> <link rel=apple-touch-icon sizes=144x144 href=/static/files/7bd91659bf3fc8c68fcd45fc1db9c630.png> <link rel=apple-touch-icon sizes=120x120 href=/static/files/fa69ffe11eb334aaef5aece8d848ca62.png> <link rel=apple-touch-icon sizes=152x152 href=/static/files/f046777feb6ab12fc43b8f9908b1db35.png> <link rel=icon type=image/png sizes=16x16 href=/static/files/02b96247d275680adaaabf01c71c571d.png> <link rel=icon type=image/png sizes=32x32 href=/static/files/1d9020f201a6762421cab8d30624fdd8.png> <link rel=icon type=image/png sizes=96x96 href=/static/files/fe23af39ae98d77dc26ae8586565970f.png> <link rel=icon type=image/png sizes=192x192 href=/static/files/d7ff68a7675f84337cc154c3d4abe713.png> <link rel=manifest href=/static/files/a985ad72552ad069537d6eea81e719c7.json> <link rel=stylesheet class=code_theme> <style>.error-screen{font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding-top:50px;max-width:750px;font-size:14px;color:#333;margin:auto;display:none;line-height:1.5}.error-screen h2{font-size:30px;font-weight:400;line-height:1.2}.error-screen ul{padding-left:15px;line-height:1.7;margin-top:0;margin-bottom:10px}.error-screen hr{color:#ddd;margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.error-screen-visible{display:block}</style> <link href="/static/main.364fd054d7a6d741efc6.css" rel="stylesheet"><script type="text/javascript" src="/static/main.e49599ac425584ffead5.js"></script></head> <body class=font--open_sans> <div id=root> <div class=error-screen> <h2>Cannot connect to Mattermost</h2> <hr/> <p>We're having trouble connecting to Mattermost. If refreshing this page (Ctrl+R or Command+R) does not work, please verify that your computer is connected to the internet.</p> <br/> </div> <div class=loading-screen style=position:relative> <div class=loading__content> <div class="round round-1"></div> <div class="round round-2"></div> <div class="round round-3"></div> </div> </div> </div> <noscript> To use Mattermost, please enable JavaScript. </noscript> </body> </html>`
const baseRootHTML = `<!DOCTYPE html> <html lang=en> <head> <meta charset=utf-8> <meta http-equiv="Content-Security-Policy" content="script-src 'self' cdn.rudderlabs.com/ js.stripe.com/v3"> <meta http-equiv=X-UA-Compatible content="IE=edge"> <meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0"> <meta name=robots content="noindex, nofollow"> <meta name=referrer content=no-referrer> <title>Mattermost</title> <meta name=apple-mobile-web-app-capable content=yes> <meta name=apple-mobile-web-app-status-bar-style content=default> <meta name=mobile-web-app-capable content=yes> <meta name=apple-mobile-web-app-title content=Mattermost> <meta name=application-name content=Mattermost> <meta name=format-detection content="telephone=no"> <link rel=apple-touch-icon sizes=57x57 href=/static/files/78b7e73b41b8731ce2c41c870ecc8886.png> <link rel=apple-touch-icon sizes=60x60 href=/static/files/51d00ffd13afb6d74fd8f6dfdeef768a.png> <link rel=apple-touch-icon sizes=72x72 href=/static/files/23645596f8f78f017bd4d457abb855c4.png> <link rel=apple-touch-icon sizes=76x76 href=/static/files/26e9d72f472663a00b4b206149459fab.png> <link rel=apple-touch-icon sizes=144x144 href=/static/files/7bd91659bf3fc8c68fcd45fc1db9c630.png> <link rel=apple-touch-icon sizes=120x120 href=/static/files/fa69ffe11eb334aaef5aece8d848ca62.png> <link rel=apple-touch-icon sizes=152x152 href=/static/files/f046777feb6ab12fc43b8f9908b1db35.png> <link rel=icon type=image/png sizes=16x16 href=/static/files/02b96247d275680adaaabf01c71c571d.png> <link rel=icon type=image/png sizes=32x32 href=/static/files/1d9020f201a6762421cab8d30624fdd8.png> <link rel=icon type=image/png sizes=96x96 href=/static/files/fe23af39ae98d77dc26ae8586565970f.png> <link rel=icon type=image/png sizes=192x192 href=/static/files/d7ff68a7675f84337cc154c3d4abe713.png> <link rel=manifest href=/static/files/a985ad72552ad069537d6eea81e719c7.json> <link rel=stylesheet class=code_theme> <style>.error-screen{font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding-top:50px;max-width:750px;font-size:14px;color:#333;margin:auto;display:none;line-height:1.5}.error-screen h2{font-size:30px;font-weight:400;line-height:1.2}.error-screen ul{padding-left:15px;line-height:1.7;margin-top:0;margin-bottom:10px}.error-screen hr{color:#ddd;margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.error-screen-visible{display:block}</style> <link href="/static/main.364fd054d7a6d741efc6.css" rel="stylesheet"><script type="text/javascript" src="/static/main.e49599ac425584ffead5.js"></script></head> <body class=font--open_sans> <div id=root> <div class=error-screen> <h2>Cannot connect to Mattermost</h2> <hr/> <p>We're having trouble connecting to Mattermost. If refreshing this page (Ctrl+R or Command+R) does not work, please verify that your computer is connected to the internet.</p> <br/> </div> <div class=loading-screen style=position:relative> <div class=loading__content> <div class="round round-1"></div> <div class="round round-2"></div> <div class="round round-3"></div> </div> </div> </div> <noscript> To use Mattermost, please enable JavaScript. </noscript> </body> </html>`
const baseCSS = `@font-face{font-family:FontAwesome;src:url(/static/files/674f50d287a8c48dc19ba404d20fe713.eot);src:url(/static/files/674f50d287a8c48dc19ba404d20fe713.eot?#iefix&v=4.7.0) format("embedded-opentype"),url(/static/files/af7ae505a9eed503f8b8e6982036873e.woff2) format("woff2"),url(/static/files/fee66e712a8a08eef5805a46892932ad.woff) format("woff"),url(/static/files/b06871f281fee6b241d60582ae9369b9.ttf) format("truetype"),url(/static/files/677433a0892aaed7b7d2628c313c9775.svg#fontawesomeregular) format("svg");font-weight:400;font-style:normal}`
const subpathRootHTML = `<!DOCTYPE html> <html lang=en> <head> <meta charset=utf-8> <meta http-equiv="Content-Security-Policy" content="script-src 'self' cdn.rudderlabs.com/ js.stripe.com/v3 'sha256-tPOjw+tkVs9axL78ZwGtYl975dtyPHB6LYKAO2R3gR4='"> <meta http-equiv=X-UA-Compatible content="IE=edge"> <meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0"> <meta name=robots content="noindex, nofollow"> <meta name=referrer content=no-referrer> <title>Mattermost</title> <meta name=apple-mobile-web-app-capable content=yes> <meta name=apple-mobile-web-app-status-bar-style content=default> <meta name=mobile-web-app-capable content=yes> <meta name=apple-mobile-web-app-title content=Mattermost> <meta name=application-name content=Mattermost> <meta name=format-detection content="telephone=no"> <link rel=apple-touch-icon sizes=57x57 href=/subpath/static/files/78b7e73b41b8731ce2c41c870ecc8886.png> <link rel=apple-touch-icon sizes=60x60 href=/subpath/static/files/51d00ffd13afb6d74fd8f6dfdeef768a.png> <link rel=apple-touch-icon sizes=72x72 href=/subpath/static/files/23645596f8f78f017bd4d457abb855c4.png> <link rel=apple-touch-icon sizes=76x76 href=/subpath/static/files/26e9d72f472663a00b4b206149459fab.png> <link rel=apple-touch-icon sizes=144x144 href=/subpath/static/files/7bd91659bf3fc8c68fcd45fc1db9c630.png> <link rel=apple-touch-icon sizes=120x120 href=/subpath/static/files/fa69ffe11eb334aaef5aece8d848ca62.png> <link rel=apple-touch-icon sizes=152x152 href=/subpath/static/files/f046777feb6ab12fc43b8f9908b1db35.png> <link rel=icon type=image/png sizes=16x16 href=/subpath/static/files/02b96247d275680adaaabf01c71c571d.png> <link rel=icon type=image/png sizes=32x32 href=/subpath/static/files/1d9020f201a6762421cab8d30624fdd8.png> <link rel=icon type=image/png sizes=96x96 href=/subpath/static/files/fe23af39ae98d77dc26ae8586565970f.png> <link rel=icon type=image/png sizes=192x192 href=/subpath/static/files/d7ff68a7675f84337cc154c3d4abe713.png> <link rel=manifest href=/subpath/static/files/a985ad72552ad069537d6eea81e719c7.json> <link rel=stylesheet class=code_theme> <style>.error-screen{font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding-top:50px;max-width:750px;font-size:14px;color:#333;margin:auto;display:none;line-height:1.5}.error-screen h2{font-size:30px;font-weight:400;line-height:1.2}.error-screen ul{padding-left:15px;line-height:1.7;margin-top:0;margin-bottom:10px}.error-screen hr{color:#ddd;margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.error-screen-visible{display:block}</style><script>window.publicPath='/subpath/static/'</script> <link href="/subpath/static/main.364fd054d7a6d741efc6.css" rel="stylesheet"><script type="text/javascript" src="/subpath/static/main.e49599ac425584ffead5.js"></script></head> <body class=font--open_sans> <div id=root> <div class=error-screen> <h2>Cannot connect to Mattermost</h2> <hr/> <p>We're having trouble connecting to Mattermost. If refreshing this page (Ctrl+R or Command+R) does not work, please verify that your computer is connected to the internet.</p> <br/> </div> <div class=loading-screen style=position:relative> <div class=loading__content> <div class="round round-1"></div> <div class="round round-2"></div> <div class="round round-3"></div> </div> </div> </div> <noscript> To use Mattermost, please enable JavaScript. </noscript> </body> </html>`
const subpathCSS = `@font-face{font-family:FontAwesome;src:url(/subpath/static/files/674f50d287a8c48dc19ba404d20fe713.eot);src:url(/subpath/static/files/674f50d287a8c48dc19ba404d20fe713.eot?#iefix&v=4.7.0) format("embedded-opentype"),url(/subpath/static/files/af7ae505a9eed503f8b8e6982036873e.woff2) format("woff2"),url(/subpath/static/files/fee66e712a8a08eef5805a46892932ad.woff) format("woff"),url(/subpath/static/files/b06871f281fee6b241d60582ae9369b9.ttf) format("truetype"),url(/subpath/static/files/677433a0892aaed7b7d2628c313c9775.svg#fontawesomeregular) format("svg");font-weight:400;font-style:normal}`
const newSubpathRootHTML = `<!DOCTYPE html> <html lang=en> <head> <meta charset=utf-8> <meta http-equiv="Content-Security-Policy" content="script-src 'self' cdn.rudderlabs.com/ js.stripe.com/v3 'sha256-mbRaPRRpWz6MNkX9SyXWMJ8XnWV4w/DoqK2M0ryUAvc='"> <meta http-equiv=X-UA-Compatible content="IE=edge"> <meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0"> <meta name=robots content="noindex, nofollow"> <meta name=referrer content=no-referrer> <title>Mattermost</title> <meta name=apple-mobile-web-app-capable content=yes> <meta name=apple-mobile-web-app-status-bar-style content=default> <meta name=mobile-web-app-capable content=yes> <meta name=apple-mobile-web-app-title content=Mattermost> <meta name=application-name content=Mattermost> <meta name=format-detection content="telephone=no"> <link rel=apple-touch-icon sizes=57x57 href=/nested/subpath/static/files/78b7e73b41b8731ce2c41c870ecc8886.png> <link rel=apple-touch-icon sizes=60x60 href=/nested/subpath/static/files/51d00ffd13afb6d74fd8f6dfdeef768a.png> <link rel=apple-touch-icon sizes=72x72 href=/nested/subpath/static/files/23645596f8f78f017bd4d457abb855c4.png> <link rel=apple-touch-icon sizes=76x76 href=/nested/subpath/static/files/26e9d72f472663a00b4b206149459fab.png> <link rel=apple-touch-icon sizes=144x144 href=/nested/subpath/static/files/7bd91659bf3fc8c68fcd45fc1db9c630.png> <link rel=apple-touch-icon sizes=120x120 href=/nested/subpath/static/files/fa69ffe11eb334aaef5aece8d848ca62.png> <link rel=apple-touch-icon sizes=152x152 href=/nested/subpath/static/files/f046777feb6ab12fc43b8f9908b1db35.png> <link rel=icon type=image/png sizes=16x16 href=/nested/subpath/static/files/02b96247d275680adaaabf01c71c571d.png> <link rel=icon type=image/png sizes=32x32 href=/nested/subpath/static/files/1d9020f201a6762421cab8d30624fdd8.png> <link rel=icon type=image/png sizes=96x96 href=/nested/subpath/static/files/fe23af39ae98d77dc26ae8586565970f.png> <link rel=icon type=image/png sizes=192x192 href=/nested/subpath/static/files/d7ff68a7675f84337cc154c3d4abe713.png> <link rel=manifest href=/nested/subpath/static/files/a985ad72552ad069537d6eea81e719c7.json> <link rel=stylesheet class=code_theme> <style>.error-screen{font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding-top:50px;max-width:750px;font-size:14px;color:#333;margin:auto;display:none;line-height:1.5}.error-screen h2{font-size:30px;font-weight:400;line-height:1.2}.error-screen ul{padding-left:15px;line-height:1.7;margin-top:0;margin-bottom:10px}.error-screen hr{color:#ddd;margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.error-screen-visible{display:block}</style><script>window.publicPath='/nested/subpath/static/'</script> <link href="/nested/subpath/static/main.364fd054d7a6d741efc6.css" rel="stylesheet"><script type="text/javascript" src="/nested/subpath/static/main.e49599ac425584ffead5.js"></script></head> <body class=font--open_sans> <div id=root> <div class=error-screen> <h2>Cannot connect to Mattermost</h2> <hr/> <p>We're having trouble connecting to Mattermost. If refreshing this page (Ctrl+R or Command+R) does not work, please verify that your computer is connected to the internet.</p> <br/> </div> <div class=loading-screen style=position:relative> <div class=loading__content> <div class="round round-1"></div> <div class="round round-2"></div> <div class="round round-3"></div> </div> </div> </div> <noscript> To use Mattermost, please enable JavaScript. </noscript> </body> </html>`
const newSubpathCSS = `@font-face{font-family:FontAwesome;src:url(/nested/subpath/static/files/674f50d287a8c48dc19ba404d20fe713.eot);src:url(/nested/subpath/static/files/674f50d287a8c48dc19ba404d20fe713.eot?#iefix&v=4.7.0) format("embedded-opentype"),url(/nested/subpath/static/files/af7ae505a9eed503f8b8e6982036873e.woff2) format("woff2"),url(/nested/subpath/static/files/fee66e712a8a08eef5805a46892932ad.woff) format("woff"),url(/nested/subpath/static/files/b06871f281fee6b241d60582ae9369b9.ttf) format("truetype"),url(/nested/subpath/static/files/677433a0892aaed7b7d2628c313c9775.svg#fontawesomeregular) format("svg");font-weight:400;font-style:normal}`
const baseManifestJSON = `{
"icons": [
{
"src": "/static/icon_96x96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "/static/icon_32x32.png",
"sizes": "32x32",
"type": "image/png"
},
{
"src": "/static/icon_16x16.png",
"sizes": "16x16",
"type": "image/png"
},
{
"src": "/static/icon_76x76.png",
"sizes": "76x76",
"type": "image/png"
},
{
"src": "/static/icon_72x72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "/static/icon_60x60.png",
"sizes": "60x60",
"type": "image/png"
},
{
"src": "/static/icon_57x57.png",
"sizes": "57x57",
"type": "image/png"
},
{
"src": "/static/icon_152x152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "/static/icon_144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "/static/icon_120x120.png",
"sizes": "120x120",
"type": "image/png"
},
{
"src": "/static/icon_192x192.png",
"sizes": "192x192",
"type": "image/png"
}
],
"name": "Mattermost",
"short_name": "Mattermost",
"orientation": "any",
"display": "standalone",
"start_url": ".",
"description": "Mattermost is an open source, self-hosted Slack-alternative",
"background_color": "#ffffff"
}
`
const subpathManifestJSON = `{
"icons": [
{
"src": "/subpath/static/icon_96x96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "/subpath/static/icon_32x32.png",
"sizes": "32x32",
"type": "image/png"
},
{
"src": "/subpath/static/icon_16x16.png",
"sizes": "16x16",
"type": "image/png"
},
{
"src": "/subpath/static/icon_76x76.png",
"sizes": "76x76",
"type": "image/png"
},
{
"src": "/subpath/static/icon_72x72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "/subpath/static/icon_60x60.png",
"sizes": "60x60",
"type": "image/png"
},
{
"src": "/subpath/static/icon_57x57.png",
"sizes": "57x57",
"type": "image/png"
},
{
"src": "/subpath/static/icon_152x152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "/subpath/static/icon_144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "/subpath/static/icon_120x120.png",
"sizes": "120x120",
"type": "image/png"
},
{
"src": "/subpath/static/icon_192x192.png",
"sizes": "192x192",
"type": "image/png"
}
],
"name": "Mattermost",
"short_name": "Mattermost",
"orientation": "any",
"display": "standalone",
"start_url": ".",
"description": "Mattermost is an open source, self-hosted Slack-alternative",
"background_color": "#ffffff"
}
`
const newSubpathManifestJSON = `{
"icons": [
{
"src": "/nested/subpath/static/icon_96x96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "/nested/subpath/static/icon_32x32.png",
"sizes": "32x32",
"type": "image/png"
},
{
"src": "/nested/subpath/static/icon_16x16.png",
"sizes": "16x16",
"type": "image/png"
},
{
"src": "/nested/subpath/static/icon_76x76.png",
"sizes": "76x76",
"type": "image/png"
},
{
"src": "/nested/subpath/static/icon_72x72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "/nested/subpath/static/icon_60x60.png",
"sizes": "60x60",
"type": "image/png"
},
{
"src": "/nested/subpath/static/icon_57x57.png",
"sizes": "57x57",
"type": "image/png"
},
{
"src": "/nested/subpath/static/icon_152x152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "/nested/subpath/static/icon_144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "/nested/subpath/static/icon_120x120.png",
"sizes": "120x120",
"type": "image/png"
},
{
"src": "/nested/subpath/static/icon_192x192.png",
"sizes": "192x192",
"type": "image/png"
}
],
"name": "Mattermost",
"short_name": "Mattermost",
"orientation": "any",
"display": "standalone",
"start_url": ".",
"description": "Mattermost is an open source, self-hosted Slack-alternative",
"background_color": "#ffffff"
}
`

Просмотреть файл

@@ -0,0 +1,73 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"bytes"
"os"
"os/exec"
"path/filepath"
"runtime"
"testing"
"github.com/stretchr/testify/require"
)
func CompileGo(t *testing.T, sourceCode, outputPath string) {
dir, err := os.MkdirTemp(".", "")
require.NoError(t, err)
defer os.RemoveAll(dir)
dir, err = filepath.Abs(dir)
require.NoError(t, err)
// Write out main.go given the source code.
main := filepath.Join(dir, "main.go")
err = os.WriteFile(main, []byte(sourceCode), 0600)
require.NoError(t, err)
_, sourceFile, _, ok := runtime.Caller(0)
require.True(t, ok)
serverPath := filepath.Dir(filepath.Dir(sourceFile))
out := &bytes.Buffer{}
cmd := exec.Command("go", "build", "-o", outputPath, main)
cmd.Dir = serverPath
cmd.Stdout = out
cmd.Stderr = out
err = cmd.Run()
if err != nil {
t.Log("Go compile errors:\n", out.String())
}
require.NoError(t, err, "failed to compile go")
}
func CompileGoTest(t *testing.T, sourceCode, outputPath string) {
dir, err := os.MkdirTemp(".", "")
require.NoError(t, err)
defer os.RemoveAll(dir)
dir, err = filepath.Abs(dir)
require.NoError(t, err)
// Write out main.go given the source code.
main := filepath.Join(dir, "main_test.go")
err = os.WriteFile(main, []byte(sourceCode), 0600)
require.NoError(t, err)
_, sourceFile, _, ok := runtime.Caller(0)
require.True(t, ok)
serverPath := filepath.Dir(filepath.Dir(sourceFile))
out := &bytes.Buffer{}
cmd := exec.Command("go", "test", "-c", "-o", outputPath, main)
cmd.Dir = serverPath
cmd.Stdout = out
cmd.Stderr = out
err = cmd.Run()
if err != nil {
t.Log("Go compile errors:\n", out.String())
}
require.NoError(t, err, "failed to compile go")
}

Просмотреть файл

@@ -0,0 +1,33 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package testutils
import (
"crypto/ecdsa"
"github.com/mattermost/mattermost-server/v6/model"
)
type StaticConfigService struct {
Cfg *model.Config
}
func (s StaticConfigService) Config() *model.Config {
return s.Cfg
}
func (StaticConfigService) AddConfigListener(func(old, current *model.Config)) string {
return ""
}
func (StaticConfigService) RemoveConfigListener(string) {
}
func (StaticConfigService) AsymmetricSigningKey() *ecdsa.PrivateKey {
return &ecdsa.PrivateKey{}
}
func (StaticConfigService) PostActionCookieSecret() []byte {
return make([]byte, 32)
}

Просмотреть файл

@@ -0,0 +1,80 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package testutils
import (
"bytes"
"io"
"net"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"time"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/fileutils"
)
func ReadTestFile(name string) ([]byte, error) {
path, _ := fileutils.FindDir("tests")
file, err := os.Open(filepath.Join(path, name))
if err != nil {
return nil, err
}
defer file.Close()
data := &bytes.Buffer{}
if _, err := io.Copy(data, file); err != nil {
return nil, err
}
return data.Bytes(), nil
}
// GetInterface returns the best match of an interface that might be listening on a given port.
// This is helpful when a test is being run in a CI environment under docker.
func GetInterface(port int) string {
dial := func(iface string, port int) bool {
c, err := net.DialTimeout("tcp", iface+":"+strconv.Itoa(port), time.Second)
if err != nil {
return false
}
c.Close()
return true
}
// First, we check dockerhost
iface := "dockerhost"
if ok := dial(iface, port); ok {
return iface
}
// If not, we check localhost
iface = "localhost"
if ok := dial(iface, port); ok {
return iface
}
// If nothing works, we just attempt to use a hack and get the interface IP.
// https://stackoverflow.com/a/37212665/4962526.
cmdStr := ""
switch runtime.GOOS {
// Using ip address for Linux, ifconfig for Darwin.
case "linux":
cmdStr = `ip address | grep -E "([0-9]{1,3}\.){3}[0-9]{1,3}" | grep -v 127.0.0.1 | awk '{ print $2 }' | cut -f2 -d: | cut -f1 -d/ | head -n1`
case "darwin":
cmdStr = `ifconfig | grep -E "([0-9]{1,3}\.){3}[0-9]{1,3}" | grep -v 127.0.0.1 | awk '{ print $2 }' | cut -f2 -d: | head -n1`
default:
return ""
}
cmd := exec.Command("bash", "-c", cmdStr)
out, err := cmd.CombinedOutput()
if err != nil {
return ""
}
return string(out)
}
func ResetLicenseValidator() {
utils.LicenseValidator = &utils.LicenseValidatorImpl{}
}

529
server/channels/utils/textgeneration.go Обычный файл
Просмотреть файл

@@ -0,0 +1,529 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"math/rand"
"strings"
)
const (
ALPHANUMERIC = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890"
LOWERCASE = "abcdefghijklmnopqrstuvwxyz"
)
// Strings that should pass as acceptable posts
var FuzzyStringsPosts = []string{
`**[1] - [Markdown Tests]**
_italics_
more _italics_
**bold**
more **bold**
**_bold-italic_**
more **_bold-italic_*8
~~strikethrough~~
more ~~strikethrough~~
` + "```" + `
multi-line code block<enter here>
multi-line code block
emoji that should not render in code block: :ice_cream:
` + "```" + `
` + "`monospace`" + `
[Link to Mattermost](www.mattermost.com)
Inline Image with link, alt text, and hover text: ![Build Status](https://travis-ci.org/mattermost/mattermost-server.svg?branch=master)](https://travis-ci.org/mattermost/mattermost-server)
Three types of lines:
***
___
---
`,
` **[2] - **[More Markdown Tests]**
> i am a blockquote!
> i am a 2nd multiline
> quote.
i am text right after a multiline quote, but not in the quote
* list item
* another list item
* indented list item
1. numbered list, item number 1
2. item number two
`,
` **[3]** - **[More Markdown Tests]**
Table
| Left-Aligned | Center Aligned | Right Aligned |
| :------------ |:---------------:| -----:|
| Left column 1 | this text | $100 |
| Left column 2 | is | $10 |
| Left column 3 | centered | $1 |
Ugly table
Markdown | Less | Pretty
--- | --- | ---
*Still* | ~~renders~~ | **nicely**
1 | 2 | 3
# Large heading
## Smaller heading
### Even smaller heading
# Large heading
## Smaller heading
### Even smaller heading
`,
` **[4]** - **[More Markdown Tests]**
# This is a heading
I am a multiline
text.
#### I am a level four heading
` + "```tex" + `
f(x) = \int_{-\infty}^\infty
\hat f(\xi)\,e^{2 \pi i \xi x}
\,d\xi
` + "```" + `
* This was some tex code*
`,
`**[5]** - **[Markdown and automatic preview of content test]**
## This should display a preview for the given vine url
Some text *before* the link
And a smiley :)
https://vine.co/v/eDeVgbFrt9L
Some more text here
and here
and even more here
`,
`**[6]** - **[More markdown and automatic preview of content test]**
## Only the first given url should render an "attachment"
Lets also add a table here, because why not
| Left-Aligned | Center Aligned | Right Aligned |
| :------------ |:---------------:| -----:|
| Left column 1 | this text | $100 |
| Left column 2 | is | $10 |
| Left column 3 | centered | $1 |
Wiki should render:
http://en.wikipedia.org/wiki/Foo
https://vine.co/v/eDeVgbFrt9L
`,
`**[7] [Image Test]**
## this *should* display an image
http://37.media.tumblr.com/tumblr_mavsumGGAd1qboaw8o1_500.jpg
`,
/* `**[2] [Username Linking Test]**
I saw @alice--and I said "Hi @alice!" then "What's up @alice?" and then @alice, was totally @alice; she just "@alice"'d me and walked on by. That's @alice...
@alice‽‽
`,
`**[3] [Mention Highlighting Test]**
`,*/
`**[8] [Emoji Display Test 1]**
:+1: :-1: :100: :1234: :8ball: :a: :ab: :abc: :abcd: :accept:
:aerial_tramway: :airplane: :alarm_clock: :ambulance: :anchor: :angel: :anger: :angry: :anguished: :ant:
:apple: :aquarius: :aries: :arrow_backward: :arrow_double_down: :arrow_double_up: :arrow_down: :arrow_down_small: :arrow_forward: :arrow_heading_down:
:arrow_heading_up: :arrow_left: :arrow_lower_left: :arrow_lower_right: :arrow_right: :arrow_right_hook: :arrow_up: :arrow_up_down:
:arrow_upper_left: :arrow_upper_right: :arrows_clockwise: :arrows_counterclockwise: :art: :articulated_lorry: :astonished: :atm: :arrow_up_small: :b:
:baby: :baby_bottle: :baby_chick: :baby_symbol: :back: :baggage_claim: :balloon: :ballot_box_with_check: :bamboo: :banana:
:bangbang: :bank: :bar_chart: :barber: :baseball: :basketball: :bath: :bathtub: :battery: :bear:
:bee: :beer: :beers: :beetle: :beginner: :bell: :bento: :bicyclist: :bike: :bikini:
:bird: :birthday: :black_circle: :black_joker: :black_medium_small_square: :black_medium_square: :black_nib: :black_small_square: :black_square: :black_square_button:
:blossom: :blowfish: :blue_book: :blue_car: :blue_heart: :blush: :boar: :boat: :bomb: :book:
:bookmark: :bookmark_tabs: :books: :boom: :boot: :bouquet: :bow: :bowling: :bowtie: :boy:
:bread: :bride_with_veil: :bridge_at_night: :briefcase: :broken_heart: :bug: :bulb: :bullettrain_front: :bullettrain_side: :bus:
:busstop: :bust_in_silhouette: :busts_in_silhouette: :cactus: :cake: :calendar: :calling: :camel: :camera: :cancer:
:candy: :capital_abcd: :capricorn: :car: :card_index: :carousel_horse: :cat: :cat2: :cd: :chart:
:chart_with_downwards_trend: :chart_with_upwards_trend: :checkered_flag: :cherries: :cherry_blossom: :chestnut: :chicken: :children_crossing: :chocolate_bar: :christmas_tree:
:church: :cinema: :circus_tent: :city_sunrise: :city_sunset: :cl: :clap: :clapper: :clipboard: :clock1:
:clock10: :clock1030: :clock11: :clock1130: :clock12: :clock1230: :clock130: :clock2: :clock230: :clock3:
:clock330: :clock4: :clock430: :clock5: :clock530: :clock6: :clock630: :clock7: :clock730: :clock8:
:clock830: :clock9: :clock930: :closed_book: :closed_lock_with_key: :closed_umbrella: :cloud: :clubs: :cn: :cocktail:
:coffee: :cold_sweat: :collision: :computer: :confetti_ball: :confounded: :confused: :congratulations: :construction: :construction_worker:
:convenience_store: :cookie: :cool: :cop: :copyright: :corn: :couple: :couple_with_heart: :couplekiss: :cow:
:cow2: :credit_card: :crescent_moon: :crocodile: :crossed_flags: :crown: :cry: :crying_cat_face: :crystal_ball: :cupid:
:curly_loop: :currency_exchange: :curry: :custard: :customs: :cyclone: :dancer: :dancers: :dango: :dart:
:dash: :date: :de: :deciduous_tree: :department_store: :diamond_shape_with_a_dot_inside: :diamonds: :disappointed: :disappointed_relieved: :dizzy:
:dizzy_face: :do_not_litter: :dog: :dog2: :dollar: :dolls: :dolphin: :donut: :door: :doughnut:
:dragon: :dragon_face: :dress: :dromedary_camel: :droplet: :dvd: :e-mail: :ear: :ear_of_rice: :earth_africa:
:earth_americas: :earth_asia: :egg: :eggplant: :eight: :eight_pointed_black_star: :eight_spoked_asterisk: :electric_plug: :elephant: :email:
:end: :envelope: :es: :euro: :european_castle: :european_post_office: :evergreen_tree: :exclamation: :expressionless: :eyeglasses:
:eyes: :facepunch: :factory: :fallen_leaf: :family: :fast_forward: :fax: :fearful: :feelsgood: :feet:
:ferris_wheel: :file_folder: :finnadie: :fire: :fire_engine: :fireworks: :first_quarter_moon: :first_quarter_moon_with_face: :fish: :fish_cake:
:fishing_pole_and_fish: :fist: :five: :flags: :flashlight: :floppy_disk: :flower_playing_cards: :flushed: :foggy: :football:
:fork_and_knife: :fountain: :four: :four_leaf_clover: :fr: :free: :fried_shrimp: :fries: :frog: :frowning:
:fu: :fuelpump: :full_moon: :full_moon_with_face: :game_die: :gb: :gem: :gemini: :ghost: :gift:`,
`**[9] [Emoji Display Test 2]**
:gift_heart: :girl: :globe_with_meridians: :goat: :goberserk: :godmode: :golf: :grapes: :green_apple: :green_book:
:green_heart: :grey_exclamation: :grey_question: :grimacing: :grin: :grinning: :guardsman: :guitar: :gun: :haircut:
:hamburger: :hammer: :hamster: :hand: :handbag: :hankey: :hash: :hatched_chick: :hatching_chick: :headphones:
:hear_no_evil: :heart: :heart_decoration: :heart_eyes: :heart_eyes_cat: :heartbeat: :heartpulse: :hearts: :heavy_check_mark: :heavy_division_sign:
:heavy_dollar_sign: :heavy_exclamation_mark: :heavy_minus_sign: :heavy_multiplication_x: :heavy_plus_sign: :helicopter: :herb: :hibiscus: :high_brightness: :high_heel:
:hocho: :honey_pot: :honeybee: :horse: :horse_racing: :hospital: :hotel: :hotsprings: :hourglass: :hourglass_flowing_sand:
:house: :house_with_garden: :hurtrealbad: :hushed: :ice_cream: :icecream: :id: :ideograph_advantage: :imp: :inbox_tray:
:incoming_envelope: :information_desk_person: :information_source: :innocent: :interrobang: :iphone: :it: :izakaya_lantern: :jack_o_lantern:
:japan: :japanese_castle: :japanese_goblin: :japanese_ogre: :jeans: :joy: :joy_cat: :jp: :key: :keycap_ten:
:kimono: :kiss: :kissing: :kissing_cat: :kissing_closed_eyes: :kissing_face: :kissing_heart: :kissing_smiling_eyes: :koala: :koko:
:kr: :large_blue_circle: :large_blue_diamond: :large_orange_diamond: :last_quarter_moon: :last_quarter_moon_with_face: :laughing: :leaves: :ledger: :left_luggage:
:left_right_arrow: :leftwards_arrow_with_hook: :lemon: :leo: :leopard: :libra: :light_rail: :link: :lips: :lipstick:
:lock: :lock_with_ink_pen: :lollipop: :loop: :loudspeaker: :love_hotel: :love_letter: :low_brightness: :m: :mag:
:mag_right: :mahjong: :mailbox: :mailbox_closed: :mailbox_with_mail: :mailbox_with_no_mail: :man: :man_with_gua_pi_mao: :man_with_turban: :mans_shoe:
:maple_leaf: :mask: :massage: :meat_on_bone: :mega: :melon: :memo: :mens: :metal: :metro:
:microphone: :microscope: :milky_way: :minibus: :minidisc: :mobile_phone_off: :money_with_wings: :moneybag: :monkey: :monkey_face:
:monorail: :mortar_board: :mount_fuji: :mountain_bicyclist: :mountain_cableway: :mountain_railway: :mouse: :mouse2: :movie_camera: :moyai:
:muscle: :mushroom: :musical_keyboard: :musical_note: :musical_score: :mute: :nail_care: :name_badge: :neckbeard: :necktie:
:negative_squared_cross_mark: :neutral_face: :new: :new_moon: :new_moon_with_face: :newspaper: :ng: :nine: :no_bell:
:no_bicycles: :no_entry: :no_entry_sign: :no_good: :no_mobile_phones: :no_mouth: :no_pedestrians: :no_smoking: :non-potable_water: :nose:
:notebook: :notebook_with_decorative_cover: :notes: :nut_and_bolt: :o: :o2: :ocean: :octocat: :octopus: :oden:
:office: :ok: :ok_hand: :ok_woman: :older_man: :older_woman: :on: :oncoming_automobile: :oncoming_bus: :oncoming_police_car:
:oncoming_taxi: :one: :open_file_folder: :open_hands: :open_mouth: :ophiuchus: :orange_book: :outbox_tray: :ox: :package:
:page_facing_up: :page_with_curl: :pager: :palm_tree: :panda_face: :paperclip: :parking: :part_alternation_mark: :partly_sunny: :passport_control:
:paw_prints: :peach: :pear: :pencil: :pencil2: :penguin: :pensive: :performing_arts: :persevere: :person_frowning:
:person_with_blond_hair: :person_with_pouting_face: :phone: :pig: :pig2: :pig_nose: :pill: :pineapple: :pisces: :pizza:
`,
`**[10] [Emoji Display Test 3]**
:plus1: :point_down: :point_left: :point_right: :point_up: :point_up_2: :police_car: :poodle: :poop: :post_office:
:postal_horn: :postbox: :potable_water: :pouch: :poultry_leg: :pound: :pouting_cat: :pray: :princess: :punch:
:purple_heart: :purse: :pushpin: :put_litter_in_its_place: :question: :rabbit: :rabbit2: :racehorse: :radio: :radio_button:
:rage: :rage1: :rage2: :rage3: :rage4: :railway_car: :rainbow: :raised_hand: :raised_hands: :raising_hand:
:ram: :ramen: :rat: :recycle: :red_car: :red_circle: :registered: :relaxed: :relieved: :repeat:
:repeat_one: :restroom: :revolving_hearts: :rewind: :ribbon: :rice: :rice_ball: :rice_cracker: :rice_scene: :ring:
:rocket: :roller_coaster: :rooster: :rose: :rotating_light: :round_pushpin: :rowboat: :ru:
:rugby_football: :runner: :running: :running_shirt_with_sash: :sa: :sagittarius: :sailboat: :sake: :sandal: :santa:
:satellite: :satisfied: :saxophone: :school: :school_satchel: :scissors: :scorpius: :scream: :scream_cat: :scroll:
:seat: :secret: :see_no_evil: :seedling: :seven: :shaved_ice: :sheep: :shell: :ship: :shipit:
:shirt: :shit: :shoe: :shower: :signal_strength: :six: :six_pointed_star: :ski: :skull: :sleeping:
:sleepy: :slot_machine: :small_blue_diamond: :small_orange_diamond: :small_red_triangle: :small_red_triangle_down: :smile: :smile_cat: :smiley: :smiley_cat:
:smiling_imp: :smirk: :smirk_cat: :smoking: :snail: :snake: :snowboarder: :snowflake: :snowman: :sob:
:soccer: :soon: :sos: :sound: :space_invader: :spades: :spaghetti: :sparkle: :sparkler: :sparkles:
:sparkling_heart: :speak_no_evil: :speaker: :speech_balloon: :speedboat: :squirrel: :star: :star2: :stars: :station:
:statue_of_liberty: :steam_locomotive: :stew: :straight_ruler: :strawberry: :stuck_out_tongue: :stuck_out_tongue_closed_eyes: :stuck_out_tongue_winking_eye: :sun_with_face: :sunflower:
:sunglasses: :sunny: :sunrise: :sunrise_over_mountains: :surfer: :sushi: :suspect: :suspension_railway: :sweat: :sweat_drops:
:sweat_smile: :sweet_potato: :swimmer: :symbols: :syringe: :tada: :tanabata_tree: :tangerine: :taurus: :taxi:
:tea: :telephone: :telephone_receiver: :telescope: :tennis: :tent: :thought_balloon: :three: :thumbsdown: :thumbsup:
:ticket: :tiger: :tiger2: :tired_face: :tm: :toilet: :tokyo_tower: :tomato: :tongue: :top:
:tophat: :tractor: :traffic_light: :train: :train2: :tram: :triangular_flag_on_post: :triangular_ruler: :trident: :triumph:
:trolleybus: :trollface: :trophy: :tropical_drink: :tropical_fish: :truck: :trumpet: :tshirt: :tulip: :turtle:
:tv: :twisted_rightwards_arrows: :two: :two_hearts: :two_men_holding_hands: :two_women_holding_hands:
:uk: :umbrella: :unamused: :underage: :unlock: :up: :us: :v: :vertical_traffic_light: :vhs:
:vibration_mode: :video_camera: :video_game: :violin: :virgo: :volcano: :vs: :walking: :waning_crescent_moon: :waning_gibbous_moon:
:warning: :watch: :water_buffalo: :watermelon: :wave: :wavy_dash: :waxing_crescent_moon: :waxing_gibbous_moon: :wc: :weary:
:wedding: :whale: :whale2: :wheelchair: :white_check_mark: :white_circle: :white_flower: :white_large_square: :white_medium_small_square: :white_medium_square:
:white_small_square: :white_square_button: :wind_chime: :wine_glass: :wink: :wolf: :woman: :womans_clothes: :womans_hat: :womens:
:worried: :wrench: :x: :yellow_heart: :yen: :yum: :zap: :zero: :zzz:
Unnamed: :u5272: :u5408: :u55b6: :u6307: :u6708: :u6709: :u6e80: :u7121: :u7533: :u7981: :u7a7a:
`,
`**[11] [Auto Linking]**
#### should be turned into links:
http://example.com
https://example.com
www.example.com
www.example.com/index
www.example.com/index.html
www.example.com/index/sub
www.example.com/index?params=1
www.example.com/index?params=1&other=2
www.example.com/index?params=1;other=2
http://example.com:8065
<http://example.com>
<www.example.com>
http://www.example.com/_/page
www.example.com/_/page
https://en.wikipedia.org/wiki/🐬
https://en.wikipedia.org/wiki/Rendering_(computer_graphics)
http://127.0.0.1
http://192.168.1.1:4040
http://[::1]:80
http://[::1]:8065
https://[::1]:80
http://[2001:0:5ef5:79fb:303a:62d5:3312:ff42]:80
http://[2001:0:5ef5:79fb:303a:62d5:3312:ff42]:8065
https://[2001:0:5ef5:79fb:303a:62d5:3312:ff42]:443
http://username:password@example.com
http://username:password@127.0.0.1
http://username:password@[2001:0:5ef5:79fb:303a:62d5:3312:ff42]:80
test@example.com
#### should be turned into links which link to the correct place:
[example link](example.com) links to ` + "`" + `http://example.com` + "`" + `
[example.com](example.com) links to ` + "`" + `http://example.com` + "`" + `
[example.com/other](example.com) links to ` + "`" + `http://example.com` + "`" + `
[example.com/other_link](example.com/example) links to ` + "`" + `http://example.com/example` + "`" + `
www.example.com links to ` + "`" + `http://www.example.com` + "`" + `
https://example.com links to ` + "`" + `https://example.com` + "`" + `and not ` + "`" + `http://example.com` + "`" + `
https://en.wikipedia.org/wiki/🐬 links to the Wikipedia article on dolphins
https://en.wikipedia.org/wiki/URLs#Syntax links to the Syntax section of the Wikipedia article on URLs
test@example.com links to ` + "`" + `mailto:test@example.com` + "`" + `
[email link](mailto:test@example.com) links to ` + "`" + `mailto:test@example.com` + "`" + `and not ` + "`" + `http://mailto:test@example.com` + "`" + `
[other link](ts3server://example.com) links to ` + "`" + `ts3server://example.com` + "`" + `and not ` + "`" + `http://ts3server://example.com` + "`" + `
#### should not be turned into links:
example.com
readme.md
<example.com>
http://
@example.com
#### should only turn the actual link into a link and not change surrounding text
(http://example.com)
(test@example.com)
This is a sentence with a http://example.com in it.
This is a sentence with a [link](http://example.com) in it.
This is a sentence with a http://example.com/_/underscore in it.
This is a sentence with a link (http://example.com) in it.
This is a sentence with a (https://en.wikipedia.org/wiki/Rendering_(computer_graphics)) in it.
This is a sentence with a http://192.168.1.1:4040 in it.
This is a sentence with a https://::1 in it.
This is a link to http://example.com.
`,
"*", "?", ".", "}{][)(><", "{}[]()<>",
"qahwah ( قهوة)",
"שָׁלוֹם עֲלֵיכֶם",
"Ramen チャーシュー chāshū",
"言而无信",
"Ṫ͌ó̍ ̍͂̓̍̍̀i̊ͯ͒",
"&amp; &lt; &qu",
"' or '1'='1' -- ",
"' or '1'='1' ({ ",
"' or '1'='1' /* ",
"1;DROP TABLE users",
"<b><i><u><strong><em>",
"sue@thatmightbe",
"sue@thatmightbe.",
"sue@thatmightbe.c",
"sue@thatmightbe.co",
"su+san@thatmightbe.com",
"a@b.中国",
"1@2.am",
"a@b.co.uk",
"a@b.cancerresearch",
"local@[127.0.0.1]",
"!@$%^&:*.,/|;'\"+=?`~#",
"'\"/\\\"\"''\\/",
"gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg",
"gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg",
"ą ć ę ł ń ó ś ź ż č ď ě ň ř š ť ž ă î ø å æ á é í ó ú Ç Ğ İ Ö Ş Ü",
"abcdefghijklmnopqrstuvwrxyz0123456789 -_",
"Ṫ͌ó̍ ̍͂̓̍̍̀i̊ͯ͒nͧ̍̓̃͋vok̂̓ͤ̓̂ěͬ ͆tͬ̐́̐͆h̒̏͌̓e͂ ̎̊h̽͆ͯ̄ͮi͊̂ͧͫ̇̃vͥͦ́ẻͤ-͒m̈́̀i̓ͮ͗̑͌̆̅n̓̓ͨd̊̑͛̔̚ ͨͮ̊̾rͪeͭͭ͑ͧ́͋p̈́̅̚rͧe̒̈̌s̍̽ͩ̓̇e͗n̏͊ͬͭtͨ͆ͤ̚iͪ͗̍n͐͒g̾ͦ̎ ͥ͌̽̊ͩͥ͗c̀ͬͣha̍̏̉ͪ̈̚o̊̏s̊̋̀̏̽̚.͒ͫ͛͛̎ͥ",
"H҉̵̞̟̠̖̗̘Ȅ̐̑̒̚̕̚ IS C̒̓̔̿̿̿̕̚̚̕̚̕̚̕̚̕̚̕̚OMI҉̵̞̟̠̖̗̘NG > ͡҉҉ ̵̡̢̛̗̘̙̜̝̞̟̠͇̊̋̌̍̎̏̿̿̿̚ ҉ ҉҉̡̢̡̢̛̛̖̗̘̙̜̝̞̟̠̖̗̘̙̜̝̞̟̠̊̋̌̍̎̏̐̑̒̓̔̊̋̌̍̎̏̐̑ ͡҉҉",
"<a href=\"//www.google.com\">Teh Googles</a>",
"<img src=\"//upload.wikimedia.org/wikipedia/meta/b/be/Wikipedia-logo-v2_2x.png\" />",
"&amp; &lt; &quot; &apos;",
" %21 %23 %24 %26 %27 %28 %29 %2A %2B %2C %2F %3A %3B %3D %3F %40 %5B %5D %0D %0A %0D%0A %20 %22 %25 %2D %2E %3C %3E %5C %5E %5F %60 %7B %7C %7D %7E",
";alert('Well this is awkward.');",
"<script type='text/javascript'>alert('yay puppies');</script>",
"http?q=foobar%0d%0aContent-\nLength:%200%0d%0a%0d%0aHTTP/1.1%20200%20OK%0d%0aContent-\nType:%20text/html%0d%0aContent-Length:%2019%0d%0a%0d%0a<html>Shazam</html>",
"apos'trophe@thatmightbe.com",
"apos''''trophe@thatmightbe.com",
"su+s+an@thatmightbe.com",
"per.iod@thatmightbe.com",
"per..iods@thatmightbe.com",
".period@thatmightbe.com",
"tom(comment)@thatmightbe.com",
"(comment)tom@thatmightbe.com",
"\"quotes\"@thatmightbe.com",
"\"\\\"(),:;<>@[\\]\"@thatmightbe.com",
"a!#$%&'*+-/=?^_`{|}~b@thatmightbe.com",
"jill@(comment)example.com",
"jill@example.com(comment)",
"ben@ggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg.com",
"judy@gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg.com",
"ggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA.com",
}
// Strings that should pass as acceptable team names
var FuzzyStringsNames = []string{
"*",
"?",
".",
"}{][)(><",
"{}[]()<>",
"qahwah ( قهوة)",
"שָׁלוֹם עֲלֵיכֶם",
"Ramen チャーシュー chāshū",
"言而无信",
"Ṫ͌ó̍ ̍͂̓̍̍̀i̊ͯ͒",
"&amp; &lt; &qu",
"' or '1'='1' -- ",
"' or '1'='1' ({ ",
"' or '1'='1' /* ",
"1;DROP TABLE users",
"<b><i><u><strong><em>",
"sue@thatmightbe",
"sue@thatmightbe.",
"sue@thatmightbe.c",
"sue@thatmightbe.co",
"sue @ thatmightbe.com",
"apos'trophe@thatmightbe.com",
"apos''''trophe@thatmightbe.com",
"su+san@thatmightbe.com",
"su+s+an@thatmightbe.com",
"per.iod@thatmightbe.com",
"per..iods@thatmightbe.com",
".period@thatmightbe.com",
"tom(comment)@thatmightbe.com",
"(comment)tom@thatmightbe.com",
"\"quotes\"@thatmightbe.com",
"\"\\\"(),:;<>@[\\]\"@thatmightbe.com",
"a!#$%&'*+-/=?^_`{|}~b@thatmightbe.com",
"local@[127.0.0.1]",
"jill@(comment)example.com",
"jill@example.com(comment)",
"a@b.中国",
"1@2.am",
"a@b.co.uk",
"a@b.cancerresearch",
"<a href=\"//www.google.com\">Teh Googles</a>",
"<img src=\"//upload.wikimedia.org/wikipelogo-v2_2x.png\" />",
"<b><i><u><strong><em>",
"&amp; &lt; &quot; &apos;",
";alert('Well this is awkward.');",
"<script type='text/javascript'>alert('yay puppies');</script>",
"Ṫ͌ó̍ ̍͂̓̍̍̀i̊ͯ͒nͧ̍̓̃͋v",
"H҉̵̞̟̠̖̗̘Ȅ̐̐̑̒̚OMI҉̵̞̟̠",
}
// Strings that should pass as acceptable emails
var FuzzyStringsEmails = []string{
"sue@thatmightbe",
"sue@thatmightbe.c",
"sue@thatmightbe.co",
"su+san@thatmightbe.com",
"1@2.am",
"a@b.co.uk",
"a@b.cancerresearch",
"su+s+an@thatmightbe.com",
"per.iod@thatmightbe.com",
}
// Lovely giberish for all to use
const GibberishText = `
Thus one besides much goodness shyly far some hyena overtook since rhinoceros nodded withdrew wombat before deserved apart a alongside the far dalmatian less ouch where yet a salmon.
Then jeez far marginal hey aboard more as leaned much oversold that inside spoke showed much went crud close save so and and after and informally much lion commendably less conductive oh excepting conductive compassionate jeepers hey a much leopard alas woolly untruthful outside snug rashly one cunning past fabulous adjusted far woodchuck and and indecisive crud loving exotic less resolute ladybug sprang drank under following far the as hence passably stolidly jeez the inset spaciously more cozily fishily the hey alas petted one audible yikes dear preparatory darn goldfinch gosh a then as moth more guinea.
Timid mislaid as salamander yikes alas ouch much that goldfinch shark in before instead dear one swore vivid versus one until regardless sang panther tolerable much preparatory hardily shuddered where coquettish far sheep coarsely exaggerated preparatory because cordial awesome gradually nutria that dear mocking behind off staunchly regarding a the komodo crud shrewd well jeez iguanodon strove strived and moodily and sought and and mounted gosh aboard crud spitefully boa.
One as highhanded fortuitous angelfish so one woodchuck dazedly kangaroo nasty instead far parrot away the worm yet testy where caribou a cuckoo onto dear reined because less tranquil kindhearted and shuddered plankton astride monkey methodically above evasive otter this wrung and courageous iguana wayward along cowered prior a.
Freely since ouch octopus the heated apart on hey the some pending placed fearless jeepers hardheadedly more that less jolly bit cuddled.
Caterpillar laboriously far wistful spilled aside far oriole newt and immeasurably yikes revealed raptly obdurately definitely scallop titilatingly one alongside monumentally ouch much wretched the spoke a before alas insolent abortive that turned hey hare much poignantly re-laid goodness yet the dear compassionate a hey scooped sped darn warmly oh and more darn craven that overtook fell and bluebird misheard that needless less ravenously in positively far romantically some babbled that rose honey then immaturely this and jollily irresistible much rarely earthworm parrot wow.
Less less bluntly jeez at goodness panther opposite oh purred a pathetically mildly less cat badly much much on from obscure in gull off manatee hatchet goodness euphemistically hence or understandable after this so that thus shook hence that mindfully yellow behind far bat wayward thanks more wrote so the flapped however alas and mallard that temperately irritably yikes squirrel.
Some reset some therefore demonstrably considering dachshund kindhearted far wow far whispered far clung this by partook much upon fit inscrutably so affirmative diligently far grinned and manifestly hummingbird hello caudal considering when aboard much buoyantly that unfitting far attractively far during much crud baneful jeez one toneless cynically oh spurious athletic meadowlark much generously one subconsciously arguable much forthrightly hawk inoffensively.
Snorted tidy stiffly against one fiendishly began burst hey revealed a beside the soothingly ceremonially affirmatively cowered when fitted this static hello emoted assenting however while far that gross besides because and dear.
Far therefore the blushed momentously the however one a wholeheartedly and considering incessantly that neurotically wore firefly grouped impotently dear one abjectly goodness so far a honey far insolently far so greyhound between above raucously echidna more halfhearted thankful squid one.
Raccoon cockatoo this while but this a far among ouch and hey alas scallop black sane as yikes hello sexy far tacky and balked wrongly more near shrewdly the yet gosh much caribou ruthlessly a on far a threw well less at the one after.
Spoke touched barbarously before much thus therefore darn scratched oh howled the less much hello after and jeez flagrantly weirdly crud komodo fabulous the much some cow jeering much egregiously a bucolically a admirably jeepers essential when ouch and tapir this while and wolverine.
Cm more much in this rewrote ouch on from aside wildebeest crane saddled where much opposite endearingly hummingbird together some beside a the goodness dear ouch ouch struck the input smooched shrugged until slick as waked hawk sincere irksomely.
Camel the pulled this richly grimaced leopard more false thought dear militant added yikes supp infallibly set orca beat hello while accurately reliably while lorikeet one strategic less hello without and smooched across plankton but jeepers pangolin the rich seal sneered pre-set lynx on radical nasty alas onto more hence flabby outbid murkily congenially dived much lubber added far eccentrically turtle before outsold onto ouch thus much and hawk tolerable much knitted yikes shot much limpet one this woolly much however hence up angry up well.
Unicorn yawned hello boundless this when express jaded closed wept tranquil after came airily merry much dismounted for much extensively less interminably far one far armadillo pled dolphin alas nutria and more oh positively koala grizzly after falcon goat strict hooted next browbeat split more far far antagonistic lingering the depending pending sheared since up before jeepers distant mastodon dropped as this more some much set far infinitesimal well shark grasshopper as hey one via some fishy and immaturely remote where weasel leopard annoying correctly wherever that sniffled much mandrill on jeez adventurous much.
Jeepers before spitefully buoyant concentric the reset moth a darn decidedly baboon giraffe outrageously groundhog on one at more overslept gosh worm away far far less much hysteric showed on so rattlesnake the and immature yikes baneful hence wow lynx hence past scornfully groaned pounded dived this one outside dachshund scowled one prior tenable therefore before scratched much much drank hey while added rabbit shark and supp cut this ironic limpet hedgehog bound more rebuking the jeepers thorough while more far due but yikes nastily brave dangerous opened tangibly aside after acrimoniously one cackled scratched.
Canny salmon hatchet more far opposite much coughed excited expedient far lizard one indiscriminate yikes jeez powerlessly forcefully tiger rooster and brought far more during this sank onto after then less amorally rude unerring some alongside irrespective bat hungrily kangaroo extravagantly inside ouch much gosh dreadfully oh much darn prior as fired guinea.
Irksomely upon up for amicably one since contrary one until flamingo tarantula far koala despite easy well gazelle ungracefully rose less that under hey more criminal unique furrowed so disbanded normal where one a a hey circuitous ouch feverish for the kookaburra and pithy far far then more the versus cliquishly across oh and explicitly much therefore as tamely alongside underlay much yikes imminently off however far across instantaneous therefore wallaby evidently foul foretold as far a jeepers invidious bearish.
More and until scandalously after wallaby petted oh much as poked much caterpillar drank beside rode actively walking scooped weird this duteous that far before human during dear house thrust more flinched opposite that ahead in far.
The painful essential jeepers merrily proudly essential and less far dismounted inside mongoose beyond confessedly robin shined heron the during since according suggestively and less some strident combed alas much man-of-war forgave so and to then inanimately.
Beside far this this a crud polite cantankerous exclusively misheard pled far circuitously and frugal less more temperately gauche goldfinch oh against this along excitedly goodhearted more classically quit serenely outside vulture ouch after one a this yet.
Less and handsomely manatee some amidst much reined komodo busted exultingly but fatuously less across mighty goodness objective alas glaringly gregariously hello the since one pridefully much well placed far less goodness jellyfish unnecessary reciprocating a far stylistic gazed one.
Hey rethought excepting lamely much and naughtily amidst more since jeez then bluebird hence less bald by some brought left the across logic loyal brightly jeez capitally that less more forward rebound a yikes chose convulsively confidently repeated broadcast much dipped when awesomely or some some regal the scowled merry zebra since more credible so inescapably fetchingly and lantern that due dear one went gosh wow well furrowed much much specially spoiled as vitally instead the seriously some rooster irrespective well imprecisely rapidly more llama.
Up to and hey without pill that this squid alas brusque on inventoried and spread the more excepting aristocratically due piquant wove beneath that macaw in more until much grimaced far and jeez enticingly unicorn some far crab more barring purely jeepers clear groomed glaring hey dear hence before the this hello.`
func RandString(l int, charset string) string {
ret := make([]byte, l)
for i := 0; i < l; i++ {
ret[i] = charset[rand.Intn(len(charset))]
}
return string(ret)
}
// func RandomEmail(length Range, charset string) string {
// emaillen := RandIntFromRange(length)
// username := RandString(emaillen, charset)
// domain := "simulator.amazonses.com"
// return "success+" + username + "@" + domain
// }
// func FuzzEmail() string {
// return FuzzyStringsEmails[RandIntFromRange(Range{0, len(FuzzyStringsEmails) - 1})]
// }
func RandomName(length Range, charset string) string {
namelen := RandIntFromRange(length)
return RandString(namelen, charset)
}
func FuzzName() string {
return FuzzyStringsNames[RandIntFromRange(Range{0, len(FuzzyStringsNames) - 1})]
}
// Random selection of text for post
func RandomText(length Range, hashtags Range, mentions Range, users []string) string {
textLength := RandIntFromRange(length)
numHashtags := RandIntFromRange(hashtags)
numMentions := RandIntFromRange(mentions)
if textLength > len(GibberishText) || textLength < 0 {
textLength = len(GibberishText)
}
startPosition := RandIntFromRange(Range{0, len(GibberishText) - textLength - 1})
words := strings.Split(GibberishText[startPosition:startPosition+textLength], " ")
for i := 0; i < numHashtags; i++ {
randword := RandIntFromRange(Range{0, len(words) - 1})
words = append(words, " #"+words[randword])
}
if len(users) > 0 {
for i := 0; i < numMentions; i++ {
randuser := RandIntFromRange(Range{0, len(users) - 1})
words = append(words, " @"+users[randuser])
}
}
// Shuffle the words
for i := range words {
j := rand.Intn(i + 1)
words[i], words[j] = words[j], words[i]
}
return strings.Join(words, " ")
}
func FuzzPost() string {
return FuzzyStringsPosts[RandIntFromRange(Range{0, len(FuzzyStringsPosts) - 1})]
}

30
server/channels/utils/time.go Обычный файл
Просмотреть файл

@@ -0,0 +1,30 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"time"
)
func MillisFromTime(t time.Time) int64 {
return t.UnixNano() / int64(time.Millisecond)
}
func TimeFromMillis(millis int64) time.Time {
return time.Unix(0, millis*int64(time.Millisecond))
}
func StartOfDay(t time.Time) time.Time {
year, month, day := t.Date()
return time.Date(year, month, day, 0, 0, 0, 0, t.Location())
}
func EndOfDay(t time.Time) time.Time {
year, month, day := t.Date()
return time.Date(year, month, day, 23, 59, 59, 999999999, t.Location())
}
func Yesterday() time.Time {
return time.Now().AddDate(0, 0, -1)
}

46
server/channels/utils/time_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,46 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
var format = "2006-01-02 15:04:05.000000000"
func TestMillisFromTime(t *testing.T) {
input, _ := time.Parse(format, "2015-01-01 12:34:00.000000000")
actual := MillisFromTime(input)
expected := int64(1420115640000)
assert.Equal(t, expected, actual)
}
func TestYesterday(t *testing.T) {
actual := Yesterday()
expected := time.Now().AddDate(0, 0, -1)
assert.Equal(t, expected.Year(), actual.Year())
assert.Equal(t, expected.Day(), actual.Day())
assert.Equal(t, expected.Month(), actual.Month())
}
func TestStartOfDay(t *testing.T) {
input, _ := time.Parse(format, "2015-01-01 12:34:00.000000000")
actual := StartOfDay(input)
expected, _ := time.Parse(format, "2015-01-01 00:00:00.000000000")
assert.Equal(t, expected, actual)
}
func TestEndOfDay(t *testing.T) {
input, _ := time.Parse(format, "2015-01-01 12:34:00.000000000")
actual := EndOfDay(input)
expected, _ := time.Parse(format, "2015-01-01 23:59:59.999999999")
assert.Equal(t, expected, actual)
}

82
server/channels/utils/true_up.go Обычный файл
Просмотреть файл

@@ -0,0 +1,82 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"time"
)
const trueUpReviewDueDay = 15
const day = time.Hour * 24
type DueDateWindow struct {
Start time.Time
End time.Time
}
func GetNextTrueUpReviewDueDate(now time.Time) time.Time {
nowYear := now.Year()
nowMonth := now.Month()
nowDay := now.Day()
finalQuarterYear := nowYear
if nowMonth >= time.October && nowMonth <= time.December {
finalQuarterYear = nowYear + 1
}
trueUpSubmissionWindows := []DueDateWindow{
{
Start: time.Date(now.Year(), time.January, 16, 0, 0, 0, 0, now.Location()),
End: time.Date(now.Year(), time.April, 15, 0, 0, 0, 0, now.Location()),
},
{
Start: time.Date(now.Year(), time.April, 16, 0, 0, 0, 0, now.Location()),
End: time.Date(now.Year(), time.July, 15, 0, 0, 0, 0, now.Location()),
},
{
Start: time.Date(now.Year(), time.July, 16, 0, 0, 0, 0, now.Location()),
End: time.Date(now.Year(), time.October, 15, 0, 0, 0, 0, now.Location()),
},
{
Start: time.Date(now.Year(), time.October, 16, 0, 0, 0, 0, now.Location()),
End: time.Date(finalQuarterYear, time.January, 15, 0, 0, 0, 0, now.Location()),
},
}
for _, window := range trueUpSubmissionWindows {
withinWindow := false
// Our due dates "wrap" around (i.e. can go into the next year), so we'll need to check the months different. Since January = 1 and December = 12, the checks
// for the current month being greater or equal to the start month and less than or equal to the end month will not work.
if window.End.Month() == time.January {
withinWindow = (nowMonth != time.January && nowMonth >= window.Start.Month()) || nowMonth == window.End.Month()
} else {
withinWindow = nowMonth >= window.Start.Month() && nowMonth <= window.End.Month()
}
// Only check the days if the current month is equal to the start or end months.
// The dates of the middle month(s) don't matter so much.
isFirstMonth := nowMonth == window.Start.Month()
if isFirstMonth {
withinWindow = withinWindow && nowDay >= window.Start.Day()
}
isFinalMonth := nowMonth == window.End.Month()
if isFinalMonth {
withinWindow = withinWindow && nowDay <= window.End.Day()
}
if withinWindow {
return window.End
}
}
return trueUpSubmissionWindows[0].End
}
func IsTrueUpReviewDueDateWithinTheNext30Days(now time.Time, dueDate time.Time) bool {
dueDateWindow := dueDate.Add(-day * 30)
if now.Before(dueDateWindow) || now.After(dueDate) {
return false
}
return true
}

125
server/channels/utils/true_up_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,125 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestGetNextTrueUpReviewDueDate(t *testing.T) {
t.Run("Due date always falls on the 15th", func(t *testing.T) {
// Before the 15th
now := time.Date(2022, time.March, 14, 0, 0, 0, 0, time.Local)
due := GetNextTrueUpReviewDueDate(now)
assert.Equal(t, trueUpReviewDueDay, due.Day())
// On the 15th
now = time.Date(2022, time.December, 15, 0, 0, 0, 0, time.Local)
due = GetNextTrueUpReviewDueDate(now)
assert.Equal(t, trueUpReviewDueDay, due.Day())
// After the 15th
now = time.Date(2022, time.September, 16, 0, 0, 0, 0, time.Local)
due = GetNextTrueUpReviewDueDate(now)
assert.Equal(t, trueUpReviewDueDay, due.Day())
})
t.Run("Due date will always be in next quarter if the current date is past the 15th", func(t *testing.T) {
now := time.Date(2022, time.March, 16, 0, 0, 0, 0, time.Local)
due := GetNextTrueUpReviewDueDate(now)
assert.Equal(t, time.April, due.Month())
now = time.Date(2022, time.June, 16, 0, 0, 0, 0, time.Local)
due = GetNextTrueUpReviewDueDate(now)
assert.Equal(t, time.July, due.Month())
now = time.Date(2022, time.September, 16, 0, 0, 0, 0, time.Local)
due = GetNextTrueUpReviewDueDate(now)
assert.Equal(t, time.October, due.Month())
now = time.Date(2022, time.December, 16, 0, 0, 0, 0, time.Local)
due = GetNextTrueUpReviewDueDate(now)
assert.Equal(t, time.January, due.Month())
})
t.Run("Due date will always be in the current quarter if the current date is before or on the 15th", func(t *testing.T) {
now := time.Date(2022, time.April, 15, 0, 0, 0, 0, time.Local)
due := GetNextTrueUpReviewDueDate(now)
assert.Equal(t, time.April, due.Month())
now = time.Date(2022, time.July, 15, 0, 0, 0, 0, time.Local)
due = GetNextTrueUpReviewDueDate(now)
assert.Equal(t, time.July, due.Month())
now = time.Date(2022, time.October, 14, 0, 0, 0, 0, time.Local)
due = GetNextTrueUpReviewDueDate(now)
assert.Equal(t, time.October, due.Month())
now = time.Date(2022, time.January, 14, 0, 0, 0, 0, time.Local)
due = GetNextTrueUpReviewDueDate(now)
assert.Equal(t, time.January, due.Month())
})
t.Run("Due date will be in the next year if the next quarter is not within the current year", func(t *testing.T) {
now := time.Date(2022, time.October, 21, 0, 0, 0, 0, time.Local)
due := GetNextTrueUpReviewDueDate(now)
assert.Equal(t, time.January, due.Month())
assert.Equal(t, 2023, due.Year())
})
}
func TestIsTrueUpReviewDueDateWithinTheNext15Days(t *testing.T) {
t.Run("Ensure a date within 30 days before the due date returns true", func(t *testing.T) {
// 1 Day before the due date
now := time.Date(2022, time.March, 16, 0, 0, 0, 0, time.Local)
// Due date is December 15th, 2022
due := GetNextTrueUpReviewDueDate(now)
res := IsTrueUpReviewDueDateWithinTheNext30Days(now, due)
assert.True(t, res)
})
t.Run("Ensure a date that is more than two weeks before the due date returns false", func(t *testing.T) {
// 15 Days before the due date
now := time.Date(2022, time.October, 16, 0, 0, 0, 0, time.Local)
// Due date is December 15th, 2022
due := GetNextTrueUpReviewDueDate(now)
res := IsTrueUpReviewDueDateWithinTheNext30Days(now, due)
assert.False(t, res)
})
t.Run("Ensure a date that is past the due date returns false", func(t *testing.T) {
now := time.Date(2022, time.April, 15, 0, 0, 0, 0, time.Local)
// Due date is April 16th, 2022
dueNow := time.Date(2022, time.April, 16, 0, 0, 0, 0, time.Local)
due := GetNextTrueUpReviewDueDate(dueNow)
res := IsTrueUpReviewDueDateWithinTheNext30Days(now, due)
assert.False(t, res)
})
t.Run("Ensure a date that is on the due date returns true", func(t *testing.T) {
now := time.Date(2022, time.January, 15, 0, 0, 0, 0, time.Local)
due := GetNextTrueUpReviewDueDate(now)
fmt.Printf("\n\ndue date: %s\n\n", due.Format("2006-Jan-02"))
fmt.Printf("\n\nnow: %s\n\n", now.Format("2006-Jan-02"))
res := IsTrueUpReviewDueDateWithinTheNext30Days(now, due)
assert.True(t, res)
})
t.Run("Ensure a date that is on the first day of the due date window returns true", func(t *testing.T) {
now := time.Date(2022, time.December, 16, 0, 0, 0, 0, time.Local)
due := GetNextTrueUpReviewDueDate(now)
res := IsTrueUpReviewDueDateWithinTheNext30Days(now, due)
assert.True(t, res)
})
}

19
server/channels/utils/urlencode.go Обычный файл
Просмотреть файл

@@ -0,0 +1,19 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"net/url"
"strings"
)
func URLEncode(str string) string {
strs := strings.Split(str, " ")
for i, s := range strs {
strs[i] = url.QueryEscape(s)
}
return strings.Join(strs, "%20")
}

28
server/channels/utils/urlencode_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,28 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestURLEncode(t *testing.T) {
toEncode := "testing 1 2 3"
encoded := URLEncode(toEncode)
require.Equal(t, encoded, "testing%201%202%203")
toEncode = "testing123"
encoded = URLEncode(toEncode)
require.Equal(t, encoded, "testing123")
toEncode = "testing$#~123"
encoded = URLEncode(toEncode)
require.Equal(t, encoded, "testing%24%23~123")
}

263
server/channels/utils/utils.go Обычный файл
Просмотреть файл

@@ -0,0 +1,263 @@
// 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/v6/model"
)
func StringInSlice(a string, slice []string) bool {
for _, b := range slice {
if b == a {
return true
}
}
return false
}
// 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 !StringInSlice(item, strings) {
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 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 != "" {
return address
}
}
if address == "" {
address, _, _ = net.SplitHostPort(r.RemoteAddr)
}
return address
}
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
}
// 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 min(a, b int) int {
if a < b {
return a
}
return b
}
func max(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 := max(0, minResolution)
if n >= -9 && n <= 9 {
if resolution == 0 {
return int64(n)
}
return 0
}
zeroes := int(math.Log10(math.Abs(n)))
resolution = min(zeroes, resolution)
tens := int64(math.Pow10(resolution))
significantDigits := int64(n) / tens
return significantDigits * tens
}

378
server/channels/utils/utils_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,378 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestStringArrayIntersection(t *testing.T) {
a := []string{
"abc",
"def",
"ghi",
}
b := []string{
"jkl",
}
c := []string{
"def",
}
assert.Empty(t, StringArrayIntersection(a, b))
assert.Len(t, StringArrayIntersection(a, c), 1)
}
func TestRemoveDuplicatesFromStringArray(t *testing.T) {
a := []string{
"a",
"b",
"a",
"a",
"b",
"c",
"a",
}
assert.Len(t, RemoveDuplicatesFromStringArray(a), 3)
}
func TestStringSliceDiff(t *testing.T) {
a := []string{"one", "two", "three", "four", "five", "six"}
b := []string{"two", "seven", "four", "six"}
expected := []string{"one", "three", "five"}
assert.Equal(t, expected, StringSliceDiff(a, b))
}
func TestGetIPAddress(t *testing.T) {
// Test with a single IP in the X-Forwarded-For
httpRequest1 := http.Request{
Header: http.Header{
"X-Forwarded-For": []string{"10.0.0.1"},
"X-Real-Ip": []string{"10.1.0.1"},
},
RemoteAddr: "10.2.0.1:12345",
}
assert.Equal(t, "10.0.0.1", GetIPAddress(&httpRequest1, []string{"X-Forwarded-For"}))
// Test with multiple IPs in the X-Forwarded-For
httpRequest2 := http.Request{
Header: http.Header{
"X-Forwarded-For": []string{"10.0.0.1, 10.0.0.2, 10.0.0.3"},
"X-Real-Ip": []string{"10.1.0.1"},
},
RemoteAddr: "10.2.0.1:12345",
}
assert.Equal(t, "10.0.0.1", GetIPAddress(&httpRequest2, []string{"X-Forwarded-For"}))
// Test with an empty X-Forwarded-For
httpRequest3 := http.Request{
Header: http.Header{
"X-Forwarded-For": []string{""},
"X-Real-Ip": []string{"10.1.0.1"},
},
RemoteAddr: "10.2.0.1:12345",
}
assert.Equal(t, "10.1.0.1", GetIPAddress(&httpRequest3, []string{"X-Forwarded-For", "X-Real-Ip"}))
// Test without an X-Forwarded-For
httpRequest4 := http.Request{
Header: http.Header{
"X-Real-Ip": []string{"10.1.0.1"},
},
RemoteAddr: "10.2.0.1:12345",
}
assert.Equal(t, "10.1.0.1", GetIPAddress(&httpRequest4, []string{"X-Forwarded-For", "X-Real-Ip"}))
// Test without any headers
httpRequest5 := http.Request{
RemoteAddr: "10.2.0.1:12345",
}
assert.Equal(t, "10.2.0.1", GetIPAddress(&httpRequest5, []string{"X-Forwarded-For", "X-Real-Ip"}))
// Test with both headers, but both untrusted
httpRequest6 := http.Request{
Header: http.Header{
"X-Forwarded-For": []string{"10.3.0.1"},
"X-Real-Ip": []string{"10.1.0.1"},
},
RemoteAddr: "10.2.0.1:12345",
}
assert.Equal(t, "10.2.0.1", GetIPAddress(&httpRequest6, nil))
// Test with both headers, but only X-Real-Ip trusted
httpRequest7 := http.Request{
Header: http.Header{
"X-Forwarded-For": []string{"10.3.0.1"},
"X-Real-Ip": []string{"10.1.0.1"},
},
RemoteAddr: "10.2.0.1:12345",
}
assert.Equal(t, "10.1.0.1", GetIPAddress(&httpRequest7, []string{"X-Real-Ip"}))
// Test with X-Forwarded-For, comma separated, untrusted
httpRequest8 := http.Request{
Header: http.Header{
"X-Forwarded-For": []string{"10.3.0.1, 10.1.0.1"},
},
RemoteAddr: "10.2.0.1:12345",
}
assert.Equal(t, "10.2.0.1", GetIPAddress(&httpRequest8, nil))
// Test with X-Forwarded-For, comma separated, untrusted
httpRequest9 := http.Request{
Header: http.Header{
"X-Forwarded-For": []string{"10.3.0.1, 10.1.0.1"},
},
RemoteAddr: "10.2.0.1:12345",
}
assert.Equal(t, "10.3.0.1", GetIPAddress(&httpRequest9, []string{"X-Forwarded-For"}))
// Test with both headers, both allowed, first one in trusted used
httpRequest10 := http.Request{
Header: http.Header{
"X-Forwarded-For": []string{"10.3.0.1"},
"X-Real-Ip": []string{"10.1.0.1"},
},
RemoteAddr: "10.2.0.1:12345",
}
assert.Equal(t, "10.1.0.1", GetIPAddress(&httpRequest10, []string{"X-Real-Ip", "X-Forwarded-For"}))
// Test with multiple IPs in the X-Forwarded-For with no spaces
httpRequest11 := http.Request{
Header: http.Header{
"X-Forwarded-For": []string{"10.0.0.1,10.0.0.2,10.0.0.3"},
"X-Real-Ip": []string{"10.1.0.1"},
},
RemoteAddr: "10.2.0.1:12345",
}
assert.Equal(t, "10.0.0.1", GetIPAddress(&httpRequest11, []string{"X-Forwarded-For"}))
}
func TestRemoveStringFromSlice(t *testing.T) {
a := []string{"one", "two", "three", "four", "five", "six"}
expected := []string{"one", "two", "three", "five", "six"}
assert.Equal(t, RemoveStringFromSlice("four", a), expected)
}
func TestAppendQueryParamsToURL(t *testing.T) {
url := "mattermost://callback"
redirectURL := AppendQueryParamsToURL(url, map[string]string{
"key1": "value1",
"key2": "value2",
})
expected := url + "?key1=value1&key2=value2"
assert.Equal(t, redirectURL, expected)
}
func TestRoundOffToZeroes(t *testing.T) {
testCases := []struct {
desc string
n float64
expected int64
}{
{
desc: "returns 0 when n is 0",
n: 0,
expected: 0,
},
{
desc: "returns 0 when n is 9",
n: 9,
expected: 0,
},
{
desc: "returns 10 when n is 10",
n: 10,
expected: 10,
},
{
desc: "returns 90 when n is 99",
n: 99,
expected: 90,
},
{
desc: "returns 100 when n is 100",
n: 100,
expected: 100,
},
{
desc: "returns 100 when n is 101",
n: 101,
expected: 100,
},
{
desc: "returns 4000 when n is 4321",
n: 4321,
expected: 4000,
},
{
desc: "returns 0 when n is -9",
n: -9,
expected: 0,
},
{
desc: "returns -4000 when n is -4321",
n: -4321,
expected: -4000,
},
{
desc: "returns 4000 when n is 4321.235",
n: 4321.235,
expected: 4000,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.desc, func(t *testing.T) {
res := RoundOffToZeroes(tc.n)
assert.Equal(t, tc.expected, res)
})
}
}
func TestRoundOffToZeroesResolution(t *testing.T) {
messageGranularity := 3
storageGranularity := 8
testCases := []struct {
desc string
n float64
minResolution int
expected int64
}{
{
desc: "returns 0 when n is 0",
n: 0,
expected: 0,
},
{
desc: "resolution of 0 does not round",
n: 12345,
expected: 12345,
minResolution: 0,
},
{
desc: "resolution of 1 truncates to 10s: 9 -> 0",
n: 9,
expected: 0,
minResolution: 1,
},
{
desc: "resolution of 1 truncates to 10s: 10 -> 10",
n: 10,
expected: 10,
minResolution: 1,
},
{
desc: "resolution of 1 truncates to 10s: 11 -> 10",
n: 11,
expected: 10,
minResolution: 1,
},
{
desc: "resolution of 1 truncates to 10s: 19 -> 10",
n: 19,
expected: 10,
minResolution: 1,
},
{
desc: "supports message usage granularity (1000s): 9 -> 0",
n: 9,
expected: 0,
minResolution: messageGranularity,
},
{
desc: "supports message usage granularity (1000s): 123 -> 100",
n: 9,
expected: 0,
minResolution: messageGranularity,
},
{
desc: "supports message usage granularity (1000s): 1234 -> 1000",
n: 1234,
expected: 1000,
minResolution: messageGranularity,
},
{
desc: "supports message usage granularity (1000s): 1500 -> 1000",
n: 1500,
expected: 1000,
minResolution: messageGranularity,
},
{
desc: "supports file storage usage granularity (~100s of MiB): 13GiB -> 12.94GiB",
n: 13 * 1024 * 1024 * 1024,
expected: 13_900_000_000,
minResolution: storageGranularity,
},
{
desc: "supports file storage usage granularity (~100s of MiB): 10GiB -> 9.965GiB",
n: 10 * 1024 * 1024 * 1024,
expected: 10_700_000_000,
minResolution: storageGranularity,
},
{
// first number at which usage reports as in excess of 10GiB.
// Evaluates to 10299.5992MiB
// Should be close enough for notifying of being over limit.
desc: "supports file storage usage granularity (~100s of MiB): 10.0583GiB -> 10.0583GiB",
n: 10.0583 * 1024 * 1024 * 1024,
expected: 10_800_000_000,
minResolution: storageGranularity,
},
{
desc: "supports file storage usage granularity (~100s of MiB): 953.67MiB -> 953.67MiB",
n: 1_000_000_000,
expected: 1_000_000_000,
minResolution: storageGranularity,
},
{
desc: "supports file storage usage granularity (~100s of MiB): 1GiB -> 953.67MiB",
n: 1 * 1024 * 1024 * 1024,
expected: 1_000_000_000,
minResolution: storageGranularity,
},
{
desc: "supports file storage usage granularity (~100s of MiB): 1049.04MiB -> 1049.04MiB",
n: 1_100_000_000,
expected: 1_100_000_000,
minResolution: storageGranularity,
},
{
desc: "supports file storage usage granularity (smaller amounts): 104.904MiB -> 104.904MiB",
n: 100_000_000,
expected: 100_000_000,
minResolution: storageGranularity,
},
{
desc: "supports file storage usage granularity (smaller amounts): 10.4904MiB -> 10.4904MiB",
n: 10_000_000,
expected: 10_000_000,
minResolution: storageGranularity,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.desc, func(t *testing.T) {
res := RoundOffToZeroesResolution(tc.n, tc.minResolution)
assert.Equal(t, tc.expected, res)
})
}
}