Merge branch 'master' into plugins-2

Этот коммит содержится в:
JoramWilander
2018-07-07 12:04:18 -04:00
родитель 359f12db33 9e5ec7d09d
Коммит b78b216a3c
40 изменённых файлов: 2236 добавлений и 8526 удалений

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

@@ -638,6 +638,11 @@ func deleteChannel(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
if channel.Type == model.CHANNEL_DIRECT || channel.Type == model.CHANNEL_GROUP {
c.Err = model.NewAppError("deleteChannel", "api.channel.delete_channel.type.invalid", nil, "", http.StatusBadRequest)
return
}
if channel.Type == model.CHANNEL_OPEN && !c.App.SessionHasPermissionToChannel(c.Session, channel.Id, model.PERMISSION_DELETE_PUBLIC_CHANNEL) {
c.SetPermissionError(model.PERMISSION_DELETE_PUBLIC_CHANNEL)
return

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

@@ -16,6 +16,7 @@ import (
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCreateChannel(t *testing.T) {
@@ -320,6 +321,23 @@ func TestCreateDirectChannel(t *testing.T) {
CheckNoError(t, resp)
}
func TestDeleteDirectChannel(t *testing.T) {
th := Setup().InitBasic().InitSystemAdmin()
defer th.TearDown()
Client := th.Client
user := th.BasicUser
user2 := th.BasicUser2
rgc, resp := Client.CreateDirectChannel(user.Id, user2.Id)
CheckNoError(t, resp)
CheckCreatedStatus(t, resp)
require.NotNil(t, rgc, "should have created a direct channel")
deleted, resp := Client.DeleteChannel(rgc.Id)
CheckErrorMessage(t, resp, "api.channel.delete_channel.type.invalid")
require.False(t, deleted, "should not have been able to delete direct channel.")
}
func TestCreateGroupChannel(t *testing.T) {
th := Setup().InitBasic().InitSystemAdmin()
defer th.TearDown()
@@ -392,6 +410,26 @@ func TestCreateGroupChannel(t *testing.T) {
CheckNoError(t, resp)
}
func TestDeleteGroupChannel(t *testing.T) {
th := Setup().InitBasic().InitSystemAdmin()
defer th.TearDown()
Client := th.Client
user := th.BasicUser
user2 := th.BasicUser2
user3 := th.CreateUser()
userIds := []string{user.Id, user2.Id, user3.Id}
rgc, resp := Client.CreateGroupChannel(userIds)
CheckNoError(t, resp)
CheckCreatedStatus(t, resp)
require.NotNil(t, rgc, "should have created a group channel")
deleted, resp := Client.DeleteChannel(rgc.Id)
CheckErrorMessage(t, resp, "api.channel.delete_channel.type.invalid")
require.False(t, deleted, "should not have been able to delete group channel.")
}
func TestGetChannel(t *testing.T) {
th := Setup().InitBasic().InitSystemAdmin()
defer th.TearDown()

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

@@ -4,7 +4,6 @@
package api4
import (
"io/ioutil"
"net/http"
"strconv"
"strings"
@@ -22,9 +21,6 @@ func (api *API) InitCommand() {
api.BaseRoutes.Team.Handle("/commands/autocomplete", api.ApiSessionRequired(listAutocompleteCommands)).Methods("GET")
api.BaseRoutes.Command.Handle("/regen_token", api.ApiSessionRequired(regenCommandToken)).Methods("PUT")
api.BaseRoutes.Teams.Handle("/command_test", api.ApiHandler(testCommand)).Methods("POST")
api.BaseRoutes.Teams.Handle("/command_test", api.ApiHandler(testCommand)).Methods("GET")
}
func createCommand(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -291,25 +287,3 @@ func regenCommandToken(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(model.MapToJson(resp)))
}
func testCommand(c *Context, w http.ResponseWriter, r *http.Request) {
r.ParseForm()
msg := ""
if r.Method == "POST" {
msg = msg + "\ntoken=" + r.FormValue("token")
msg = msg + "\nteam_domain=" + r.FormValue("team_domain")
} else {
body, _ := ioutil.ReadAll(r.Body)
msg = string(body)
}
rc := &model.CommandResponse{
Text: "test command response " + msg,
ResponseType: model.COMMAND_RESPONSE_TYPE_IN_CHANNEL,
Type: "custom_test",
Props: map[string]interface{}{"someprop": "somevalue"},
}
w.Write([]byte(rc.ToJson()))
}

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

@@ -4,7 +4,6 @@
package api4
import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
@@ -423,7 +422,7 @@ func TestExecuteInvalidCommand(t *testing.T) {
getCmd := &model.Command{
CreatorId: th.BasicUser.Id,
TeamId: th.BasicTeam.Id,
URL: fmt.Sprintf("%s/%s/teams/command_test", ts.URL, model.API_URL_SUFFIX_V4),
URL: ts.URL,
Method: model.COMMAND_METHOD_GET,
Trigger: "getcommand",
}
@@ -501,7 +500,7 @@ func TestExecuteGetCommand(t *testing.T) {
getCmd := &model.Command{
CreatorId: th.BasicUser.Id,
TeamId: th.BasicTeam.Id,
URL: fmt.Sprintf("%s/%s/teams/command_test", ts.URL, model.API_URL_SUFFIX_V4),
URL: ts.URL,
Method: model.COMMAND_METHOD_GET,
Trigger: "getcommand",
Token: token,
@@ -556,16 +555,16 @@ func TestExecutePostCommand(t *testing.T) {
}))
defer ts.Close()
getCmd := &model.Command{
postCmd := &model.Command{
CreatorId: th.BasicUser.Id,
TeamId: th.BasicTeam.Id,
URL: fmt.Sprintf("%s/%s/teams/command_test", ts.URL, model.API_URL_SUFFIX_V4),
URL: ts.URL,
Method: model.COMMAND_METHOD_POST,
Trigger: "postcommand",
Token: token,
}
if _, err := th.App.CreateCommand(getCmd); err != nil {
if _, err := th.App.CreateCommand(postCmd); err != nil {
t.Fatal("failed to create get command")
}
@@ -592,14 +591,29 @@ func TestExecuteCommandAgainstChannelOnAnotherTeam(t *testing.T) {
})
}()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCommands = true })
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost" })
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost 127.0.0.1"
})
expectedCommandResponse := &model.CommandResponse{
Text: "test post command response",
ResponseType: model.COMMAND_RESPONSE_TYPE_IN_CHANNEL,
Type: "custom_test",
Props: map[string]interface{}{"someprop": "somevalue"},
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(expectedCommandResponse.ToJson()))
}))
defer ts.Close()
// create a slash command on some other team where we have permission to do so
team2 := th.CreateTeam()
postCmd := &model.Command{
CreatorId: th.BasicUser.Id,
TeamId: team2.Id,
URL: fmt.Sprintf("http://localhost:%v", th.App.Srv.ListenAddr.Port) + model.API_URL_SUFFIX_V4 + "/teams/command_test",
URL: ts.URL,
Method: model.COMMAND_METHOD_POST,
Trigger: "postcommand",
}
@@ -627,14 +641,29 @@ func TestExecuteCommandAgainstChannelUserIsNotIn(t *testing.T) {
})
}()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCommands = true })
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost" })
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost 127.0.0.1"
})
expectedCommandResponse := &model.CommandResponse{
Text: "test post command response",
ResponseType: model.COMMAND_RESPONSE_TYPE_IN_CHANNEL,
Type: "custom_test",
Props: map[string]interface{}{"someprop": "somevalue"},
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(expectedCommandResponse.ToJson()))
}))
defer ts.Close()
// create a slash command on some other team where we have permission to do so
team2 := th.CreateTeam()
postCmd := &model.Command{
CreatorId: th.BasicUser.Id,
TeamId: team2.Id,
URL: fmt.Sprintf("http://localhost:%v", th.App.Srv.ListenAddr.Port) + model.API_URL_SUFFIX_V4 + "/teams/command_test",
URL: ts.URL,
Method: model.COMMAND_METHOD_POST,
Trigger: "postcommand",
}
@@ -667,14 +696,32 @@ func TestExecuteCommandInDirectMessageChannel(t *testing.T) {
})
}()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCommands = true })
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost" })
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost 127.0.0.1"
})
// create a team that the user isn't a part of
team2 := th.CreateTeam()
expectedCommandResponse := &model.CommandResponse{
Text: "test post command response",
ResponseType: model.COMMAND_RESPONSE_TYPE_IN_CHANNEL,
Type: "custom_test",
Props: map[string]interface{}{"someprop": "somevalue"},
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodPost, r.Method)
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(expectedCommandResponse.ToJson()))
}))
defer ts.Close()
// create a slash command on some other team where we have permission to do so
team2 := th.CreateTeam()
postCmd := &model.Command{
CreatorId: th.BasicUser.Id,
TeamId: team2.Id,
URL: fmt.Sprintf("http://localhost:%v", th.App.Srv.ListenAddr.Port) + model.API_URL_SUFFIX_V4 + "/teams/command_test",
URL: ts.URL,
Method: model.COMMAND_METHOD_POST,
Trigger: "postcommand",
}
@@ -709,16 +756,35 @@ func TestExecuteCommandInTeamUserIsNotOn(t *testing.T) {
})
}()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCommands = true })
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost" })
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost 127.0.0.1"
})
// create a team that the user isn't a part of
team2 := th.CreateTeam()
expectedCommandResponse := &model.CommandResponse{
Text: "test post command response",
ResponseType: model.COMMAND_RESPONSE_TYPE_IN_CHANNEL,
Type: "custom_test",
Props: map[string]interface{}{"someprop": "somevalue"},
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodPost, r.Method)
r.ParseForm()
require.Equal(t, team2.Name, r.FormValue("team_domain"))
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(expectedCommandResponse.ToJson()))
}))
defer ts.Close()
// create a slash command on that team
postCmd := &model.Command{
CreatorId: th.BasicUser.Id,
TeamId: team2.Id,
URL: fmt.Sprintf("http://localhost:%v", th.App.Srv.ListenAddr.Port) + model.API_URL_SUFFIX_V4 + "/teams/command_test",
URL: ts.URL,
Method: model.COMMAND_METHOD_POST,
Trigger: "postcommand",
}

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

