Mono repo -> Master (#22553)
Combines the following repositories into one: https://github.com/mattermost/mattermost-server https://github.com/mattermost/mattermost-webapp https://github.com/mattermost/focalboard https://github.com/mattermost/mattermost-plugin-playbooks
Этот коммит содержится в:
177
server/channels/utils/api.go
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
|
||||
}
|
||||
123
server/channels/utils/fileutils/fileutils_test.go
Обычный файл
123
server/channels/utils/fileutils/fileutils_test.go
Обычный файл
@@ -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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
9
server/channels/utils/license-public-key-test.txt
Обычный файл
9
server/channels/utils/license-public-key-test.txt
Обычный файл
@@ -0,0 +1,9 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyZmShlU8Z8HdG0IWSZ8r
|
||||
tSyzyxrXkJjsFUf0Ke7bm/TLtIggRdqOcUF3XEWqQk5RGD5vuq7Rlg1zZqMEBk8N
|
||||
EZeRhkxyaZW8pLjxwuBUOnXfJew31+gsTNdKZzRjrvPumKr3EtkleuoxNdoatu4E
|
||||
HrKmR/4Yi71EqAvkhk7ZjQFuF0osSWJMEEGGCSUYQnTEqUzcZSh1BhVpkIkeu8Kk
|
||||
1wCtptODixvEujgqVe+SrE3UlZjBmPjC/CL+3cYmufpSNgcEJm2mwsdaXp2OPpfn
|
||||
a0v85XL6i9ote2P+fLZ3wX9EoioHzgdgB7arOxY50QRJO7OyCqpKFKv6lRWTXuSt
|
||||
hwIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
9
server/channels/utils/license-public-key.txt
Обычный файл
9
server/channels/utils/license-public-key.txt
Обычный файл
@@ -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
Обычный файл
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
Обычный файл
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
|
||||
13
server/channels/utils/license_public_key_test_env.go
Обычный файл
13
server/channels/utils/license_public_key_test_env.go
Обычный файл
@@ -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
Обычный файл
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
Обычный файл
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(>)`)
|
||||
|
||||
// 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 ¬ificationRenderer{}
|
||||
}
|
||||
|
||||
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
Обычный файл
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: ",
|
||||
args: "",
|
||||
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: & entity",
|
||||
args: "you & me",
|
||||
want: "you & me",
|
||||
},
|
||||
{
|
||||
name: "text: < entity",
|
||||
args: "1<2",
|
||||
want: "1<2",
|
||||
},
|
||||
{
|
||||
name: "text: > entity",
|
||||
args: "2>1",
|
||||
want: "2>1",
|
||||
},
|
||||
{
|
||||
name: "text: ' entity",
|
||||
args: "he's out",
|
||||
want: "he's out",
|
||||
},
|
||||
{
|
||||
name: "text: " 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
Обычный файл
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
Обычный файл
1695
server/channels/utils/merge_test.go
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
61
server/channels/utils/mocks/LicenseValidatorIface.go
Обычный файл
61
server/channels/utils/mocks/LicenseValidatorIface.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
|
||||
}
|
||||
542
server/channels/utils/policies-roles-mapping.json
Обычный файл
542
server/channels/utils/policies-roles-mapping.json
Обычный файл
@@ -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
Обычный файл
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
Обычный файл
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
Обычный файл
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"
|
||||
}
|
||||
`
|
||||
73
server/channels/utils/test_files_compiler.go
Обычный файл
73
server/channels/utils/test_files_compiler.go
Обычный файл
@@ -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")
|
||||
}
|
||||
33
server/channels/utils/testutils/static_config_service.go
Обычный файл
33
server/channels/utils/testutils/static_config_service.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)
|
||||
}
|
||||
80
server/channels/utils/testutils/testutils.go
Обычный файл
80
server/channels/utils/testutils/testutils.go
Обычный файл
@@ -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
Обычный файл
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: ](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̊ͯ͒",
|
||||
"& < &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\" />",
|
||||
"& < " '",
|
||||
" %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̊ͯ͒",
|
||||
"& < &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>",
|
||||
"& < " '",
|
||||
|
||||
";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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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
Обычный файл
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
Ссылка в новой задаче
Block a user