@@ -182,7 +182,6 @@ func New(options ...Option) (outApp *App, outErr error) {
})
})
app.regenerateClientConfig()
mlog.Info("Server is initializing...")
@@ -205,6 +204,9 @@ func New(options ...Option) (outApp *App, outErr error) {
return nil, errors.Wrapf(err, "unable to ensure asymmetric signing key")
}
app.EnsureDiagnosticId()
app.regenerateClientConfig()
app.initJobs()
app.AddLicenseListener(func() {
app.initJobs()

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

@@ -208,6 +208,9 @@ func (a *App) trackConfig() {
"enable_user_access_tokens": *cfg.ServiceSettings.EnableUserAccessTokens,
"enable_custom_emoji": *cfg.ServiceSettings.EnableCustomEmoji,
"enable_emoji_picker": *cfg.ServiceSettings.EnableEmojiPicker,
"enable_gif_picker": *cfg.ServiceSettings.EnableGifPicker,
"gfycat_api_key": isDefault(*cfg.ServiceSettings.GfycatApiKey, model.SERVICE_SETTINGS_DEFAULT_GFYCAT_API_KEY),
"gfycat_api_secret": isDefault(*cfg.ServiceSettings.GfycatApiSecret, model.SERVICE_SETTINGS_DEFAULT_GFYCAT_API_SECRET),
"experimental_enable_authentication_transfer": *cfg.ServiceSettings.ExperimentalEnableAuthenticationTransfer,
"restrict_custom_emoji_creation": *cfg.ServiceSettings.RestrictCustomEmojiCreation,
"enable_testing": cfg.ServiceSettings.EnableTesting,

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

@@ -33,6 +33,7 @@ type LineImportData struct {
Post *PostImportData `json:"post"`
DirectChannel *DirectChannelImportData `json:"direct_channel"`
DirectPost *DirectPostImportData `json:"direct_post"`
Emoji *EmojiImportData `json:"emoji"`
Version *int `json:"version"`
}
@@ -114,6 +115,11 @@ type UserChannelNotifyPropsImportData struct {
MarkUnread *string `json:"mark_unread"`
}
type EmojiImportData struct {
Name *string `json:"name"`
Image *string `json:"image"`
}
type ReactionImportData struct {
User *string `json:"user"`
CreateAt *int64 `json:"create_at"`
@@ -337,6 +343,12 @@ func (a *App) ImportLine(line LineImportData, dryRun bool) *model.AppError {
} else {
return a.ImportDirectPost(line.DirectPost, dryRun)
}
case line.Type == "emoji":
if line.Emoji == nil {
return model.NewAppError("BulkImport", "app.import.import_line.null_emoji.error", nil, "", http.StatusBadRequest)
} else {
return a.ImportEmoji(line.Emoji, dryRun)
}
default:
return model.NewAppError("BulkImport", "app.import.import_line.unknown_line_type.error", map[string]interface{}{"Type": line.Type}, "", http.StatusBadRequest)
}
@@ -1925,6 +1937,71 @@ func validateDirectPostImportData(data *DirectPostImportData, maxPostSize int) *
return nil
}
func (a *App) ImportEmoji(data *EmojiImportData, dryRun bool) *model.AppError {
if err := validateEmojiImportData(data); err != nil {
return err
}
// If this is a Dry Run, do not continue any further.
if dryRun {
return nil
}
var emoji *model.Emoji
var err *model.AppError
emoji, err = a.GetEmojiByName(*data.Name)
if err != nil && err.StatusCode != http.StatusNotFound {
return err
}
alreadyExists := emoji != nil
if !alreadyExists {
emoji = &model.Emoji{
Name: *data.Name,
}
emoji.PreSave()
}
file, fileErr := os.Open(*data.Image)
if fileErr != nil {
return model.NewAppError("BulkImport", "app.import.emoji.bad_file.error", map[string]interface{}{"EmojiName": *data.Name}, "", http.StatusBadRequest)
}
if _, err := a.WriteFile(file, getEmojiImagePath(emoji.Id)); err != nil {
return err
}
if !alreadyExists {
if result := <-a.Srv.Store.Emoji().Save(emoji); result.Err != nil {
return result.Err
}
}
return nil
}
func validateEmojiImportData(data *EmojiImportData) *model.AppError {
if data == nil {
return model.NewAppError("BulkImport", "app.import.validate_emoji_import_data.empty.error", nil, "", http.StatusBadRequest)
}
if data.Name == nil || len(*data.Name) == 0 {
return model.NewAppError("BulkImport", "app.import.validate_emoji_import_data.name_missing.error", nil, "", http.StatusBadRequest)
}
if err := model.IsValidEmojiName(*data.Name); err != nil {
return err
}
if data.Image == nil || len(*data.Image) == 0 {
return model.NewAppError("BulkImport", "app.import.validate_emoji_import_data.image_missing.error", nil, "", http.StatusBadRequest)
}
return nil
}
//
// -- Old SlackImport Functions --
// Import functions are sutible for entering posts and users into the database without

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

@@ -3774,11 +3774,16 @@ func TestImportBulkImport(t *testing.T) {
th := Setup()
defer th.TearDown()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = true })
teamName := model.NewId()
channelName := model.NewId()
username := model.NewId()
username2 := model.NewId()
username3 := model.NewId()
emojiName := model.NewId()
testsDir, _ := utils.FindDir("tests")
testImage := filepath.Join(testsDir, "test.png")
// Run bulk import with a valid 1 of everything.
data1 := `{"type": "version", "version": 1}
@@ -3791,7 +3796,8 @@ func TestImportBulkImport(t *testing.T) {
{"type": "direct_channel", "direct_channel": {"members": ["` + username + `", "` + username2 + `"]}}
{"type": "direct_channel", "direct_channel": {"members": ["` + username + `", "` + username2 + `", "` + username3 + `"]}}
{"type": "direct_post", "direct_post": {"channel_members": ["` + username + `", "` + username2 + `"], "user": "` + username + `", "message": "Hello Direct Channel", "create_at": 123456789013}}
{"type": "direct_post", "direct_post": {"channel_members": ["` + username + `", "` + username2 + `", "` + username3 + `"], "user": "` + username + `", "message": "Hello Group Channel", "create_at": 123456789014}}`
{"type": "direct_post", "direct_post": {"channel_members": ["` + username + `", "` + username2 + `", "` + username3 + `"], "user": "` + username + `", "message": "Hello Group Channel", "create_at": 123456789014}}
{"type": "emoji", "emoji": {"name": "` + emojiName + `", "image": "` + testImage + `"}}`
if err, line := th.App.BulkImport(strings.NewReader(data1), false, 2); err != nil || line != 0 {
t.Fatalf("BulkImport should have succeeded: %v, %v", err.Error(), line)
@@ -3833,3 +3839,75 @@ func TestImportProcessImportDataFileVersionLine(t *testing.T) {
t.Fatalf("Expected error on invalid version line.")
}
}
func TestImportValidateEmojiImportData(t *testing.T) {
data := EmojiImportData{
Name: ptrStr("parrot"),
Image: ptrStr("/path/to/image"),
}
err := validateEmojiImportData(&data)
assert.Nil(t, err, "Validation should succeed")
*data.Name = "smiley"
err = validateEmojiImportData(&data)
assert.NotNil(t, err)
*data.Name = ""
err = validateEmojiImportData(&data)
assert.NotNil(t, err)
*data.Name = ""
*data.Image = ""
err = validateEmojiImportData(&data)
assert.NotNil(t, err)
*data.Image = "/path/to/image"
data.Name = nil
err = validateEmojiImportData(&data)
assert.NotNil(t, err)
data.Name = ptrStr("parrot")
data.Image = nil
err = validateEmojiImportData(&data)
assert.NotNil(t, err)
}
func TestImportImportEmoji(t *testing.T) {
th := Setup()
defer th.TearDown()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = true })
testsDir, _ := utils.FindDir("tests")
testImage := filepath.Join(testsDir, "test.png")
data := EmojiImportData{Name: ptrStr(model.NewId())}
err := th.App.ImportEmoji(&data, true)
assert.NotNil(t, err, "Invalid emoji should have failed dry run")
result := <-th.App.Srv.Store.Emoji().GetByName(*data.Name)
assert.Nil(t, result.Data, "Emoji should not have been imported")
data.Image = ptrStr(testImage)
err = th.App.ImportEmoji(&data, true)
assert.Nil(t, err, "Valid emoji should have passed dry run")
data = EmojiImportData{Name: ptrStr(model.NewId())}
err = th.App.ImportEmoji(&data, false)
assert.NotNil(t, err, "Invalid emoji should have failed apply mode")
data.Image = ptrStr("non-existent-file")
err = th.App.ImportEmoji(&data, false)
assert.NotNil(t, err, "Emoji with bad image file should have failed apply mode")
data.Image = ptrStr(testImage)
err = th.App.ImportEmoji(&data, false)
assert.Nil(t, err, "Valid emoji should have succeeded apply mode")
result = <-th.App.Srv.Store.Emoji().GetByName(*data.Name)
assert.NotNil(t, result.Data, "Emoji should have been imported")
err = th.App.ImportEmoji(&data, false)
assert.Nil(t, err, "Second run should have succeeded apply mode")
}

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

@@ -782,6 +782,11 @@ func (a *App) GetOpenGraphMetadata(requestURL string) *opengraph.OpenGraph {
makeOpenGraphURLsAbsolute(og, requestURL)
// The URL should be the link the user provided in their message, not a redirected one.
if og.URL != "" {
og.URL = requestURL
}
return og
}

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

@@ -24,6 +24,7 @@ import (
"github.com/disintegration/imaging"
"github.com/golang/freetype"
"github.com/golang/freetype/truetype"
"github.com/mattermost/mattermost-server/einterfaces"
"github.com/mattermost/mattermost-server/mlog"
"github.com/mattermost/mattermost-server/model"
@@ -696,12 +697,7 @@ func CreateProfileImage(username string, userId string, initialFont string) ([]b
initial := string(strings.ToUpper(username)[0])
fontDir, _ := utils.FindDir("fonts")
fontBytes, err := ioutil.ReadFile(filepath.Join(fontDir, initialFont))
if err != nil {
return nil, model.NewAppError("CreateProfileImage", "api.user.create_profile_image.default_font.app_error", nil, err.Error(), http.StatusInternalServerError)
}
font, err := freetype.ParseFont(fontBytes)
font, err := getFont(initialFont)
if err != nil {
return nil, model.NewAppError("CreateProfileImage", "api.user.create_profile_image.default_font.app_error", nil, err.Error(), http.StatusInternalServerError)
}
@@ -719,7 +715,7 @@ func CreateProfileImage(username string, userId string, initialFont string) ([]b
c.SetDst(dstImg)
c.SetSrc(srcImg)
pt := freetype.Pt(IMAGE_PROFILE_PIXEL_DIMENSION/6, IMAGE_PROFILE_PIXEL_DIMENSION*2/3)
pt := freetype.Pt(IMAGE_PROFILE_PIXEL_DIMENSION/5, IMAGE_PROFILE_PIXEL_DIMENSION*2/3)
_, err = c.DrawString(initial, pt)
if err != nil {
return nil, model.NewAppError("CreateProfileImage", "api.user.create_profile_image.initial.app_error", nil, err.Error(), http.StatusInternalServerError)
@@ -734,6 +730,21 @@ func CreateProfileImage(username string, userId string, initialFont string) ([]b
}
}
func getFont(initialFont string) (*truetype.Font, error) {
// Some people have the old default font still set, so just treat that as if they're using the new default
if initialFont == "luximbi.ttf" {
initialFont = "nunito-bold.ttf"
}
fontDir, _ := utils.FindDir("fonts")
fontBytes, err := ioutil.ReadFile(filepath.Join(fontDir, initialFont))
if err != nil {
return nil, err
}
return freetype.ParseFont(fontBytes)
}
func (a *App) GetProfileImage(user *model.User) ([]byte, bool, *model.AppError) {
var img []byte
readFailed := false

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

@@ -97,7 +97,7 @@ func TestCreateOAuthUser(t *testing.T) {
}
func TestCreateProfileImage(t *testing.T) {
b, err := CreateProfileImage("Corey Hulen", "eo1zkdr96pdj98pjmq8zy35wba", "luximbi.ttf")
b, err := CreateProfileImage("Corey Hulen", "eo1zkdr96pdj98pjmq8zy35wba", "nunito-bold.ttf")
if err != nil {
t.Fatal(err)
}

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

@@ -587,6 +587,8 @@ func (a *App) HandleIncomingWebhook(hookId string, req *model.IncomingWebhookReq
hook = result.Data.(*model.IncomingWebhook)
}
uchan := a.Srv.Store.User().Get(hook.UserId)
if len(req.Props) == 0 {
req.Props = make(model.StringInterface)
}
@@ -637,8 +639,15 @@ func (a *App) HandleIncomingWebhook(hookId string, req *model.IncomingWebhookReq
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.channel_locked.app_error", nil, "", http.StatusForbidden)
}
var user *model.User
if result := <-uchan; result.Err != nil {
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.user.app_error", nil, "err="+result.Err.Message, http.StatusForbidden)
} else {
user = result.Data.(*model.User)
}
if a.License() != nil && *a.Config().TeamSettings.ExperimentalTownSquareIsReadOnly &&
channel.Name == model.DEFAULT_CHANNEL {
channel.Name == model.DEFAULT_CHANNEL && !a.RolesGrantPermission(user.GetRoles(), model.PERMISSION_MANAGE_SYSTEM.Id) {
return model.NewAppError("HandleIncomingWebhook", "api.post.create_post.town_square_read_only", nil, "", http.StatusForbidden)
}

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

@@ -146,8 +146,6 @@ func runServer(configFileLocation string, disableConfigWatch bool, usedPlatform
manualtesting.Init(api)
}
a.EnsureDiagnosticId()
a.Go(func() {
runSecurityJob(a)
})

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

@@ -44,9 +44,9 @@
"WebserverMode": "gzip",
"EnableCustomEmoji": false,
"EnableEmojiPicker": true,
"EnableGifPicker": true,
"GfycatApiKey": "",
"GfycatApiSecret": "",
"EnableGifPicker": false,
"GfycatApiKey": "2_KtH_W5",
"GfycatApiSecret": "3wLVZPiswc3DnaiaFoLkDvB4X0IV6CpMkj4tf2inJRsBY6-FnkT08zGmppWFgeof",
"RestrictCustomEmojiCreation": "all",
"RestrictPostDelete": "all",
"AllowEditPost": "always",
@@ -152,7 +152,7 @@
"Directory": "./data/",
"EnablePublicLink": false,
"PublicLinkSalt": "",
"InitialFont": "luximbi.ttf",
"InitialFont": "nunito-bold.ttf",
"AmazonS3AccessKeyId": "",
"AmazonS3SecretAccessKey": "",
"AmazonS3Bucket": "",

95
fonts/OFL.txt Исполняемый файл
Просмотреть файл

@@ -0,0 +1,95 @@
Copyright 2014 The Nunito Project Authors (contact@sansoxygen.com)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
The font was downloaded from https://fonts.google.com/specimen/Nunito?selection.family=Nunito:700
Its source is available at https://github.com/google/fonts/blob/master/ofl/nunito/Nunito-Bold.ttf
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Двоичные данные
fonts/luximbi.ttf

Двоичный файл не отображается.

Двоичные данные
fonts/nunito-bold.ttf Исполняемый файл

Двоичный файл не отображается.

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

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

@@ -159,6 +159,10 @@
"id": "api.channel.delete_channel.deleted.app_error",
"translation": "The channel has been archived or deleted"
},
{
"id": "api.channel.delete_channel.type.invalid",
"translation": "Cannot delete direct or group message channels"
},
{
"id": "api.channel.join_channel.already_deleted.app_error",
"translation": "Channel is already deleted"
@@ -2362,6 +2366,22 @@
"id": "app.cluster.404.app_error",
"translation": "Cluster API endpoint not found."
},
{
"id": "app.import.emoji.bad_file.error",
"translation": "Error reading import emoji image file. Emoji with name: \"{{.EmojiName}}\""
},
{
"id": "app.import.validate_emoji_import_data.empty.error",
"translation": "Import emoji data empty."
},
{
"id": "app.import.validate_emoji_import_data.name_missing.error",
"translation": "Import emoji name field missing or blank."
},
{
"id": "app.import.validate_emoji_import_data.image_missing.error",
"translation": "Import emoji image field missing or blank."
},
{
"id": "app.import.bulk_import.file_scan.error",
"translation": "Error reading import data file."
@@ -4022,6 +4042,10 @@
"id": "model.config.is_valid.sitename_length.app_error",
"translation": "Site name must be less than or equal to {{.MaxLength}} characters."
},
{
"id": "model.config.is_valid.sql_conn_max_lifetime_milliseconds.app_error",
"translation": "Invalid connection maximum lifetime for SQL settings. Must be a non-negative number."
},
{
"id": "model.config.is_valid.sql_data_src.app_error",
"translation": "Invalid data source for SQL settings. Must be set."
@@ -4038,10 +4062,6 @@
"id": "model.config.is_valid.sql_max_conn.app_error",
"translation": "Invalid maximum open connection for SQL settings. Must be a positive number."
},
{
"id": "model.config.is_valid.sql_conn_max_lifetime_milliseconds.app_error",
"translation": "Invalid connection maximum lifetime for SQL settings. Must be a non-negative number."
},
{
"id": "model.config.is_valid.sql_query_timeout.app_error",
"translation": "Invalid query timeout for SQL settings. Must be a positive number."

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -87,6 +87,8 @@ const (
SERVICE_SETTINGS_DEFAULT_MAX_LOGIN_ATTEMPTS = 10
SERVICE_SETTINGS_DEFAULT_ALLOW_CORS_FROM = ""
SERVICE_SETTINGS_DEFAULT_LISTEN_AND_ADDRESS = ":8065"
SERVICE_SETTINGS_DEFAULT_GFYCAT_API_KEY = "2_KtH_W5"
SERVICE_SETTINGS_DEFAULT_GFYCAT_API_SECRET = "3wLVZPiswc3DnaiaFoLkDvB4X0IV6CpMkj4tf2inJRsBY6-FnkT08zGmppWFgeof"
TEAM_SETTINGS_DEFAULT_MAX_USERS_PER_TEAM = 50
TEAM_SETTINGS_DEFAULT_CUSTOM_BRAND_TEXT = ""
@@ -417,15 +419,15 @@ func (s *ServiceSettings) SetDefaults() {
}
if s.EnableGifPicker == nil {
s.EnableGifPicker = NewBool(true)
s.EnableGifPicker = NewBool(false)
}
if s.GfycatApiKey == nil {
s.GfycatApiKey = NewString("")
if s.GfycatApiKey == nil || *s.GfycatApiKey == "" {
s.GfycatApiKey = NewString(SERVICE_SETTINGS_DEFAULT_GFYCAT_API_KEY)
}
if s.GfycatApiSecret == nil {
s.GfycatApiSecret = NewString("")
if s.GfycatApiSecret == nil || *s.GfycatApiSecret == "" {
s.GfycatApiSecret = NewString(SERVICE_SETTINGS_DEFAULT_GFYCAT_API_SECRET)
}
if s.RestrictCustomEmojiCreation == nil {
@@ -772,8 +774,8 @@ func (s *FileSettings) SetDefaults() {
}
if s.InitialFont == "" {
// Defaults to "luximbi.ttf"
s.InitialFont = "luximbi.ttf"
// Defaults to "nunito-bold.ttf"
s.InitialFont = "nunito-bold.ttf"
}
if s.Directory == "" {

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

@@ -41,11 +41,15 @@ func (emoji *Emoji) IsValid() *AppError {
return NewAppError("Emoji.IsValid", "model.emoji.update_at.app_error", nil, "id="+emoji.Id, http.StatusBadRequest)
}
if len(emoji.CreatorId) != 26 {
if len(emoji.CreatorId) > 26 {
return NewAppError("Emoji.IsValid", "model.emoji.user_id.app_error", nil, "", http.StatusBadRequest)
}
if len(emoji.Name) == 0 || len(emoji.Name) > EMOJI_NAME_MAX_LENGTH || !IsValidAlphaNumHyphenUnderscore(emoji.Name, false) || inSystemEmoji(emoji.Name) {
return IsValidEmojiName(emoji.Name)
}
func IsValidEmojiName(name string) *AppError {
if len(name) == 0 || len(name) > EMOJI_NAME_MAX_LENGTH || !IsValidAlphaNumHyphenUnderscore(name, false) || inSystemEmoji(name) {
return NewAppError("Emoji.IsValid", "model.emoji.name.app_error", nil, "", http.StatusBadRequest)
}

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

@@ -40,11 +40,6 @@ func TestEmojiIsValid(t *testing.T) {
}
emoji.UpdateAt = 1234
emoji.CreatorId = strings.Repeat("1", 25)
if err := emoji.IsValid(); err == nil {
t.Fatal()
}
emoji.CreatorId = strings.Repeat("1", 27)
if err := emoji.IsValid(); err == nil {
t.Fatal()

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

@@ -21,6 +21,7 @@ type MessageExport struct {
PostCreateAt *int64
PostMessage *string
PostType *string
PostRootId *string
PostOriginalId *string
PostFileIds StringArray
}

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

@@ -13,6 +13,7 @@ import (
// It should be maintained in chronological order with most current
// release at the front of the list.
var versions = []string{
"5.1.0",
"5.0.0",
"4.10.0",
"4.9.0",

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

@@ -224,6 +224,7 @@ func (s SqlComplianceStore) MessageExport(after int64, limit int) store.StoreCha
Posts.Message AS PostMessage,
Posts.Type AS PostType,
Posts.OriginalId AS PostOriginalId,
Posts.RootId AS PostRootId,
Posts.FileIds AS PostFileIds,
Teams.Id AS TeamId,
Teams.Name AS TeamName,

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

@@ -431,6 +431,19 @@ func UpgradeDatabaseToVersion50(sqlStore SqlStore) {
// in `config.json` to a `Permission` in the database. The migration code can be seen
// in the file `app/app.go` in the function `DoEmojisPermissionsMigration()`.
// This version of Mattermost also includes a online-migration which migrates some roles from the `Roles` columns of
// TeamMember and ChannelMember rows to the new SchemeAdmin and SchemeUser columns. If you need to downgrade to a
// version of Mattermost prior to 5.0, you should take your server offline and run the following SQL statements
// prior to launching the downgraded version:
//
// UPDATE Teams SET SchemeId = NULL;
// UPDATE Channels SET SchemeId = NULL;
// UPDATE TeamMembers SET Roles = CONCAT(Roles, ' team_user'), SchemeUser = NULL where SchemeUser = 1;
// UPDATE TeamMembers SET Roles = CONCAT(Roles, ' team_admin'), SchemeAdmin = NULL where SchemeAdmin = 1;
// UPDATE ChannelMembers SET Roles = CONCAT(Roles, ' channel_user'), SchemeUser = NULL where SchemeUser = 1;
// UPDATE ChannelMembers SET Roles = CONCAT(Roles, ' channel_admin'), SchemeAdmin = NULL where SchemeAdmin = 1;
// DELETE from Systems WHERE Name = 'migration_advanced_permissions_phase_2';
if shouldPerformUpgrade(sqlStore, VERSION_4_10_0, VERSION_5_0_0) {
sqlStore.CreateColumnIfNotExistsNoDefault("Teams", "SchemeId", "varchar(26)", "varchar(26)")
@@ -453,9 +466,7 @@ func UpgradeDatabaseToVersion50(sqlStore SqlStore) {
}
func UpgradeDatabaseToVersion51(sqlStore SqlStore) {
// TODO: Uncomment following condition when version 5.1.0 is released
// if shouldPerformUpgrade(sqlStore, VERSION_5_0_0, VERSION_5_1_0) {
// saveSchemaVersion(sqlStore, VERSION_5_1_0)
// }
if shouldPerformUpgrade(sqlStore, VERSION_5_0_0, VERSION_5_1_0) {
saveSchemaVersion(sqlStore, VERSION_5_1_0)
}
}

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

@@ -37,10 +37,13 @@ func StopTestStore() {
}
type TestHelper struct {
App *app.App
BasicUser *model.User
BasicChannel *model.Channel
BasicTeam *model.Team
App *app.App
BasicUser *model.User
BasicChannel *model.Channel
BasicTeam *model.Team
SystemAdminUser *model.User
}
func Setup() *TestHelper {
@@ -77,7 +80,9 @@ func Setup() *TestHelper {
}
func (th *TestHelper) InitBasic() *TestHelper {
user, _ := th.App.CreateUser(&model.User{Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1", EmailVerified: true, Roles: model.SYSTEM_ADMIN_ROLE_ID})
th.SystemAdminUser, _ = th.App.CreateUser(&model.User{Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1", EmailVerified: true, Roles: model.SYSTEM_ADMIN_ROLE_ID})
user, _ := th.App.CreateUser(&model.User{Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1", EmailVerified: true, Roles: model.SYSTEM_USER_ROLE_ID})
team, _ := th.App.CreateTeam(&model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: user.Email, Type: model.TEAM_OPEN})

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

@@ -97,13 +97,30 @@ func TestIncomingWebhook(t *testing.T) {
assert.True(t, resp.StatusCode == http.StatusOK)
})
t.Run("WebhookExperimentReadOnly", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.ExperimentalTownSquareIsReadOnly = false })
_, err := http.Post(url, "application/json", strings.NewReader(fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"%s\"}", model.DEFAULT_CHANNEL)))
assert.Nil(t, err, "Not read only")
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.ExperimentalTownSquareIsReadOnly = true })
t.Run("WebhookExperimentalReadOnly", func(t *testing.T) {
th.App.SetLicense(model.NewTestLicense())
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.ExperimentalTownSquareIsReadOnly = true })
// Read only default channel should fail.
resp, err := http.Post(url, "application/json", strings.NewReader(fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"%s\"}", model.DEFAULT_CHANNEL)))
require.Nil(t, err)
assert.True(t, resp.StatusCode != http.StatusOK)
// None-default channel should still work.
resp, err = http.Post(url, "application/json", strings.NewReader(fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"%s\"}", th.BasicChannel.Name)))
require.Nil(t, err)
assert.True(t, resp.StatusCode == http.StatusOK)
// System-Admin Owned Hook
adminHook, err := th.App.CreateIncomingWebhookForChannel(th.SystemAdminUser.Id, th.BasicChannel, &model.IncomingWebhook{ChannelId: th.BasicChannel.Id})
require.Nil(t, err)
adminUrl := ApiClient.Url + "/hooks/" + adminHook.Id
resp, err = http.Post(adminUrl, "application/json", strings.NewReader(fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"%s\"}", model.DEFAULT_CHANNEL)))
require.Nil(t, err)
assert.True(t, resp.StatusCode == http.StatusOK)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.ExperimentalTownSquareIsReadOnly = false })
})
t.Run("WebhookAttachments", func(t *testing.T) {