Refactor and migrate more functions out of api into app package (#5063)
Этот коммит содержится в:
коммит произвёл
Harrison Healey
родитель
97558f6a6e
Коммит
0e2b321e6f
@@ -665,7 +665,7 @@ func adminResetMfa(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := DeactivateMfa(userId); err != nil {
|
||||
if err := app.DeactivateMfa(userId); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
186
api/channel.go
186
api/channel.go
@@ -119,7 +119,7 @@ func createDirectChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if sc, err := CreateDirectChannel(c.Session.UserId, userId); err != nil {
|
||||
if sc, err := app.CreateDirectChannel(c.Session.UserId, userId); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
} else {
|
||||
@@ -127,33 +127,6 @@ func createDirectChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func CreateDirectChannel(userId string, otherUserId string) (*model.Channel, *model.AppError) {
|
||||
uc := app.Srv.Store.User().Get(otherUserId)
|
||||
|
||||
if uresult := <-uc; uresult.Err != nil {
|
||||
return nil, model.NewLocAppError("CreateDirectChannel", "api.channel.create_direct_channel.invalid_user.app_error", nil, otherUserId)
|
||||
}
|
||||
|
||||
if result := <-app.Srv.Store.Channel().CreateDirectChannel(userId, otherUserId); result.Err != nil {
|
||||
if result.Err.Id == store.CHANNEL_EXISTS_ERROR {
|
||||
return result.Data.(*model.Channel), nil
|
||||
} else {
|
||||
return nil, result.Err
|
||||
}
|
||||
} else {
|
||||
channel := result.Data.(*model.Channel)
|
||||
|
||||
app.InvalidateCacheForUser(userId)
|
||||
app.InvalidateCacheForUser(otherUserId)
|
||||
|
||||
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_DIRECT_ADDED, "", channel.Id, "", nil)
|
||||
message.Add("teammate_id", otherUserId)
|
||||
go app.Publish(message)
|
||||
|
||||
return channel, nil
|
||||
}
|
||||
}
|
||||
|
||||
func CanManageChannel(c *Context, channel *model.Channel) bool {
|
||||
if channel.Type == model.CHANNEL_OPEN && !HasPermissionToChannelContext(c, channel.Id, model.PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES) {
|
||||
return false
|
||||
@@ -229,7 +202,9 @@ func updateChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
} else {
|
||||
if oldChannelDisplayName != channel.DisplayName {
|
||||
go PostUpdateChannelDisplayNameMessage(c, channel.Id, oldChannelDisplayName, channel.DisplayName)
|
||||
if err := app.PostUpdateChannelDisplayNameMessage(c.Session.UserId, channel.Id, c.TeamId, oldChannelDisplayName, channel.DisplayName); err != nil {
|
||||
l4g.Error(err.Error())
|
||||
}
|
||||
}
|
||||
c.LogAudit("name=" + channel.Name)
|
||||
w.Write([]byte(oldChannel.ToJson()))
|
||||
@@ -277,76 +252,15 @@ func updateChannelHeader(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.Err = ucresult.Err
|
||||
return
|
||||
} else {
|
||||
go PostUpdateChannelHeaderMessage(c, channel.Id, oldChannelHeader, channelHeader)
|
||||
if err := app.PostUpdateChannelHeaderMessage(c.Session.UserId, channel.Id, c.TeamId, oldChannelHeader, channelHeader); err != nil {
|
||||
l4g.Error(err.Error())
|
||||
}
|
||||
c.LogAudit("name=" + channel.Name)
|
||||
w.Write([]byte(channel.ToJson()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func PostUpdateChannelHeaderMessage(c *Context, channelId string, oldChannelHeader, newChannelHeader string) {
|
||||
uc := app.Srv.Store.User().Get(c.Session.UserId)
|
||||
|
||||
if uresult := <-uc; uresult.Err != nil {
|
||||
l4g.Error(utils.T("api.channel.post_update_channel_header_message_and_forget.retrieve_user.error"), uresult.Err)
|
||||
return
|
||||
} else {
|
||||
user := uresult.Data.(*model.User)
|
||||
|
||||
var message string
|
||||
if oldChannelHeader == "" {
|
||||
message = fmt.Sprintf(utils.T("api.channel.post_update_channel_header_message_and_forget.updated_to"), user.Username, newChannelHeader)
|
||||
} else if newChannelHeader == "" {
|
||||
message = fmt.Sprintf(utils.T("api.channel.post_update_channel_header_message_and_forget.removed"), user.Username, oldChannelHeader)
|
||||
} else {
|
||||
message = fmt.Sprintf(utils.T("api.channel.post_update_channel_header_message_and_forget.updated_from"), user.Username, oldChannelHeader, newChannelHeader)
|
||||
}
|
||||
|
||||
post := &model.Post{
|
||||
ChannelId: channelId,
|
||||
Message: message,
|
||||
Type: model.POST_HEADER_CHANGE,
|
||||
UserId: c.Session.UserId,
|
||||
Props: model.StringInterface{
|
||||
"old_header": oldChannelHeader,
|
||||
"new_header": newChannelHeader,
|
||||
},
|
||||
}
|
||||
|
||||
if _, err := app.CreatePost(post, c.TeamId, false); err != nil {
|
||||
l4g.Error(utils.T("api.channel.post_update_channel_header_message_and_forget.join_leave.error"), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func PostUpdateChannelDisplayNameMessage(c *Context, channelId string, oldChannelDisplayName, newChannelDisplayName string) {
|
||||
uc := app.Srv.Store.User().Get(c.Session.UserId)
|
||||
|
||||
if uresult := <-uc; uresult.Err != nil {
|
||||
l4g.Error(utils.T("api.channel.post_update_channel_displayname_message_and_forget.retrieve_user.error"), uresult.Err)
|
||||
return
|
||||
} else {
|
||||
user := uresult.Data.(*model.User)
|
||||
|
||||
message := fmt.Sprintf(utils.T("api.channel.post_update_channel_displayname_message_and_forget.updated_from"), user.Username, oldChannelDisplayName, newChannelDisplayName)
|
||||
|
||||
post := &model.Post{
|
||||
ChannelId: channelId,
|
||||
Message: message,
|
||||
Type: model.POST_DISPLAYNAME_CHANGE,
|
||||
UserId: c.Session.UserId,
|
||||
Props: model.StringInterface{
|
||||
"old_displayname": oldChannelDisplayName,
|
||||
"new_displayname": newChannelDisplayName,
|
||||
},
|
||||
}
|
||||
|
||||
if _, err := app.CreatePost(post, c.TeamId, false); err != nil {
|
||||
l4g.Error(utils.T("api.channel.post_update_channel_displayname_message_and_forget.create_post.error"), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateChannelPurpose(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
props := model.MapFromJson(r.Body)
|
||||
channelId := props["channel_id"]
|
||||
@@ -473,84 +387,34 @@ func join(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
channelId := params["channel_id"]
|
||||
channelName := params["channel_name"]
|
||||
|
||||
var outChannel *model.Channel = nil
|
||||
var channel *model.Channel
|
||||
var err *model.AppError
|
||||
if channelId != "" {
|
||||
if err, channel := JoinChannelById(c, c.Session.UserId, channelId); err != nil {
|
||||
c.Err = err
|
||||
c.Err.StatusCode = http.StatusForbidden
|
||||
return
|
||||
} else {
|
||||
outChannel = channel
|
||||
}
|
||||
channel, err = app.GetChannel(channelId)
|
||||
} else if channelName != "" {
|
||||
if err, channel := JoinChannelByName(c, c.Session.UserId, c.TeamId, channelName); err != nil {
|
||||
c.Err = err
|
||||
c.Err.StatusCode = http.StatusForbidden
|
||||
return
|
||||
} else {
|
||||
outChannel = channel
|
||||
}
|
||||
channel, err = app.GetChannelByName(channelName, c.TeamId)
|
||||
} else {
|
||||
c.SetInvalidParam("join", "channel_id, channel_name")
|
||||
return
|
||||
}
|
||||
w.Write([]byte(outChannel.ToJson()))
|
||||
}
|
||||
|
||||
func JoinChannelByName(c *Context, userId string, teamId string, channelName string) (*model.AppError, *model.Channel) {
|
||||
channelChannel := app.Srv.Store.Channel().GetByName(teamId, channelName)
|
||||
userChannel := app.Srv.Store.User().Get(userId)
|
||||
|
||||
return joinChannel(c, channelChannel, userChannel)
|
||||
}
|
||||
|
||||
func JoinChannelById(c *Context, userId string, channelId string) (*model.AppError, *model.Channel) {
|
||||
channelChannel := app.Srv.Store.Channel().Get(channelId, true)
|
||||
userChannel := app.Srv.Store.User().Get(userId)
|
||||
|
||||
return joinChannel(c, channelChannel, userChannel)
|
||||
}
|
||||
|
||||
func joinChannel(c *Context, channelChannel store.StoreChannel, userChannel store.StoreChannel) (*model.AppError, *model.Channel) {
|
||||
if cresult := <-channelChannel; cresult.Err != nil {
|
||||
return cresult.Err, nil
|
||||
} else if uresult := <-userChannel; uresult.Err != nil {
|
||||
return uresult.Err, nil
|
||||
} else {
|
||||
channel := cresult.Data.(*model.Channel)
|
||||
user := uresult.Data.(*model.User)
|
||||
|
||||
if mresult := <-app.Srv.Store.Channel().GetMember(channel.Id, user.Id); mresult.Err == nil && mresult.Data != nil {
|
||||
// the user is already in the channel so just return successful
|
||||
return nil, channel
|
||||
}
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if channel.Type == model.CHANNEL_OPEN {
|
||||
if !HasPermissionToTeamContext(c, channel.TeamId, model.PERMISSION_JOIN_PUBLIC_CHANNELS) {
|
||||
return c.Err, nil
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if channel.Type == model.CHANNEL_OPEN {
|
||||
if _, err := app.AddUserToChannel(user, channel); err != nil {
|
||||
return err, nil
|
||||
}
|
||||
go PostUserAddRemoveMessage(c, channel.Id, fmt.Sprintf(utils.T("api.channel.join_channel.post_and_forget"), user.Username), model.POST_JOIN_LEAVE)
|
||||
} else {
|
||||
return model.NewLocAppError("join", "api.channel.join_channel.permissions.app_error", nil, ""), nil
|
||||
}
|
||||
return nil, channel
|
||||
if err = app.JoinChannel(channel, c.Session.UserId); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func PostUserAddRemoveMessage(c *Context, channelId string, message, postType string) {
|
||||
post := &model.Post{
|
||||
ChannelId: channelId,
|
||||
Message: message,
|
||||
Type: postType,
|
||||
UserId: c.Session.UserId,
|
||||
}
|
||||
if _, err := app.CreatePost(post, c.TeamId, false); err != nil {
|
||||
l4g.Error(utils.T("api.channel.post_user_add_remove_message_and_forget.error"), err)
|
||||
}
|
||||
w.Write([]byte(channel.ToJson()))
|
||||
}
|
||||
|
||||
func leave(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
@@ -601,7 +465,7 @@ func leave(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
RemoveUserFromChannel(c.Session.UserId, c.Session.UserId, channel)
|
||||
|
||||
go PostUserAddRemoveMessage(c, channel.Id, fmt.Sprintf(utils.T("api.channel.leave.left"), user.Username), model.POST_JOIN_LEAVE)
|
||||
go app.PostUserAddRemoveMessage(c.Session.UserId, channel.Id, channel.TeamId, fmt.Sprintf(utils.T("api.channel.leave.left"), user.Username), model.POST_JOIN_LEAVE)
|
||||
|
||||
result := make(map[string]string)
|
||||
result["id"] = channel.Id
|
||||
@@ -902,7 +766,7 @@ func addMember(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
c.LogAudit("name=" + channel.Name + " user_id=" + userId)
|
||||
|
||||
go PostUserAddRemoveMessage(c, channel.Id, fmt.Sprintf(utils.T("api.channel.add_member.added"), nUser.Username, oUser.Username), model.POST_ADD_REMOVE)
|
||||
go app.PostUserAddRemoveMessage(c.Session.UserId, channel.Id, channel.TeamId, fmt.Sprintf(utils.T("api.channel.add_member.added"), nUser.Username, oUser.Username), model.POST_ADD_REMOVE)
|
||||
|
||||
<-app.Srv.Store.Channel().UpdateLastViewedAt([]string{id}, oUser.Id)
|
||||
w.Write([]byte(cm.ToJson()))
|
||||
@@ -956,7 +820,7 @@ func removeMember(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
c.LogAudit("name=" + channel.Name + " user_id=" + userIdToRemove)
|
||||
|
||||
go PostUserAddRemoveMessage(c, channel.Id, fmt.Sprintf(utils.T("api.channel.remove_member.removed"), oUser.Username), model.POST_ADD_REMOVE)
|
||||
go app.PostUserAddRemoveMessage(c.Session.UserId, channel.Id, channel.TeamId, fmt.Sprintf(utils.T("api.channel.remove_member.removed"), oUser.Username), model.POST_ADD_REMOVE)
|
||||
|
||||
result := make(map[string]string)
|
||||
result["channel_id"] = channel.Id
|
||||
|
||||
@@ -45,7 +45,7 @@ func (me *JoinProvider) DoCommand(c *Context, args *model.CommandArgs, message s
|
||||
return &model.CommandResponse{Text: c.T("api.command_join.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
|
||||
}
|
||||
|
||||
if err, _ := JoinChannelById(c, c.Session.UserId, channel.Id); err != nil {
|
||||
if err := app.JoinChannel(channel, c.Session.UserId); err != nil {
|
||||
return &model.CommandResponse{Text: c.T("api.command_join.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/mattermost/platform/app"
|
||||
"github.com/mattermost/platform/model"
|
||||
)
|
||||
|
||||
@@ -38,8 +39,7 @@ func (me *LogoutProvider) DoCommand(c *Context, args *model.CommandArgs, message
|
||||
|
||||
// We can't actually remove the user's cookie from here so we just dump their session and let the browser figure it out
|
||||
if c.Session.Id != "" {
|
||||
RevokeSessionById(c, c.Session.Id)
|
||||
if c.Err != nil {
|
||||
if err := app.RevokeSessionById(c.Session.Id); err != nil {
|
||||
return FAIL
|
||||
}
|
||||
return SUCCESS
|
||||
|
||||
@@ -66,7 +66,7 @@ func (me *msgProvider) DoCommand(c *Context, args *model.CommandArgs, message st
|
||||
targetChannelId := ""
|
||||
if channel := <-app.Srv.Store.Channel().GetByName(c.TeamId, channelName); channel.Err != nil {
|
||||
if channel.Err.Id == "store.sql_channel.get_by_name.missing.app_error" {
|
||||
if directChannel, err := CreateDirectChannel(c.Session.UserId, userProfile.Id); err != nil {
|
||||
if directChannel, err := app.CreateDirectChannel(c.Session.UserId, userProfile.Id); err != nil {
|
||||
c.Err = err
|
||||
return &model.CommandResponse{Text: c.T("api.command_msg.dm_fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
|
||||
} else {
|
||||
|
||||
@@ -469,7 +469,7 @@ func (c *Context) CheckTeamId() {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// just return because it fail on the HasPermissionToContext and the error is already on the Context c.Err
|
||||
// HasPermissionToContext automatically fills the Context error
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
10
api/emoji.go
10
api/emoji.go
@@ -163,7 +163,7 @@ func uploadEmojiImage(id string, imageData *multipart.FileHeader) *model.AppErro
|
||||
if err := gif.EncodeAll(newbuf, resized_gif); err != nil {
|
||||
return model.NewLocAppError("uploadEmojiImage", "api.emoji.upload.large_image.gif_encode_error", nil, "")
|
||||
}
|
||||
if err := WriteFile(newbuf.Bytes(), getEmojiImagePath(id)); err != nil {
|
||||
if err := app.WriteFile(newbuf.Bytes(), getEmojiImagePath(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -175,13 +175,13 @@ func uploadEmojiImage(id string, imageData *multipart.FileHeader) *model.AppErro
|
||||
if err := png.Encode(newbuf, resized_image); err != nil {
|
||||
return model.NewLocAppError("uploadEmojiImage", "api.emoji.upload.large_image.encode_error", nil, "")
|
||||
}
|
||||
if err := WriteFile(newbuf.Bytes(), getEmojiImagePath(id)); err != nil {
|
||||
if err := app.WriteFile(newbuf.Bytes(), getEmojiImagePath(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := WriteFile(buf.Bytes(), getEmojiImagePath(id)); err != nil {
|
||||
if err := app.WriteFile(buf.Bytes(), getEmojiImagePath(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -236,7 +236,7 @@ func deleteEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func deleteEmojiImage(id string) {
|
||||
if err := MoveFile(getEmojiImagePath(id), "emoji/"+id+"/image_deleted"); err != nil {
|
||||
if err := app.MoveFile(getEmojiImagePath(id), "emoji/"+id+"/image_deleted"); err != nil {
|
||||
l4g.Error("Failed to rename image when deleting emoji %v", id)
|
||||
}
|
||||
}
|
||||
@@ -275,7 +275,7 @@ func getEmojiImage(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
} else {
|
||||
var img []byte
|
||||
|
||||
if data, err := ReadFile(getEmojiImagePath(id)); err != nil {
|
||||
if data, err := app.ReadFile(getEmojiImagePath(id)); err != nil {
|
||||
c.Err = model.NewLocAppError("getEmojiImage", "api.emoji.get_image.read.app_error", nil, err.Error())
|
||||
return
|
||||
} else {
|
||||
|
||||
@@ -317,7 +317,7 @@ func createTestPng(t *testing.T, width int, height int) []byte {
|
||||
func createTestEmoji(t *testing.T, emoji *model.Emoji, imageData []byte) *model.Emoji {
|
||||
emoji = store.Must(app.Srv.Store.Emoji().Save(emoji)).(*model.Emoji)
|
||||
|
||||
if err := WriteFile(imageData, "emoji/"+emoji.Id+"/image"); err != nil {
|
||||
if err := app.WriteFile(imageData, "emoji/"+emoji.Id+"/image"); err != nil {
|
||||
store.Must(app.Srv.Store.Emoji().Delete(emoji.Id, time.Now().Unix()))
|
||||
t.Fatalf("failed to write image: %v", err.Error())
|
||||
}
|
||||
|
||||
346
api/file.go
346
api/file.go
@@ -5,24 +5,17 @@ package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
_ "image/gif"
|
||||
"image/jpeg"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
l4g "github.com/alecthomas/log4go"
|
||||
"github.com/disintegration/imaging"
|
||||
@@ -32,8 +25,6 @@ import (
|
||||
"github.com/mattermost/platform/utils"
|
||||
"github.com/rwcarlsen/goexif/exif"
|
||||
_ "golang.org/x/image/bmp"
|
||||
|
||||
s3 "github.com/minio/minio-go"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -182,7 +173,7 @@ func doUploadFile(teamId string, channelId string, userId string, rawFilename st
|
||||
info.ThumbnailPath = pathPrefix + nameWithoutExtension + "_thumb.jpg"
|
||||
}
|
||||
|
||||
if err := WriteFile(data, info.Path); err != nil {
|
||||
if err := app.WriteFile(data, info.Path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -285,7 +276,7 @@ func generateThumbnailImage(img image.Image, thumbnailPath string, width int, he
|
||||
return
|
||||
}
|
||||
|
||||
if err := WriteFile(buf.Bytes(), thumbnailPath); err != nil {
|
||||
if err := app.WriteFile(buf.Bytes(), thumbnailPath); err != nil {
|
||||
l4g.Error(utils.T("api.file.handle_images_forget.upload_thumb.error"), thumbnailPath, err)
|
||||
return
|
||||
}
|
||||
@@ -306,7 +297,7 @@ func generatePreviewImage(img image.Image, previewPath string, width int) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := WriteFile(buf.Bytes(), previewPath); err != nil {
|
||||
if err := app.WriteFile(buf.Bytes(), previewPath); err != nil {
|
||||
l4g.Error(utils.T("api.file.handle_images_forget.upload_preview.error"), previewPath, err)
|
||||
return
|
||||
}
|
||||
@@ -319,7 +310,7 @@ func getFile(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if data, err := ReadFile(info.Path); err != nil {
|
||||
if data, err := app.ReadFile(info.Path); err != nil {
|
||||
c.Err = err
|
||||
c.Err.StatusCode = http.StatusNotFound
|
||||
} else if err := writeFileResponse(info.Name, info.MimeType, data, w, r); err != nil {
|
||||
@@ -341,7 +332,7 @@ func getFileThumbnail(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if data, err := ReadFile(info.ThumbnailPath); err != nil {
|
||||
if data, err := app.ReadFile(info.ThumbnailPath); err != nil {
|
||||
c.Err = err
|
||||
c.Err.StatusCode = http.StatusNotFound
|
||||
} else if err := writeFileResponse(info.Name, "", data, w, r); err != nil {
|
||||
@@ -363,7 +354,7 @@ func getFilePreview(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if data, err := ReadFile(info.PreviewPath); err != nil {
|
||||
if data, err := app.ReadFile(info.PreviewPath); err != nil {
|
||||
c.Err = err
|
||||
c.Err.StatusCode = http.StatusNotFound
|
||||
} else if err := writeFileResponse(info.Name, "", data, w, r); err != nil {
|
||||
@@ -400,7 +391,7 @@ func getPublicFile(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
hash := r.URL.Query().Get("h")
|
||||
|
||||
if len(hash) > 0 {
|
||||
correctHash := generatePublicLinkHash(info.Id, *utils.Cfg.FileSettings.PublicLinkSalt)
|
||||
correctHash := app.GeneratePublicLinkHash(info.Id, *utils.Cfg.FileSettings.PublicLinkSalt)
|
||||
|
||||
if hash != correctHash {
|
||||
c.Err = model.NewLocAppError("getPublicFile", "api.file.get_file.public_invalid.app_error", nil, "")
|
||||
@@ -413,7 +404,7 @@ func getPublicFile(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if data, err := ReadFile(info.Path); err != nil {
|
||||
if data, err := app.ReadFile(info.Path); err != nil {
|
||||
c.Err = err
|
||||
c.Err.StatusCode = http.StatusNotFound
|
||||
} else if err := writeFileResponse(info.Name, info.MimeType, data, w, r); err != nil {
|
||||
@@ -482,7 +473,7 @@ func getPublicFileOld(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
hash := r.URL.Query().Get("h")
|
||||
|
||||
if len(hash) > 0 {
|
||||
correctHash := generatePublicLinkHash(filename, *utils.Cfg.FileSettings.PublicLinkSalt)
|
||||
correctHash := app.GeneratePublicLinkHash(filename, *utils.Cfg.FileSettings.PublicLinkSalt)
|
||||
|
||||
if hash != correctHash {
|
||||
c.Err = model.NewLocAppError("getPublicFile", "api.file.get_file.public_invalid.app_error", nil, "")
|
||||
@@ -511,7 +502,7 @@ func getPublicFileOld(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if data, err := ReadFile(info.Path); err != nil {
|
||||
if data, err := app.ReadFile(info.Path); err != nil {
|
||||
c.Err = err
|
||||
c.Err.StatusCode = http.StatusNotFound
|
||||
} else if err := writeFileResponse(info.Name, info.MimeType, data, w, r); err != nil {
|
||||
@@ -560,320 +551,5 @@ func getPublicLink(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
w.Write([]byte(model.StringToJson(generatePublicLink(c.GetSiteURL(), info))))
|
||||
}
|
||||
|
||||
func generatePublicLink(siteURL string, info *model.FileInfo) string {
|
||||
hash := generatePublicLinkHash(info.Id, *utils.Cfg.FileSettings.PublicLinkSalt)
|
||||
return fmt.Sprintf("%s%s/public/files/%v/get?h=%s", siteURL, model.API_URL_SUFFIX, info.Id, hash)
|
||||
}
|
||||
|
||||
func generatePublicLinkHash(fileId, salt string) string {
|
||||
hash := sha256.New()
|
||||
hash.Write([]byte(salt))
|
||||
hash.Write([]byte(fileId))
|
||||
|
||||
return base64.RawURLEncoding.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
|
||||
var fileMigrationLock sync.Mutex
|
||||
|
||||
// Creates and stores FileInfos for a post created before the FileInfos table existed.
|
||||
func migrateFilenamesToFileInfos(post *model.Post) []*model.FileInfo {
|
||||
if len(post.Filenames) == 0 {
|
||||
l4g.Warn(utils.T("api.file.migrate_filenames_to_file_infos.no_filenames.warn"), post.Id)
|
||||
return []*model.FileInfo{}
|
||||
}
|
||||
|
||||
cchan := app.Srv.Store.Channel().Get(post.ChannelId, true)
|
||||
|
||||
// There's a weird bug that rarely happens where a post ends up with duplicate Filenames so remove those
|
||||
filenames := utils.RemoveDuplicatesFromStringArray(post.Filenames)
|
||||
|
||||
var channel *model.Channel
|
||||
if result := <-cchan; result.Err != nil {
|
||||
l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.channel.app_error"), post.Id, post.ChannelId, result.Err)
|
||||
return []*model.FileInfo{}
|
||||
} else {
|
||||
channel = result.Data.(*model.Channel)
|
||||
}
|
||||
|
||||
// Find the team that was used to make this post since its part of the file path that isn't saved in the Filename
|
||||
var teamId string
|
||||
if channel.TeamId == "" {
|
||||
// This post was made in a cross-team DM channel so we need to find where its files were saved
|
||||
teamId = findTeamIdForFilename(post, filenames[0])
|
||||
} else {
|
||||
teamId = channel.TeamId
|
||||
}
|
||||
|
||||
// Create FileInfo objects for this post
|
||||
infos := make([]*model.FileInfo, 0, len(filenames))
|
||||
if teamId == "" {
|
||||
l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.team_id.error"), post.Id, filenames)
|
||||
} else {
|
||||
for _, filename := range filenames {
|
||||
info := getInfoForFilename(post, teamId, filename)
|
||||
if info == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
infos = append(infos, info)
|
||||
}
|
||||
}
|
||||
|
||||
// Lock to prevent only one migration thread from trying to update the post at once, preventing duplicate FileInfos from being created
|
||||
fileMigrationLock.Lock()
|
||||
defer fileMigrationLock.Unlock()
|
||||
|
||||
if result := <-app.Srv.Store.Post().Get(post.Id); result.Err != nil {
|
||||
l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.get_post_again.app_error"), post.Id, result.Err)
|
||||
return []*model.FileInfo{}
|
||||
} else if newPost := result.Data.(*model.PostList).Posts[post.Id]; len(newPost.Filenames) != len(post.Filenames) {
|
||||
// Another thread has already created FileInfos for this post, so just return those
|
||||
if result := <-app.Srv.Store.FileInfo().GetForPost(post.Id); result.Err != nil {
|
||||
l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.get_post_file_infos_again.app_error"), post.Id, result.Err)
|
||||
return []*model.FileInfo{}
|
||||
} else {
|
||||
l4g.Debug(utils.T("api.file.migrate_filenames_to_file_infos.not_migrating_post.debug"), post.Id)
|
||||
return result.Data.([]*model.FileInfo)
|
||||
}
|
||||
}
|
||||
|
||||
l4g.Debug(utils.T("api.file.migrate_filenames_to_file_infos.migrating_post.debug"), post.Id)
|
||||
|
||||
savedInfos := make([]*model.FileInfo, 0, len(infos))
|
||||
fileIds := make([]string, 0, len(filenames))
|
||||
for _, info := range infos {
|
||||
if result := <-app.Srv.Store.FileInfo().Save(info); result.Err != nil {
|
||||
l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.save_file_info.app_error"), post.Id, info.Id, info.Path, result.Err)
|
||||
continue
|
||||
}
|
||||
|
||||
savedInfos = append(savedInfos, info)
|
||||
fileIds = append(fileIds, info.Id)
|
||||
}
|
||||
|
||||
// Copy and save the updated post
|
||||
newPost := &model.Post{}
|
||||
*newPost = *post
|
||||
|
||||
newPost.Filenames = []string{}
|
||||
newPost.FileIds = fileIds
|
||||
|
||||
// Update Posts to clear Filenames and set FileIds
|
||||
if result := <-app.Srv.Store.Post().Update(newPost, post); result.Err != nil {
|
||||
l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.save_post.app_error"), post.Id, newPost.FileIds, post.Filenames, result.Err)
|
||||
return []*model.FileInfo{}
|
||||
} else {
|
||||
return savedInfos
|
||||
}
|
||||
}
|
||||
|
||||
func findTeamIdForFilename(post *model.Post, filename string) string {
|
||||
split := strings.SplitN(filename, "/", 5)
|
||||
id := split[3]
|
||||
name, _ := url.QueryUnescape(split[4])
|
||||
|
||||
// This post is in a direct channel so we need to figure out what team the files are stored under.
|
||||
if result := <-app.Srv.Store.Team().GetTeamsByUserId(post.UserId); result.Err != nil {
|
||||
l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.teams.app_error"), post.Id, result.Err)
|
||||
} else if teams := result.Data.([]*model.Team); len(teams) == 1 {
|
||||
// The user has only one team so the post must've been sent from it
|
||||
return teams[0].Id
|
||||
} else {
|
||||
for _, team := range teams {
|
||||
path := fmt.Sprintf("teams/%s/channels/%s/users/%s/%s/%s", team.Id, post.ChannelId, post.UserId, id, name)
|
||||
if _, err := ReadFile(path); err == nil {
|
||||
// Found the team that this file was posted from
|
||||
return team.Id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func getInfoForFilename(post *model.Post, teamId string, filename string) *model.FileInfo {
|
||||
// Find the path from the Filename of the form /{channelId}/{userId}/{uid}/{nameWithExtension}
|
||||
split := strings.SplitN(filename, "/", 5)
|
||||
if len(split) < 5 {
|
||||
l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.unexpected_filename.error"), post.Id, filename)
|
||||
return nil
|
||||
}
|
||||
|
||||
channelId := split[1]
|
||||
userId := split[2]
|
||||
oldId := split[3]
|
||||
name, _ := url.QueryUnescape(split[4])
|
||||
|
||||
if split[0] != "" || split[1] != post.ChannelId || split[2] != post.UserId || strings.Contains(split[4], "/") {
|
||||
l4g.Warn(utils.T("api.file.migrate_filenames_to_file_infos.mismatched_filename.warn"), post.Id, post.ChannelId, post.UserId, filename)
|
||||
}
|
||||
|
||||
pathPrefix := fmt.Sprintf("teams/%s/channels/%s/users/%s/%s/", teamId, channelId, userId, oldId)
|
||||
path := pathPrefix + name
|
||||
|
||||
// Open the file and populate the fields of the FileInfo
|
||||
var info *model.FileInfo
|
||||
if data, err := ReadFile(path); err != nil {
|
||||
l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.file_not_found.error"), post.Id, filename, path, err)
|
||||
return nil
|
||||
} else {
|
||||
var err *model.AppError
|
||||
info, err = model.GetInfoForBytes(name, data)
|
||||
if err != nil {
|
||||
l4g.Warn(utils.T("api.file.migrate_filenames_to_file_infos.info.app_error"), post.Id, filename, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a new ID because with the old system, you could very rarely get multiple posts referencing the same file
|
||||
info.Id = model.NewId()
|
||||
info.CreatorId = post.UserId
|
||||
info.PostId = post.Id
|
||||
info.CreateAt = post.CreateAt
|
||||
info.UpdateAt = post.UpdateAt
|
||||
info.Path = path
|
||||
|
||||
if info.IsImage() {
|
||||
nameWithoutExtension := name[:strings.LastIndex(name, ".")]
|
||||
info.PreviewPath = pathPrefix + nameWithoutExtension + "_preview.jpg"
|
||||
info.ThumbnailPath = pathPrefix + nameWithoutExtension + "_thumb.jpg"
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
func WriteFile(f []byte, path string) *model.AppError {
|
||||
if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
|
||||
endpoint := utils.Cfg.FileSettings.AmazonS3Endpoint
|
||||
accessKey := utils.Cfg.FileSettings.AmazonS3AccessKeyId
|
||||
secretKey := utils.Cfg.FileSettings.AmazonS3SecretAccessKey
|
||||
secure := *utils.Cfg.FileSettings.AmazonS3SSL
|
||||
s3Clnt, err := s3.New(endpoint, accessKey, secretKey, secure)
|
||||
if err != nil {
|
||||
return model.NewLocAppError("WriteFile", "api.file.write_file.s3.app_error", nil, err.Error())
|
||||
}
|
||||
bucket := utils.Cfg.FileSettings.AmazonS3Bucket
|
||||
ext := filepath.Ext(path)
|
||||
|
||||
if model.IsFileExtImage(ext) {
|
||||
_, err = s3Clnt.PutObject(bucket, path, bytes.NewReader(f), model.GetImageMimeType(ext))
|
||||
} else {
|
||||
_, err = s3Clnt.PutObject(bucket, path, bytes.NewReader(f), "binary/octet-stream")
|
||||
}
|
||||
if err != nil {
|
||||
return model.NewLocAppError("WriteFile", "api.file.write_file.s3.app_error", nil, err.Error())
|
||||
}
|
||||
} else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
|
||||
if err := writeFileLocally(f, utils.Cfg.FileSettings.Directory+path); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return model.NewLocAppError("WriteFile", "api.file.write_file.configured.app_error", nil, "")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func MoveFile(oldPath, newPath string) *model.AppError {
|
||||
if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
|
||||
endpoint := utils.Cfg.FileSettings.AmazonS3Endpoint
|
||||
accessKey := utils.Cfg.FileSettings.AmazonS3AccessKeyId
|
||||
secretKey := utils.Cfg.FileSettings.AmazonS3SecretAccessKey
|
||||
secure := *utils.Cfg.FileSettings.AmazonS3SSL
|
||||
s3Clnt, err := s3.New(endpoint, accessKey, secretKey, secure)
|
||||
if err != nil {
|
||||
return model.NewLocAppError("moveFile", "api.file.write_file.s3.app_error", nil, err.Error())
|
||||
}
|
||||
bucket := utils.Cfg.FileSettings.AmazonS3Bucket
|
||||
|
||||
var copyConds = s3.NewCopyConditions()
|
||||
if err = s3Clnt.CopyObject(bucket, newPath, "/"+path.Join(bucket, oldPath), copyConds); err != nil {
|
||||
return model.NewLocAppError("moveFile", "api.file.move_file.delete_from_s3.app_error", nil, err.Error())
|
||||
}
|
||||
if err = s3Clnt.RemoveObject(bucket, oldPath); err != nil {
|
||||
return model.NewLocAppError("moveFile", "api.file.move_file.delete_from_s3.app_error", nil, err.Error())
|
||||
}
|
||||
} else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
|
||||
if err := os.MkdirAll(filepath.Dir(utils.Cfg.FileSettings.Directory+newPath), 0774); err != nil {
|
||||
return model.NewLocAppError("moveFile", "api.file.move_file.rename.app_error", nil, err.Error())
|
||||
}
|
||||
|
||||
if err := os.Rename(utils.Cfg.FileSettings.Directory+oldPath, utils.Cfg.FileSettings.Directory+newPath); err != nil {
|
||||
return model.NewLocAppError("moveFile", "api.file.move_file.rename.app_error", nil, err.Error())
|
||||
}
|
||||
} else {
|
||||
return model.NewLocAppError("moveFile", "api.file.move_file.configured.app_error", nil, "")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeFileLocally(f []byte, path string) *model.AppError {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0774); err != nil {
|
||||
directory, _ := filepath.Abs(filepath.Dir(path))
|
||||
return model.NewLocAppError("WriteFile", "api.file.write_file_locally.create_dir.app_error", nil, "directory="+directory+", err="+err.Error())
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(path, f, 0644); err != nil {
|
||||
return model.NewLocAppError("WriteFile", "api.file.write_file_locally.writing.app_error", nil, err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReadFile(path string) ([]byte, *model.AppError) {
|
||||
if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
|
||||
endpoint := utils.Cfg.FileSettings.AmazonS3Endpoint
|
||||
accessKey := utils.Cfg.FileSettings.AmazonS3AccessKeyId
|
||||
secretKey := utils.Cfg.FileSettings.AmazonS3SecretAccessKey
|
||||
secure := *utils.Cfg.FileSettings.AmazonS3SSL
|
||||
s3Clnt, err := s3.New(endpoint, accessKey, secretKey, secure)
|
||||
if err != nil {
|
||||
return nil, model.NewLocAppError("ReadFile", "api.file.read_file.s3.app_error", nil, err.Error())
|
||||
}
|
||||
bucket := utils.Cfg.FileSettings.AmazonS3Bucket
|
||||
minioObject, err := s3Clnt.GetObject(bucket, path)
|
||||
defer minioObject.Close()
|
||||
if err != nil {
|
||||
return nil, model.NewLocAppError("ReadFile", "api.file.read_file.s3.app_error", nil, err.Error())
|
||||
}
|
||||
if f, err := ioutil.ReadAll(minioObject); err != nil {
|
||||
return nil, model.NewLocAppError("ReadFile", "api.file.read_file.s3.app_error", nil, err.Error())
|
||||
} else {
|
||||
return f, nil
|
||||
}
|
||||
} else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
|
||||
if f, err := ioutil.ReadFile(utils.Cfg.FileSettings.Directory + path); err != nil {
|
||||
return nil, model.NewLocAppError("ReadFile", "api.file.read_file.reading_local.app_error", nil, err.Error())
|
||||
} else {
|
||||
return f, nil
|
||||
}
|
||||
} else {
|
||||
return nil, model.NewLocAppError("ReadFile", "api.file.read_file.configured.app_error", nil, "")
|
||||
}
|
||||
}
|
||||
|
||||
func openFileWriteStream(path string) (io.Writer, *model.AppError) {
|
||||
if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
|
||||
return nil, model.NewLocAppError("openFileWriteStream", "api.file.open_file_write_stream.s3.app_error", nil, "")
|
||||
} else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
|
||||
if err := os.MkdirAll(filepath.Dir(utils.Cfg.FileSettings.Directory+path), 0774); err != nil {
|
||||
return nil, model.NewLocAppError("openFileWriteStream", "api.file.open_file_write_stream.creating_dir.app_error", nil, err.Error())
|
||||
}
|
||||
|
||||
if fileHandle, err := os.Create(utils.Cfg.FileSettings.Directory + path); err != nil {
|
||||
return nil, model.NewLocAppError("openFileWriteStream", "api.file.open_file_write_stream.local_server.app_error", nil, err.Error())
|
||||
} else {
|
||||
fileHandle.Chmod(0644)
|
||||
return fileHandle, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, model.NewLocAppError("openFileWriteStream", "api.file.open_file_write_stream.configured.app_error", nil, "")
|
||||
}
|
||||
|
||||
func closeFileWriteStream(file io.Writer) {
|
||||
file.(*os.File).Close()
|
||||
w.Write([]byte(model.StringToJson(app.GeneratePublicLink(c.GetSiteURL(), info))))
|
||||
}
|
||||
|
||||
@@ -508,7 +508,7 @@ func TestGetPublicFileOld(t *testing.T) {
|
||||
}
|
||||
|
||||
func generatePublicLinkOld(siteURL, teamId, channelId, userId, filename string) string {
|
||||
hash := generatePublicLinkHash(filename, *utils.Cfg.FileSettings.PublicLinkSalt)
|
||||
hash := app.GeneratePublicLinkHash(filename, *utils.Cfg.FileSettings.PublicLinkSalt)
|
||||
return fmt.Sprintf("%s%s/public/files/get/%s/%s/%s/%s?h=%s", siteURL, model.API_URL_SUFFIX, teamId, channelId, userId, filename, hash)
|
||||
}
|
||||
|
||||
@@ -581,29 +581,6 @@ func TestGetPublicLink(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratePublicLinkHash(t *testing.T) {
|
||||
filename1 := model.NewId() + "/" + model.NewRandomString(16) + ".txt"
|
||||
filename2 := model.NewId() + "/" + model.NewRandomString(16) + ".txt"
|
||||
salt1 := model.NewRandomString(32)
|
||||
salt2 := model.NewRandomString(32)
|
||||
|
||||
hash1 := generatePublicLinkHash(filename1, salt1)
|
||||
hash2 := generatePublicLinkHash(filename2, salt1)
|
||||
hash3 := generatePublicLinkHash(filename1, salt2)
|
||||
|
||||
if hash1 != generatePublicLinkHash(filename1, salt1) {
|
||||
t.Fatal("hash should be equal for the same file name and salt")
|
||||
}
|
||||
|
||||
if hash1 == hash2 {
|
||||
t.Fatal("hashes for different files should not be equal")
|
||||
}
|
||||
|
||||
if hash1 == hash3 {
|
||||
t.Fatal("hashes for the same file with different salts should not be equal")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateFilenamesToFileInfos(t *testing.T) {
|
||||
th := Setup().InitBasic()
|
||||
|
||||
@@ -740,7 +717,7 @@ func TestFindTeamIdForFilename(t *testing.T) {
|
||||
Filenames: []string{fmt.Sprintf("/%s/%s/%s/%s", channel1.Id, user1.Id, fileId1, "test.png")},
|
||||
})).(*model.Post)
|
||||
|
||||
if teamId := findTeamIdForFilename(post1, post1.Filenames[0]); teamId != team1.Id {
|
||||
if teamId := app.FindTeamIdForFilename(post1, post1.Filenames[0]); teamId != team1.Id {
|
||||
t.Fatal("file should've been found under team1")
|
||||
}
|
||||
|
||||
@@ -753,7 +730,7 @@ func TestFindTeamIdForFilename(t *testing.T) {
|
||||
})).(*model.Post)
|
||||
Client.SetTeamId(team1.Id)
|
||||
|
||||
if teamId := findTeamIdForFilename(post2, post2.Filenames[0]); teamId != team2.Id {
|
||||
if teamId := app.FindTeamIdForFilename(post2, post2.Filenames[0]); teamId != team2.Id {
|
||||
t.Fatal("file should've been found under team2")
|
||||
}
|
||||
}
|
||||
@@ -795,7 +772,7 @@ func TestGetInfoForFilename(t *testing.T) {
|
||||
Filenames: []string{fmt.Sprintf("/%s/%s/%s/%s", channel1.Id, user1.Id, fileId1, "test.png")},
|
||||
})).(*model.Post)
|
||||
|
||||
if info := getInfoForFilename(post1, team1.Id, post1.Filenames[0]); info == nil {
|
||||
if info := app.GetInfoForFilename(post1, team1.Id, post1.Filenames[0]); info == nil {
|
||||
t.Fatal("info shouldn't be nil")
|
||||
} else if info.Id == "" {
|
||||
t.Fatal("info.Id shouldn't be empty")
|
||||
|
||||
34
api/oauth.go
34
api/oauth.go
@@ -250,32 +250,6 @@ func getAuthorizedApps(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func RevokeAccessToken(token string) *model.AppError {
|
||||
|
||||
session, _ := app.GetSession(token)
|
||||
schan := app.Srv.Store.Session().Remove(token)
|
||||
|
||||
if result := <-app.Srv.Store.OAuth().GetAccessData(token); result.Err != nil {
|
||||
return model.NewLocAppError("RevokeAccessToken", "api.oauth.revoke_access_token.get.app_error", nil, "")
|
||||
}
|
||||
|
||||
tchan := app.Srv.Store.OAuth().RemoveAccessData(token)
|
||||
|
||||
if result := <-tchan; result.Err != nil {
|
||||
return model.NewLocAppError("RevokeAccessToken", "api.oauth.revoke_access_token.del_token.app_error", nil, "")
|
||||
}
|
||||
|
||||
if result := <-schan; result.Err != nil {
|
||||
return model.NewLocAppError("RevokeAccessToken", "api.oauth.revoke_access_token.del_session.app_error", nil, "")
|
||||
}
|
||||
|
||||
if session != nil {
|
||||
app.RemoveAllSessionsForUserId(session.UserId)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetAuthData(code string) *model.AuthData {
|
||||
if result := <-app.Srv.Store.OAuth().GetAuthData(code); result.Err != nil {
|
||||
l4g.Error(utils.T("api.oauth.get_auth_data.find.error"), code)
|
||||
@@ -311,7 +285,11 @@ func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
action := props["action"]
|
||||
switch action {
|
||||
case model.OAUTH_ACTION_SIGNUP:
|
||||
CreateOAuthUser(c, w, r, service, body, teamId)
|
||||
if user, err := app.CreateOAuthUser(service, body, teamId); err != nil {
|
||||
c.Err = err
|
||||
} else {
|
||||
doLogin(c, w, r, user, "")
|
||||
}
|
||||
if c.Err == nil {
|
||||
http.Redirect(w, r, GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect)
|
||||
}
|
||||
@@ -931,7 +909,7 @@ func deauthorizeOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
accessData := result.Data.([]*model.AccessData)
|
||||
|
||||
for _, a := range accessData {
|
||||
if err := RevokeAccessToken(a.Token); err != nil {
|
||||
if err := app.RevokeAccessToken(a.Token); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
21
api/post.go
21
api/post.go
@@ -362,13 +362,18 @@ func getPermalinkTmp(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
post := list.Posts[list.Order[0]]
|
||||
|
||||
// Because we confuse permissions and membership in Mattermost's model, we have to just
|
||||
// try to join the channel without checking if we already have permission to it. This is
|
||||
// because system admins have permissions to every channel but are not nessisary a member
|
||||
// of every channel. If we checked here then system admins would skip joining the channel and
|
||||
// error when they tried to view it.
|
||||
if err, _ := JoinChannelById(c, c.Session.UserId, post.ChannelId); err != nil {
|
||||
// On error just return with permissions error
|
||||
var channel *model.Channel
|
||||
var err *model.AppError
|
||||
if channel, err = app.GetChannel(post.ChannelId); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if !HasPermissionToTeamContext(c, channel.TeamId, model.PERMISSION_JOIN_PUBLIC_CHANNELS) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = app.JoinChannel(channel, c.Session.UserId); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
@@ -611,7 +616,7 @@ func getFileInfosForPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if len(post.Filenames) > 0 {
|
||||
// The post has Filenames that need to be replaced with FileInfos
|
||||
infos = migrateFilenamesToFileInfos(post)
|
||||
infos = app.MigrateFilenamesToFileInfos(post)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -248,7 +248,10 @@ func revokeAllSessions(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.LogAudit("revoked_all=" + id)
|
||||
|
||||
if session.IsOAuth {
|
||||
RevokeAccessToken(session.Token)
|
||||
if err := app.RevokeAccessToken(session.Token); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if result := <-app.Srv.Store.Session().Remove(session.Id); result.Err != nil {
|
||||
c.Err = result.Err
|
||||
|
||||
700
api/user.go
700
api/user.go
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -107,25 +107,6 @@ func TestCheckUserDomain(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsUsernameTaken(t *testing.T) {
|
||||
th := Setup().InitBasic()
|
||||
user := th.BasicUser
|
||||
taken := IsUsernameTaken(user.Username)
|
||||
|
||||
if !taken {
|
||||
t.Logf("the username '%v' should be taken", user.Username)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
newUsername := "randomUsername"
|
||||
taken = IsUsernameTaken(newUsername)
|
||||
|
||||
if taken {
|
||||
t.Logf("the username '%v' should not be taken", newUsername)
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
th := Setup()
|
||||
Client := th.CreateClient()
|
||||
@@ -227,7 +208,10 @@ func TestLogin(t *testing.T) {
|
||||
data := model.MapToJson(props)
|
||||
hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt))
|
||||
|
||||
ruser2, _ := Client.CreateUserFromSignup(&user2, data, hash)
|
||||
ruser2, err := Client.CreateUserFromSignup(&user2, data, hash)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := Client.Login(ruser2.Data.(*model.User).Email, user2.Password); err != nil {
|
||||
t.Fatal("From verfied hash")
|
||||
@@ -686,7 +670,7 @@ func TestUserCreateImage(t *testing.T) {
|
||||
th := Setup()
|
||||
Client := th.CreateClient()
|
||||
|
||||
b, err := createProfileImage("Corey Hulen", "eo1zkdr96pdj98pjmq8zy35wba")
|
||||
b, err := app.CreateProfileImage("Corey Hulen", "eo1zkdr96pdj98pjmq8zy35wba")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -115,22 +115,3 @@ func closeBody(r *http.Response) {
|
||||
r.Body.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func RevokeWebrtcToken(sessionId string) {
|
||||
token := base64.StdEncoding.EncodeToString([]byte(sessionId))
|
||||
data := make(map[string]string)
|
||||
data["janus"] = "remove_token"
|
||||
data["token"] = token
|
||||
data["transaction"] = model.NewId()
|
||||
data["admin_secret"] = *utils.Cfg.WebrtcSettings.GatewayAdminSecret
|
||||
|
||||
rq, _ := http.NewRequest("POST", *utils.Cfg.WebrtcSettings.GatewayAdminUrl, strings.NewReader(model.MapToJson(data)))
|
||||
rq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// we do not care about the response
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: *utils.Cfg.ServiceSettings.EnableInsecureOutgoingConnections},
|
||||
}
|
||||
httpClient := &http.Client{Transport: tr}
|
||||
httpClient.Do(rq)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mattermost/platform/app"
|
||||
"github.com/mattermost/platform/model"
|
||||
"github.com/mattermost/platform/utils"
|
||||
"testing"
|
||||
@@ -39,5 +40,5 @@ func TestWebrtcToken(t *testing.T) {
|
||||
fmt.Println("Turn Password", result["turn_password"])
|
||||
}
|
||||
|
||||
RevokeWebrtcToken(sessionId)
|
||||
app.RevokeWebrtcToken(sessionId)
|
||||
}
|
||||
|
||||
16
app/audit.go
Обычный файл
16
app/audit.go
Обычный файл
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"github.com/mattermost/platform/model"
|
||||
)
|
||||
|
||||
func GetAudits(userId string, limit int) (model.Audits, *model.AppError) {
|
||||
if result := <-Srv.Store.Audit().Get(userId, limit); result.Err != nil {
|
||||
return nil, result.Err
|
||||
} else {
|
||||
return result.Data.(model.Audits), nil
|
||||
}
|
||||
}
|
||||
187
app/channel.go
187
app/channel.go
@@ -163,6 +163,33 @@ func CreateChannel(channel *model.Channel, addMember bool) (*model.Channel, *mod
|
||||
}
|
||||
}
|
||||
|
||||
func CreateDirectChannel(userId string, otherUserId string) (*model.Channel, *model.AppError) {
|
||||
uc := Srv.Store.User().Get(otherUserId)
|
||||
|
||||
if uresult := <-uc; uresult.Err != nil {
|
||||
return nil, model.NewLocAppError("CreateDirectChannel", "api.channel.create_direct_channel.invalid_user.app_error", nil, otherUserId)
|
||||
}
|
||||
|
||||
if result := <-Srv.Store.Channel().CreateDirectChannel(userId, otherUserId); result.Err != nil {
|
||||
if result.Err.Id == store.CHANNEL_EXISTS_ERROR {
|
||||
return result.Data.(*model.Channel), nil
|
||||
} else {
|
||||
return nil, result.Err
|
||||
}
|
||||
} else {
|
||||
channel := result.Data.(*model.Channel)
|
||||
|
||||
InvalidateCacheForUser(userId)
|
||||
InvalidateCacheForUser(otherUserId)
|
||||
|
||||
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_DIRECT_ADDED, "", channel.Id, "", nil)
|
||||
message.Add("teammate_id", otherUserId)
|
||||
Publish(message)
|
||||
|
||||
return channel, nil
|
||||
}
|
||||
}
|
||||
|
||||
func AddUserToChannel(user *model.User, channel *model.Channel) (*model.ChannelMember, *model.AppError) {
|
||||
if channel.DeleteAt > 0 {
|
||||
return nil, model.NewLocAppError("AddUserToChannel", "api.channel.add_user_to_channel.deleted.app_error", nil, "")
|
||||
@@ -210,7 +237,165 @@ func AddUserToChannel(user *model.User, channel *model.Channel) (*model.ChannelM
|
||||
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_ADDED, "", channel.Id, "", nil)
|
||||
message.Add("user_id", user.Id)
|
||||
message.Add("team_id", channel.TeamId)
|
||||
go Publish(message)
|
||||
Publish(message)
|
||||
|
||||
return newMember, nil
|
||||
}
|
||||
|
||||
func AddDirectChannels(teamId string, user *model.User) *model.AppError {
|
||||
var profiles map[string]*model.User
|
||||
if result := <-Srv.Store.User().GetProfiles(teamId, 0, 100); result.Err != nil {
|
||||
return model.NewLocAppError("AddDirectChannels", "api.user.add_direct_channels_and_forget.failed.error", map[string]interface{}{"UserId": user.Id, "TeamId": teamId, "Error": result.Err.Error()}, "")
|
||||
} else {
|
||||
profiles = result.Data.(map[string]*model.User)
|
||||
}
|
||||
|
||||
var preferences model.Preferences
|
||||
|
||||
for id := range profiles {
|
||||
if id == user.Id {
|
||||
continue
|
||||
}
|
||||
|
||||
profile := profiles[id]
|
||||
|
||||
preference := model.Preference{
|
||||
UserId: user.Id,
|
||||
Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW,
|
||||
Name: profile.Id,
|
||||
Value: "true",
|
||||
}
|
||||
|
||||
preferences = append(preferences, preference)
|
||||
|
||||
if len(preferences) >= 10 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if result := <-Srv.Store.Preference().Save(&preferences); result.Err != nil {
|
||||
return model.NewLocAppError("AddDirectChannels", "api.user.add_direct_channels_and_forget.failed.error", map[string]interface{}{"UserId": user.Id, "TeamId": teamId, "Error": result.Err.Error()}, "")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func PostUpdateChannelHeaderMessage(userId string, channelId string, teamId string, oldChannelHeader, newChannelHeader string) *model.AppError {
|
||||
uc := Srv.Store.User().Get(userId)
|
||||
|
||||
if uresult := <-uc; uresult.Err != nil {
|
||||
return model.NewLocAppError("PostUpdateChannelHeaderMessage", "api.channel.post_update_channel_header_message_and_forget.retrieve_user.error", nil, uresult.Err.Error())
|
||||
} else {
|
||||
user := uresult.Data.(*model.User)
|
||||
|
||||
var message string
|
||||
if oldChannelHeader == "" {
|
||||
message = fmt.Sprintf(utils.T("api.channel.post_update_channel_header_message_and_forget.updated_to"), user.Username, newChannelHeader)
|
||||
} else if newChannelHeader == "" {
|
||||
message = fmt.Sprintf(utils.T("api.channel.post_update_channel_header_message_and_forget.removed"), user.Username, oldChannelHeader)
|
||||
} else {
|
||||
message = fmt.Sprintf(utils.T("api.channel.post_update_channel_header_message_and_forget.updated_from"), user.Username, oldChannelHeader, newChannelHeader)
|
||||
}
|
||||
|
||||
post := &model.Post{
|
||||
ChannelId: channelId,
|
||||
Message: message,
|
||||
Type: model.POST_HEADER_CHANGE,
|
||||
UserId: userId,
|
||||
Props: model.StringInterface{
|
||||
"old_header": oldChannelHeader,
|
||||
"new_header": newChannelHeader,
|
||||
},
|
||||
}
|
||||
|
||||
if _, err := CreatePost(post, teamId, false); err != nil {
|
||||
return model.NewLocAppError("", "api.channel.post_update_channel_header_message_and_forget.post.error", nil, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func PostUpdateChannelDisplayNameMessage(userId string, channelId string, teamId string, oldChannelDisplayName, newChannelDisplayName string) *model.AppError {
|
||||
uc := Srv.Store.User().Get(userId)
|
||||
|
||||
if uresult := <-uc; uresult.Err != nil {
|
||||
return model.NewLocAppError("PostUpdateChannelDisplayNameMessage", "api.channel.post_update_channel_displayname_message_and_forget.retrieve_user.error", nil, uresult.Err.Error())
|
||||
} else {
|
||||
user := uresult.Data.(*model.User)
|
||||
|
||||
message := fmt.Sprintf(utils.T("api.channel.post_update_channel_displayname_message_and_forget.updated_from"), user.Username, oldChannelDisplayName, newChannelDisplayName)
|
||||
|
||||
post := &model.Post{
|
||||
ChannelId: channelId,
|
||||
Message: message,
|
||||
Type: model.POST_DISPLAYNAME_CHANGE,
|
||||
UserId: userId,
|
||||
Props: model.StringInterface{
|
||||
"old_displayname": oldChannelDisplayName,
|
||||
"new_displayname": newChannelDisplayName,
|
||||
},
|
||||
}
|
||||
|
||||
if _, err := CreatePost(post, teamId, false); err != nil {
|
||||
return model.NewLocAppError("PostUpdateChannelDisplayNameMessage", "api.channel.post_update_channel_displayname_message_and_forget.create_post.error", nil, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetChannel(channelId string) (*model.Channel, *model.AppError) {
|
||||
if result := <-Srv.Store.Channel().Get(channelId, true); result.Err != nil {
|
||||
return nil, result.Err
|
||||
} else {
|
||||
return result.Data.(*model.Channel), nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetChannelByName(channelName, teamId string) (*model.Channel, *model.AppError) {
|
||||
if result := <-Srv.Store.Channel().GetByName(teamId, channelName); result.Err != nil {
|
||||
return nil, result.Err
|
||||
} else {
|
||||
return result.Data.(*model.Channel), nil
|
||||
}
|
||||
}
|
||||
|
||||
func JoinChannel(channel *model.Channel, userId string) *model.AppError {
|
||||
userChan := Srv.Store.User().Get(userId)
|
||||
memberChan := Srv.Store.Channel().GetMember(channel.Id, userId)
|
||||
|
||||
if uresult := <-userChan; uresult.Err != nil {
|
||||
return uresult.Err
|
||||
} else if mresult := <-memberChan; mresult.Err == nil && mresult.Data != nil {
|
||||
// user is already in the channel
|
||||
return nil
|
||||
} else {
|
||||
user := uresult.Data.(*model.User)
|
||||
|
||||
if channel.Type == model.CHANNEL_OPEN {
|
||||
if _, err := AddUserToChannel(user, channel); err != nil {
|
||||
return err
|
||||
}
|
||||
PostUserAddRemoveMessage(userId, channel.Id, channel.TeamId, fmt.Sprintf(utils.T("api.channel.join_channel.post_and_forget"), user.Username), model.POST_JOIN_LEAVE)
|
||||
} else {
|
||||
return model.NewLocAppError("JoinChannel", "api.channel.join_channel.permissions.app_error", nil, "")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func PostUserAddRemoveMessage(userId, channelId, teamId, message, postType string) *model.AppError {
|
||||
post := &model.Post{
|
||||
ChannelId: channelId,
|
||||
Message: message,
|
||||
Type: postType,
|
||||
UserId: userId,
|
||||
}
|
||||
if _, err := CreatePost(post, teamId, false); err != nil {
|
||||
return model.NewLocAppError("PostUserAddRemoveMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
340
app/file.go
Обычный файл
340
app/file.go
Обычный файл
@@ -0,0 +1,340 @@
|
||||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
l4g "github.com/alecthomas/log4go"
|
||||
"github.com/mattermost/platform/model"
|
||||
"github.com/mattermost/platform/utils"
|
||||
|
||||
s3 "github.com/minio/minio-go"
|
||||
)
|
||||
|
||||
func ReadFile(path string) ([]byte, *model.AppError) {
|
||||
if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
|
||||
endpoint := utils.Cfg.FileSettings.AmazonS3Endpoint
|
||||
accessKey := utils.Cfg.FileSettings.AmazonS3AccessKeyId
|
||||
secretKey := utils.Cfg.FileSettings.AmazonS3SecretAccessKey
|
||||
secure := *utils.Cfg.FileSettings.AmazonS3SSL
|
||||
s3Clnt, err := s3.New(endpoint, accessKey, secretKey, secure)
|
||||
if err != nil {
|
||||
return nil, model.NewLocAppError("ReadFile", "api.file.read_file.s3.app_error", nil, err.Error())
|
||||
}
|
||||
bucket := utils.Cfg.FileSettings.AmazonS3Bucket
|
||||
minioObject, err := s3Clnt.GetObject(bucket, path)
|
||||
defer minioObject.Close()
|
||||
if err != nil {
|
||||
return nil, model.NewLocAppError("ReadFile", "api.file.read_file.s3.app_error", nil, err.Error())
|
||||
}
|
||||
if f, err := ioutil.ReadAll(minioObject); err != nil {
|
||||
return nil, model.NewLocAppError("ReadFile", "api.file.read_file.s3.app_error", nil, err.Error())
|
||||
} else {
|
||||
return f, nil
|
||||
}
|
||||
} else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
|
||||
if f, err := ioutil.ReadFile(utils.Cfg.FileSettings.Directory + path); err != nil {
|
||||
return nil, model.NewLocAppError("ReadFile", "api.file.read_file.reading_local.app_error", nil, err.Error())
|
||||
} else {
|
||||
return f, nil
|
||||
}
|
||||
} else {
|
||||
return nil, model.NewLocAppError("ReadFile", "api.file.read_file.configured.app_error", nil, "")
|
||||
}
|
||||
}
|
||||
|
||||
func MoveFile(oldPath, newPath string) *model.AppError {
|
||||
if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
|
||||
endpoint := utils.Cfg.FileSettings.AmazonS3Endpoint
|
||||
accessKey := utils.Cfg.FileSettings.AmazonS3AccessKeyId
|
||||
secretKey := utils.Cfg.FileSettings.AmazonS3SecretAccessKey
|
||||
secure := *utils.Cfg.FileSettings.AmazonS3SSL
|
||||
s3Clnt, err := s3.New(endpoint, accessKey, secretKey, secure)
|
||||
if err != nil {
|
||||
return model.NewLocAppError("moveFile", "api.file.write_file.s3.app_error", nil, err.Error())
|
||||
}
|
||||
bucket := utils.Cfg.FileSettings.AmazonS3Bucket
|
||||
|
||||
var copyConds = s3.NewCopyConditions()
|
||||
if err = s3Clnt.CopyObject(bucket, newPath, "/"+path.Join(bucket, oldPath), copyConds); err != nil {
|
||||
return model.NewLocAppError("moveFile", "api.file.move_file.delete_from_s3.app_error", nil, err.Error())
|
||||
}
|
||||
if err = s3Clnt.RemoveObject(bucket, oldPath); err != nil {
|
||||
return model.NewLocAppError("moveFile", "api.file.move_file.delete_from_s3.app_error", nil, err.Error())
|
||||
}
|
||||
} else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
|
||||
if err := os.MkdirAll(filepath.Dir(utils.Cfg.FileSettings.Directory+newPath), 0774); err != nil {
|
||||
return model.NewLocAppError("moveFile", "api.file.move_file.rename.app_error", nil, err.Error())
|
||||
}
|
||||
|
||||
if err := os.Rename(utils.Cfg.FileSettings.Directory+oldPath, utils.Cfg.FileSettings.Directory+newPath); err != nil {
|
||||
return model.NewLocAppError("moveFile", "api.file.move_file.rename.app_error", nil, err.Error())
|
||||
}
|
||||
} else {
|
||||
return model.NewLocAppError("moveFile", "api.file.move_file.configured.app_error", nil, "")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func WriteFile(f []byte, path string) *model.AppError {
|
||||
if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
|
||||
endpoint := utils.Cfg.FileSettings.AmazonS3Endpoint
|
||||
accessKey := utils.Cfg.FileSettings.AmazonS3AccessKeyId
|
||||
secretKey := utils.Cfg.FileSettings.AmazonS3SecretAccessKey
|
||||
secure := *utils.Cfg.FileSettings.AmazonS3SSL
|
||||
s3Clnt, err := s3.New(endpoint, accessKey, secretKey, secure)
|
||||
if err != nil {
|
||||
return model.NewLocAppError("WriteFile", "api.file.write_file.s3.app_error", nil, err.Error())
|
||||
}
|
||||
bucket := utils.Cfg.FileSettings.AmazonS3Bucket
|
||||
ext := filepath.Ext(path)
|
||||
|
||||
if model.IsFileExtImage(ext) {
|
||||
_, err = s3Clnt.PutObject(bucket, path, bytes.NewReader(f), model.GetImageMimeType(ext))
|
||||
} else {
|
||||
_, err = s3Clnt.PutObject(bucket, path, bytes.NewReader(f), "binary/octet-stream")
|
||||
}
|
||||
if err != nil {
|
||||
return model.NewLocAppError("WriteFile", "api.file.write_file.s3.app_error", nil, err.Error())
|
||||
}
|
||||
} else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
|
||||
if err := writeFileLocally(f, utils.Cfg.FileSettings.Directory+path); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return model.NewLocAppError("WriteFile", "api.file.write_file.configured.app_error", nil, "")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeFileLocally(f []byte, path string) *model.AppError {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0774); err != nil {
|
||||
directory, _ := filepath.Abs(filepath.Dir(path))
|
||||
return model.NewLocAppError("WriteFile", "api.file.write_file_locally.create_dir.app_error", nil, "directory="+directory+", err="+err.Error())
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(path, f, 0644); err != nil {
|
||||
return model.NewLocAppError("WriteFile", "api.file.write_file_locally.writing.app_error", nil, err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func openFileWriteStream(path string) (io.Writer, *model.AppError) {
|
||||
if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
|
||||
return nil, model.NewLocAppError("openFileWriteStream", "api.file.open_file_write_stream.s3.app_error", nil, "")
|
||||
} else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
|
||||
if err := os.MkdirAll(filepath.Dir(utils.Cfg.FileSettings.Directory+path), 0774); err != nil {
|
||||
return nil, model.NewLocAppError("openFileWriteStream", "api.file.open_file_write_stream.creating_dir.app_error", nil, err.Error())
|
||||
}
|
||||
|
||||
if fileHandle, err := os.Create(utils.Cfg.FileSettings.Directory + path); err != nil {
|
||||
return nil, model.NewLocAppError("openFileWriteStream", "api.file.open_file_write_stream.local_server.app_error", nil, err.Error())
|
||||
} else {
|
||||
fileHandle.Chmod(0644)
|
||||
return fileHandle, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, model.NewLocAppError("openFileWriteStream", "api.file.open_file_write_stream.configured.app_error", nil, "")
|
||||
}
|
||||
|
||||
func closeFileWriteStream(file io.Writer) {
|
||||
file.(*os.File).Close()
|
||||
}
|
||||
|
||||
func GetInfoForFilename(post *model.Post, teamId string, filename string) *model.FileInfo {
|
||||
// Find the path from the Filename of the form /{channelId}/{userId}/{uid}/{nameWithExtension}
|
||||
split := strings.SplitN(filename, "/", 5)
|
||||
if len(split) < 5 {
|
||||
l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.unexpected_filename.error"), post.Id, filename)
|
||||
return nil
|
||||
}
|
||||
|
||||
channelId := split[1]
|
||||
userId := split[2]
|
||||
oldId := split[3]
|
||||
name, _ := url.QueryUnescape(split[4])
|
||||
|
||||
if split[0] != "" || split[1] != post.ChannelId || split[2] != post.UserId || strings.Contains(split[4], "/") {
|
||||
l4g.Warn(utils.T("api.file.migrate_filenames_to_file_infos.mismatched_filename.warn"), post.Id, post.ChannelId, post.UserId, filename)
|
||||
}
|
||||
|
||||
pathPrefix := fmt.Sprintf("teams/%s/channels/%s/users/%s/%s/", teamId, channelId, userId, oldId)
|
||||
path := pathPrefix + name
|
||||
|
||||
// Open the file and populate the fields of the FileInfo
|
||||
var info *model.FileInfo
|
||||
if data, err := ReadFile(path); err != nil {
|
||||
l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.file_not_found.error"), post.Id, filename, path, err)
|
||||
return nil
|
||||
} else {
|
||||
var err *model.AppError
|
||||
info, err = model.GetInfoForBytes(name, data)
|
||||
if err != nil {
|
||||
l4g.Warn(utils.T("api.file.migrate_filenames_to_file_infos.info.app_error"), post.Id, filename, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a new ID because with the old system, you could very rarely get multiple posts referencing the same file
|
||||
info.Id = model.NewId()
|
||||
info.CreatorId = post.UserId
|
||||
info.PostId = post.Id
|
||||
info.CreateAt = post.CreateAt
|
||||
info.UpdateAt = post.UpdateAt
|
||||
info.Path = path
|
||||
|
||||
if info.IsImage() {
|
||||
nameWithoutExtension := name[:strings.LastIndex(name, ".")]
|
||||
info.PreviewPath = pathPrefix + nameWithoutExtension + "_preview.jpg"
|
||||
info.ThumbnailPath = pathPrefix + nameWithoutExtension + "_thumb.jpg"
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
func FindTeamIdForFilename(post *model.Post, filename string) string {
|
||||
split := strings.SplitN(filename, "/", 5)
|
||||
id := split[3]
|
||||
name, _ := url.QueryUnescape(split[4])
|
||||
|
||||
// This post is in a direct channel so we need to figure out what team the files are stored under.
|
||||
if result := <-Srv.Store.Team().GetTeamsByUserId(post.UserId); result.Err != nil {
|
||||
l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.teams.app_error"), post.Id, result.Err)
|
||||
} else if teams := result.Data.([]*model.Team); len(teams) == 1 {
|
||||
// The user has only one team so the post must've been sent from it
|
||||
return teams[0].Id
|
||||
} else {
|
||||
for _, team := range teams {
|
||||
path := fmt.Sprintf("teams/%s/channels/%s/users/%s/%s/%s", team.Id, post.ChannelId, post.UserId, id, name)
|
||||
if _, err := ReadFile(path); err == nil {
|
||||
// Found the team that this file was posted from
|
||||
return team.Id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
var fileMigrationLock sync.Mutex
|
||||
|
||||
// Creates and stores FileInfos for a post created before the FileInfos table existed.
|
||||
func MigrateFilenamesToFileInfos(post *model.Post) []*model.FileInfo {
|
||||
if len(post.Filenames) == 0 {
|
||||
l4g.Warn(utils.T("api.file.migrate_filenames_to_file_infos.no_filenames.warn"), post.Id)
|
||||
return []*model.FileInfo{}
|
||||
}
|
||||
|
||||
cchan := Srv.Store.Channel().Get(post.ChannelId, true)
|
||||
|
||||
// There's a weird bug that rarely happens where a post ends up with duplicate Filenames so remove those
|
||||
filenames := utils.RemoveDuplicatesFromStringArray(post.Filenames)
|
||||
|
||||
var channel *model.Channel
|
||||
if result := <-cchan; result.Err != nil {
|
||||
l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.channel.app_error"), post.Id, post.ChannelId, result.Err)
|
||||
return []*model.FileInfo{}
|
||||
} else {
|
||||
channel = result.Data.(*model.Channel)
|
||||
}
|
||||
|
||||
// Find the team that was used to make this post since its part of the file path that isn't saved in the Filename
|
||||
var teamId string
|
||||
if channel.TeamId == "" {
|
||||
// This post was made in a cross-team DM channel so we need to find where its files were saved
|
||||
teamId = FindTeamIdForFilename(post, filenames[0])
|
||||
} else {
|
||||
teamId = channel.TeamId
|
||||
}
|
||||
|
||||
// Create FileInfo objects for this post
|
||||
infos := make([]*model.FileInfo, 0, len(filenames))
|
||||
if teamId == "" {
|
||||
l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.team_id.error"), post.Id, filenames)
|
||||
} else {
|
||||
for _, filename := range filenames {
|
||||
info := GetInfoForFilename(post, teamId, filename)
|
||||
if info == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
infos = append(infos, info)
|
||||
}
|
||||
}
|
||||
|
||||
// Lock to prevent only one migration thread from trying to update the post at once, preventing duplicate FileInfos from being created
|
||||
fileMigrationLock.Lock()
|
||||
defer fileMigrationLock.Unlock()
|
||||
|
||||
if result := <-Srv.Store.Post().Get(post.Id); result.Err != nil {
|
||||
l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.get_post_again.app_error"), post.Id, result.Err)
|
||||
return []*model.FileInfo{}
|
||||
} else if newPost := result.Data.(*model.PostList).Posts[post.Id]; len(newPost.Filenames) != len(post.Filenames) {
|
||||
// Another thread has already created FileInfos for this post, so just return those
|
||||
if result := <-Srv.Store.FileInfo().GetForPost(post.Id); result.Err != nil {
|
||||
l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.get_post_file_infos_again.app_error"), post.Id, result.Err)
|
||||
return []*model.FileInfo{}
|
||||
} else {
|
||||
l4g.Debug(utils.T("api.file.migrate_filenames_to_file_infos.not_migrating_post.debug"), post.Id)
|
||||
return result.Data.([]*model.FileInfo)
|
||||
}
|
||||
}
|
||||
|
||||
l4g.Debug(utils.T("api.file.migrate_filenames_to_file_infos.migrating_post.debug"), post.Id)
|
||||
|
||||
savedInfos := make([]*model.FileInfo, 0, len(infos))
|
||||
fileIds := make([]string, 0, len(filenames))
|
||||
for _, info := range infos {
|
||||
if result := <-Srv.Store.FileInfo().Save(info); result.Err != nil {
|
||||
l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.save_file_info.app_error"), post.Id, info.Id, info.Path, result.Err)
|
||||
continue
|
||||
}
|
||||
|
||||
savedInfos = append(savedInfos, info)
|
||||
fileIds = append(fileIds, info.Id)
|
||||
}
|
||||
|
||||
// Copy and save the updated post
|
||||
newPost := &model.Post{}
|
||||
*newPost = *post
|
||||
|
||||
newPost.Filenames = []string{}
|
||||
newPost.FileIds = fileIds
|
||||
|
||||
// Update Posts to clear Filenames and set FileIds
|
||||
if result := <-Srv.Store.Post().Update(newPost, post); result.Err != nil {
|
||||
l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.save_post.app_error"), post.Id, newPost.FileIds, post.Filenames, result.Err)
|
||||
return []*model.FileInfo{}
|
||||
} else {
|
||||
return savedInfos
|
||||
}
|
||||
}
|
||||
|
||||
func GeneratePublicLink(siteURL string, info *model.FileInfo) string {
|
||||
hash := GeneratePublicLinkHash(info.Id, *utils.Cfg.FileSettings.PublicLinkSalt)
|
||||
return fmt.Sprintf("%s%s/public/files/%v/get?h=%s", siteURL, model.API_URL_SUFFIX, info.Id, hash)
|
||||
}
|
||||
|
||||
func GeneratePublicLinkHash(fileId, salt string) string {
|
||||
hash := sha256.New()
|
||||
hash.Write([]byte(salt))
|
||||
hash.Write([]byte(fileId))
|
||||
|
||||
return base64.RawURLEncoding.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
33
app/file_test.go
Обычный файл
33
app/file_test.go
Обычный файл
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/platform/model"
|
||||
)
|
||||
|
||||
func TestGeneratePublicLinkHash(t *testing.T) {
|
||||
filename1 := model.NewId() + "/" + model.NewRandomString(16) + ".txt"
|
||||
filename2 := model.NewId() + "/" + model.NewRandomString(16) + ".txt"
|
||||
salt1 := model.NewRandomString(32)
|
||||
salt2 := model.NewRandomString(32)
|
||||
|
||||
hash1 := GeneratePublicLinkHash(filename1, salt1)
|
||||
hash2 := GeneratePublicLinkHash(filename2, salt1)
|
||||
hash3 := GeneratePublicLinkHash(filename1, salt2)
|
||||
|
||||
if hash1 != GeneratePublicLinkHash(filename1, salt1) {
|
||||
t.Fatal("hash should be equal for the same file name and salt")
|
||||
}
|
||||
|
||||
if hash1 == hash2 {
|
||||
t.Fatal("hashes for different files should not be equal")
|
||||
}
|
||||
|
||||
if hash1 == hash3 {
|
||||
t.Fatal("hashes for the same file with different salts should not be equal")
|
||||
}
|
||||
}
|
||||
@@ -25,192 +25,189 @@ import (
|
||||
)
|
||||
|
||||
func SendNotifications(post *model.Post, team *model.Team, channel *model.Channel) ([]string, *model.AppError) {
|
||||
mentionedUsersList := make([]string, 0)
|
||||
var fchan store.StoreChannel
|
||||
var senderUsername string
|
||||
pchan := Srv.Store.User().GetProfilesInChannel(channel.Id, -1, -1, true)
|
||||
fchan := Srv.Store.FileInfo().GetForPost(post.Id)
|
||||
|
||||
if post.IsSystemMessage() {
|
||||
senderUsername = utils.T("system.message.name")
|
||||
var profileMap map[string]*model.User
|
||||
if result := <-pchan; result.Err != nil {
|
||||
return nil, result.Err
|
||||
} else {
|
||||
pchan := Srv.Store.User().GetProfilesInChannel(channel.Id, -1, -1, true)
|
||||
fchan = Srv.Store.FileInfo().GetForPost(post.Id)
|
||||
profileMap = result.Data.(map[string]*model.User)
|
||||
}
|
||||
|
||||
var profileMap map[string]*model.User
|
||||
if result := <-pchan; result.Err != nil {
|
||||
// If the user who made the post isn't in the channel, don't send a notification
|
||||
if _, ok := profileMap[post.UserId]; !ok {
|
||||
l4g.Debug(utils.T("api.post.send_notifications.user_id.debug"), post.Id, channel.Id, post.UserId)
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
mentionedUserIds := make(map[string]bool)
|
||||
allActivityPushUserIds := []string{}
|
||||
hereNotification := false
|
||||
channelNotification := false
|
||||
allNotification := false
|
||||
updateMentionChans := []store.StoreChannel{}
|
||||
|
||||
if channel.Type == model.CHANNEL_DIRECT {
|
||||
var otherUserId string
|
||||
if userIds := strings.Split(channel.Name, "__"); userIds[0] == post.UserId {
|
||||
otherUserId = userIds[1]
|
||||
} else {
|
||||
otherUserId = userIds[0]
|
||||
}
|
||||
|
||||
mentionedUserIds[otherUserId] = true
|
||||
if post.Props["from_webhook"] == "true" {
|
||||
mentionedUserIds[post.UserId] = true
|
||||
}
|
||||
} else {
|
||||
keywords := GetMentionKeywordsInChannel(profileMap)
|
||||
|
||||
var potentialOtherMentions []string
|
||||
mentionedUserIds, potentialOtherMentions, hereNotification, channelNotification, allNotification = GetExplicitMentions(post.Message, keywords)
|
||||
|
||||
// get users that have comment thread mentions enabled
|
||||
if len(post.RootId) > 0 {
|
||||
if result := <-Srv.Store.Post().Get(post.RootId); result.Err != nil {
|
||||
return nil, result.Err
|
||||
} else {
|
||||
list := result.Data.(*model.PostList)
|
||||
|
||||
for _, threadPost := range list.Posts {
|
||||
profile := profileMap[threadPost.UserId]
|
||||
if profile.NotifyProps["comments"] == "any" || (profile.NotifyProps["comments"] == "root" && threadPost.Id == list.Order[0]) {
|
||||
mentionedUserIds[threadPost.UserId] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// prevent the user from mentioning themselves
|
||||
if post.Props["from_webhook"] != "true" {
|
||||
delete(mentionedUserIds, post.UserId)
|
||||
}
|
||||
|
||||
if len(potentialOtherMentions) > 0 {
|
||||
if result := <-Srv.Store.User().GetProfilesByUsernames(potentialOtherMentions, team.Id); result.Err == nil {
|
||||
outOfChannelMentions := result.Data.(map[string]*model.User)
|
||||
go sendOutOfChannelMentions(post, team.Id, outOfChannelMentions)
|
||||
}
|
||||
}
|
||||
|
||||
// find which users in the channel are set up to always receive mobile notifications
|
||||
for _, profile := range profileMap {
|
||||
if profile.NotifyProps["push"] == model.USER_NOTIFY_ALL &&
|
||||
(post.UserId != profile.Id || post.Props["from_webhook"] == "true") &&
|
||||
!post.IsSystemMessage() {
|
||||
allActivityPushUserIds = append(allActivityPushUserIds, profile.Id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mentionedUsersList := make([]string, 0, len(mentionedUserIds))
|
||||
for id := range mentionedUserIds {
|
||||
mentionedUsersList = append(mentionedUsersList, id)
|
||||
updateMentionChans = append(updateMentionChans, Srv.Store.Channel().IncrementMentionCount(post.ChannelId, id))
|
||||
}
|
||||
|
||||
var sender *model.User
|
||||
senderName := make(map[string]string)
|
||||
for _, id := range mentionedUsersList {
|
||||
senderName[id] = ""
|
||||
if post.IsSystemMessage() {
|
||||
senderName[id] = utils.T("system.message.name")
|
||||
} else if profile, ok := profileMap[post.UserId]; ok {
|
||||
if value, ok := post.Props["override_username"]; ok && post.Props["from_webhook"] == "true" {
|
||||
senderName[id] = value.(string)
|
||||
} else {
|
||||
// Get the Display name preference from the receiver
|
||||
if result := <-Srv.Store.Preference().Get(id, model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS, "name_format"); result.Err != nil {
|
||||
// Show default sender's name if user doesn't set display settings.
|
||||
senderName[id] = profile.Username
|
||||
} else {
|
||||
senderName[id] = profile.GetDisplayNameForPreference(result.Data.(model.Preference).Value)
|
||||
}
|
||||
}
|
||||
sender = profile
|
||||
}
|
||||
}
|
||||
|
||||
var senderUsername string
|
||||
if value, ok := post.Props["override_username"]; ok && post.Props["from_webhook"] == "true" {
|
||||
senderUsername = value.(string)
|
||||
} else {
|
||||
senderUsername = profileMap[post.UserId].Username
|
||||
}
|
||||
|
||||
if utils.Cfg.EmailSettings.SendEmailNotifications {
|
||||
for _, id := range mentionedUsersList {
|
||||
userAllowsEmails := profileMap[id].NotifyProps["email"] != "false"
|
||||
|
||||
var status *model.Status
|
||||
var err *model.AppError
|
||||
if status, err = GetStatus(id); err != nil {
|
||||
status = &model.Status{
|
||||
UserId: id,
|
||||
Status: model.STATUS_OFFLINE,
|
||||
Manual: false,
|
||||
LastActivityAt: 0,
|
||||
ActiveChannel: "",
|
||||
}
|
||||
}
|
||||
|
||||
if userAllowsEmails && status.Status != model.STATUS_ONLINE && profileMap[id].DeleteAt == 0 {
|
||||
sendNotificationEmail(post, profileMap[id], channel, team, senderName[id], sender)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
T := utils.GetUserTranslations(profileMap[post.UserId].Locale)
|
||||
|
||||
// If the channel has more than 1K users then @here is disabled
|
||||
if hereNotification && int64(len(profileMap)) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel {
|
||||
hereNotification = false
|
||||
SendEphemeralPost(
|
||||
team.Id,
|
||||
post.UserId,
|
||||
&model.Post{
|
||||
ChannelId: post.ChannelId,
|
||||
Message: T("api.post.disabled_here", map[string]interface{}{"Users": *utils.Cfg.TeamSettings.MaxNotificationsPerChannel}),
|
||||
CreateAt: post.CreateAt + 1,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// If the channel has more than 1K users then @channel is disabled
|
||||
if channelNotification && int64(len(profileMap)) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel {
|
||||
SendEphemeralPost(
|
||||
team.Id,
|
||||
post.UserId,
|
||||
&model.Post{
|
||||
ChannelId: post.ChannelId,
|
||||
Message: T("api.post.disabled_channel", map[string]interface{}{"Users": *utils.Cfg.TeamSettings.MaxNotificationsPerChannel}),
|
||||
CreateAt: post.CreateAt + 1,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// If the channel has more than 1K users then @all is disabled
|
||||
if allNotification && int64(len(profileMap)) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel {
|
||||
SendEphemeralPost(
|
||||
team.Id,
|
||||
post.UserId,
|
||||
&model.Post{
|
||||
ChannelId: post.ChannelId,
|
||||
Message: T("api.post.disabled_all", map[string]interface{}{"Users": *utils.Cfg.TeamSettings.MaxNotificationsPerChannel}),
|
||||
CreateAt: post.CreateAt + 1,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if hereNotification {
|
||||
if result := <-Srv.Store.Status().GetOnline(); result.Err != nil {
|
||||
return nil, result.Err
|
||||
} else {
|
||||
profileMap = result.Data.(map[string]*model.User)
|
||||
}
|
||||
|
||||
// If the user who made the post isn't in the channel don't send a notification
|
||||
if _, ok := profileMap[post.UserId]; !ok {
|
||||
l4g.Debug(utils.T("api.post.send_notifications.user_id.debug"), post.Id, channel.Id, post.UserId)
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
mentionedUserIds := make(map[string]bool)
|
||||
allActivityPushUserIds := []string{}
|
||||
hereNotification := false
|
||||
channelNotification := false
|
||||
allNotification := false
|
||||
updateMentionChans := []store.StoreChannel{}
|
||||
|
||||
if channel.Type == model.CHANNEL_DIRECT {
|
||||
var otherUserId string
|
||||
if userIds := strings.Split(channel.Name, "__"); userIds[0] == post.UserId {
|
||||
otherUserId = userIds[1]
|
||||
} else {
|
||||
otherUserId = userIds[0]
|
||||
}
|
||||
|
||||
mentionedUserIds[otherUserId] = true
|
||||
if post.Props["from_webhook"] == "true" {
|
||||
mentionedUserIds[post.UserId] = true
|
||||
}
|
||||
} else {
|
||||
keywords := GetMentionKeywordsInChannel(profileMap)
|
||||
|
||||
var potentialOtherMentions []string
|
||||
mentionedUserIds, potentialOtherMentions, hereNotification, channelNotification, allNotification = GetExplicitMentions(post.Message, keywords)
|
||||
|
||||
// get users that have comment thread mentions enabled
|
||||
if len(post.RootId) > 0 {
|
||||
if result := <-Srv.Store.Post().Get(post.RootId); result.Err != nil {
|
||||
return nil, result.Err
|
||||
} else {
|
||||
list := result.Data.(*model.PostList)
|
||||
|
||||
for _, threadPost := range list.Posts {
|
||||
if profile, ok := profileMap[threadPost.UserId]; ok {
|
||||
if profile.NotifyProps["comments"] == "any" || (profile.NotifyProps["comments"] == "root" && threadPost.Id == list.Order[0]) {
|
||||
mentionedUserIds[threadPost.UserId] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// prevent the user from mentioning themselves
|
||||
if post.Props["from_webhook"] != "true" {
|
||||
delete(mentionedUserIds, post.UserId)
|
||||
}
|
||||
|
||||
if len(potentialOtherMentions) > 0 {
|
||||
if result := <-Srv.Store.User().GetProfilesByUsernames(potentialOtherMentions, team.Id); result.Err == nil {
|
||||
outOfChannelMentions := result.Data.(map[string]*model.User)
|
||||
go sendOutOfChannelMentions(post, team.Id, outOfChannelMentions)
|
||||
}
|
||||
}
|
||||
|
||||
// find which users in the channel are set up to always receive mobile notifications
|
||||
for _, profile := range profileMap {
|
||||
if profile.NotifyProps["push"] == model.USER_NOTIFY_ALL &&
|
||||
(post.UserId != profile.Id || post.Props["from_webhook"] == "true") {
|
||||
allActivityPushUserIds = append(allActivityPushUserIds, profile.Id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mentionedUsersList = make([]string, 0, len(mentionedUserIds))
|
||||
for id := range mentionedUserIds {
|
||||
mentionedUsersList = append(mentionedUsersList, id)
|
||||
updateMentionChans = append(updateMentionChans, Srv.Store.Channel().IncrementMentionCount(post.ChannelId, id))
|
||||
}
|
||||
|
||||
var sender *model.User
|
||||
senderName := make(map[string]string)
|
||||
for _, id := range mentionedUsersList {
|
||||
senderName[id] = ""
|
||||
if profile, ok := profileMap[post.UserId]; ok {
|
||||
if value, ok := post.Props["override_username"]; ok && post.Props["from_webhook"] == "true" {
|
||||
senderName[id] = value.(string)
|
||||
} else {
|
||||
//Get the Display name preference from the receiver
|
||||
if result := <-Srv.Store.Preference().Get(id, model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS, "name_format"); result.Err != nil {
|
||||
// Show default sender's name if user doesn't set display settings.
|
||||
senderName[id] = profile.Username
|
||||
} else {
|
||||
senderName[id] = profile.GetDisplayNameForPreference(result.Data.(model.Preference).Value)
|
||||
}
|
||||
}
|
||||
sender = profile
|
||||
}
|
||||
}
|
||||
|
||||
if value, ok := post.Props["override_username"]; ok && post.Props["from_webhook"] == "true" {
|
||||
senderUsername = value.(string)
|
||||
} else {
|
||||
senderUsername = profileMap[post.UserId].Username
|
||||
}
|
||||
|
||||
if utils.Cfg.EmailSettings.SendEmailNotifications {
|
||||
for _, id := range mentionedUsersList {
|
||||
userAllowsEmails := profileMap[id].NotifyProps["email"] != "false"
|
||||
|
||||
var status *model.Status
|
||||
var err *model.AppError
|
||||
if status, err = GetStatus(id); err != nil {
|
||||
status = &model.Status{
|
||||
UserId: id,
|
||||
Status: model.STATUS_OFFLINE,
|
||||
Manual: false,
|
||||
LastActivityAt: 0,
|
||||
ActiveChannel: "",
|
||||
}
|
||||
}
|
||||
|
||||
if userAllowsEmails && status.Status != model.STATUS_ONLINE && profileMap[id].DeleteAt == 0 {
|
||||
if err := sendNotificationEmail(post, profileMap[id], channel, team, senderName[id], sender); err != nil {
|
||||
l4g.Error(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
T := utils.GetUserTranslations(profileMap[post.UserId].Locale)
|
||||
|
||||
// If the channel has more than 1K users then @here is disabled
|
||||
if hereNotification && int64(len(profileMap)) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel {
|
||||
hereNotification = false
|
||||
SendEphemeralPost(
|
||||
team.Id,
|
||||
post.UserId,
|
||||
&model.Post{
|
||||
ChannelId: post.ChannelId,
|
||||
Message: T("api.post.disabled_here", map[string]interface{}{"Users": *utils.Cfg.TeamSettings.MaxNotificationsPerChannel}),
|
||||
CreateAt: post.CreateAt + 1,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// If the channel has more than 1K users then @channel is disabled
|
||||
if channelNotification && int64(len(profileMap)) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel {
|
||||
SendEphemeralPost(
|
||||
team.Id,
|
||||
post.UserId,
|
||||
&model.Post{
|
||||
ChannelId: post.ChannelId,
|
||||
Message: T("api.post.disabled_channel", map[string]interface{}{"Users": *utils.Cfg.TeamSettings.MaxNotificationsPerChannel}),
|
||||
CreateAt: post.CreateAt + 1,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// If the channel has more than 1K users then @all is disabled
|
||||
if allNotification && int64(len(profileMap)) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel {
|
||||
SendEphemeralPost(
|
||||
team.Id,
|
||||
post.UserId,
|
||||
&model.Post{
|
||||
ChannelId: post.ChannelId,
|
||||
Message: T("api.post.disabled_all", map[string]interface{}{"Users": *utils.Cfg.TeamSettings.MaxNotificationsPerChannel}),
|
||||
CreateAt: post.CreateAt + 1,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if hereNotification {
|
||||
statuses := GetAllStatuses()
|
||||
statuses := result.Data.([]*model.Status)
|
||||
for _, status := range statuses {
|
||||
if status.UserId == post.UserId {
|
||||
continue
|
||||
@@ -225,29 +222,43 @@ func SendNotifications(post *model.Post, team *model.Team, channel *model.Channe
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure all mention updates are complete to prevent race
|
||||
// Probably better to batch these DB updates in the future
|
||||
// MUST be completed before push notifications send
|
||||
for _, uchan := range updateMentionChans {
|
||||
if result := <-uchan; result.Err != nil {
|
||||
l4g.Warn(utils.T("api.post.update_mention_count_and_forget.update_error"), post.Id, post.ChannelId, result.Err)
|
||||
// Make sure all mention updates are complete to prevent race
|
||||
// Probably better to batch these DB updates in the future
|
||||
// MUST be completed before push notifications send
|
||||
for _, uchan := range updateMentionChans {
|
||||
if result := <-uchan; result.Err != nil {
|
||||
l4g.Warn(utils.T("api.post.update_mention_count_and_forget.update_error"), post.Id, post.ChannelId, result.Err)
|
||||
}
|
||||
}
|
||||
|
||||
sendPushNotifications := false
|
||||
if *utils.Cfg.EmailSettings.SendPushNotifications {
|
||||
pushServer := *utils.Cfg.EmailSettings.PushNotificationServer
|
||||
if pushServer == model.MHPNS && (!utils.IsLicensed || !*utils.License.Features.MHPNS) {
|
||||
l4g.Warn(utils.T("api.post.send_notifications_and_forget.push_notification.mhpnsWarn"))
|
||||
sendPushNotifications = false
|
||||
} else {
|
||||
sendPushNotifications = true
|
||||
}
|
||||
}
|
||||
|
||||
if sendPushNotifications {
|
||||
for _, id := range mentionedUsersList {
|
||||
var status *model.Status
|
||||
var err *model.AppError
|
||||
if status, err = GetStatus(id); err != nil {
|
||||
status = &model.Status{id, model.STATUS_OFFLINE, false, 0, ""}
|
||||
}
|
||||
|
||||
if DoesStatusAllowPushNotification(profileMap[id], status, post.ChannelId) {
|
||||
sendPushNotification(post, profileMap[id], channel, senderName[id], true)
|
||||
}
|
||||
}
|
||||
|
||||
sendPushNotifications := false
|
||||
if *utils.Cfg.EmailSettings.SendPushNotifications {
|
||||
pushServer := *utils.Cfg.EmailSettings.PushNotificationServer
|
||||
if pushServer == model.MHPNS && (!utils.IsLicensed || !*utils.License.Features.MHPNS) {
|
||||
l4g.Warn(utils.T("api.post.send_notifications_and_forget.push_notification.mhpnsWarn"))
|
||||
sendPushNotifications = false
|
||||
} else {
|
||||
sendPushNotifications = true
|
||||
}
|
||||
}
|
||||
|
||||
if sendPushNotifications {
|
||||
for _, id := range mentionedUsersList {
|
||||
for _, id := range allActivityPushUserIds {
|
||||
if _, ok := mentionedUserIds[id]; !ok {
|
||||
var status *model.Status
|
||||
var err *model.AppError
|
||||
if status, err = GetStatus(id); err != nil {
|
||||
@@ -255,25 +266,7 @@ func SendNotifications(post *model.Post, team *model.Team, channel *model.Channe
|
||||
}
|
||||
|
||||
if DoesStatusAllowPushNotification(profileMap[id], status, post.ChannelId) {
|
||||
if err := sendPushNotification(post, profileMap[id], channel, senderName[id], true); err != nil {
|
||||
l4g.Error(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, id := range allActivityPushUserIds {
|
||||
if _, ok := mentionedUserIds[id]; !ok {
|
||||
var status *model.Status
|
||||
var err *model.AppError
|
||||
if status, err = GetStatus(id); err != nil {
|
||||
status = &model.Status{id, model.STATUS_OFFLINE, false, 0, ""}
|
||||
}
|
||||
|
||||
if DoesStatusAllowPushNotification(profileMap[id], status, post.ChannelId) {
|
||||
if err := sendPushNotification(post, profileMap[id], channel, senderName[id], false); err != nil {
|
||||
l4g.Error(err.Error())
|
||||
}
|
||||
}
|
||||
sendPushNotification(post, profileMap[id], channel, senderName[id], false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -287,7 +280,7 @@ func SendNotifications(post *model.Post, team *model.Team, channel *model.Channe
|
||||
message.Add("sender_name", senderUsername)
|
||||
message.Add("team_id", team.Id)
|
||||
|
||||
if len(post.FileIds) != 0 && fchan != nil {
|
||||
if len(post.FileIds) != 0 {
|
||||
message.Add("otherFile", "true")
|
||||
|
||||
var infos []*model.FileInfo
|
||||
@@ -314,7 +307,6 @@ func SendNotifications(post *model.Post, team *model.Team, channel *model.Channe
|
||||
}
|
||||
|
||||
func sendNotificationEmail(post *model.Post, user *model.User, channel *model.Channel, team *model.Team, senderName string, sender *model.User) *model.AppError {
|
||||
|
||||
if channel.Type == model.CHANNEL_DIRECT && channel.TeamId != team.Id {
|
||||
// this message is a cross-team DM so it we need to find a team that the recipient is on to use in the link
|
||||
if result := <-Srv.Store.Team().GetTeamsByUserId(user.Id); result.Err != nil {
|
||||
@@ -327,18 +319,15 @@ func sendNotificationEmail(post *model.Post, user *model.User, channel *model.Ch
|
||||
for i := range teams {
|
||||
if teams[i].Id == team.Id {
|
||||
found = true
|
||||
team = teams[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
if len(teams) > 0 {
|
||||
team = teams[0]
|
||||
} else {
|
||||
// in case the user hasn't joined any teams we send them to the select_team page
|
||||
team = &model.Team{Name: "select_team", DisplayName: utils.Cfg.TeamSettings.SiteName}
|
||||
}
|
||||
if !found && len(teams) > 0 {
|
||||
team = teams[0]
|
||||
} else {
|
||||
// in case the user hasn't joined any teams we send them to the select_team page
|
||||
team = &model.Team{Name: "select_team", DisplayName: utils.Cfg.TeamSettings.SiteName}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -511,8 +500,9 @@ func sendPushNotification(post *model.Post, user *model.User, channel *model.Cha
|
||||
tmpMessage := *model.PushNotificationFromJson(strings.NewReader(msg.ToJson()))
|
||||
tmpMessage.SetDeviceIdAndPlatform(session.DeviceId)
|
||||
if err := sendToPushProxy(tmpMessage); err != nil {
|
||||
l4g.Error(err.Error)
|
||||
return err
|
||||
}
|
||||
|
||||
if einterfaces.GetMetricsInterface() != nil {
|
||||
einterfaces.GetMetricsInterface().IncrementPostSentPush()
|
||||
}
|
||||
@@ -539,11 +529,12 @@ func ClearPushNotification(userId string, channelId string) *model.AppError {
|
||||
}
|
||||
|
||||
l4g.Debug(utils.T("api.post.send_notifications_and_forget.clear_push_notification.debug"), msg.DeviceId, msg.ChannelId)
|
||||
|
||||
for _, session := range sessions {
|
||||
tmpMessage := *model.PushNotificationFromJson(strings.NewReader(msg.ToJson()))
|
||||
tmpMessage.SetDeviceIdAndPlatform(session.DeviceId)
|
||||
if err := sendToPushProxy(tmpMessage); err != nil {
|
||||
l4g.Error(err.Error)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -551,6 +542,7 @@ func ClearPushNotification(userId string, channelId string) *model.AppError {
|
||||
}
|
||||
|
||||
func sendToPushProxy(msg model.PushNotification) *model.AppError {
|
||||
|
||||
msg.ServerId = utils.CfgDiagnosticId
|
||||
|
||||
tr := &http.Transport{
|
||||
|
||||
34
app/oauth.go
Обычный файл
34
app/oauth.go
Обычный файл
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"github.com/mattermost/platform/model"
|
||||
)
|
||||
|
||||
func RevokeAccessToken(token string) *model.AppError {
|
||||
|
||||
session, _ := GetSession(token)
|
||||
schan := Srv.Store.Session().Remove(token)
|
||||
|
||||
if result := <-Srv.Store.OAuth().GetAccessData(token); result.Err != nil {
|
||||
return model.NewLocAppError("RevokeAccessToken", "api.oauth.revoke_access_token.get.app_error", nil, "")
|
||||
}
|
||||
|
||||
tchan := Srv.Store.OAuth().RemoveAccessData(token)
|
||||
|
||||
if result := <-tchan; result.Err != nil {
|
||||
return model.NewLocAppError("RevokeAccessToken", "api.oauth.revoke_access_token.del_token.app_error", nil, "")
|
||||
}
|
||||
|
||||
if result := <-schan; result.Err != nil {
|
||||
return model.NewLocAppError("RevokeAccessToken", "api.oauth.revoke_access_token.del_session.app_error", nil, "")
|
||||
}
|
||||
|
||||
if session != nil {
|
||||
RemoveAllSessionsForUserId(session.UserId)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
16
app/preference.go
Обычный файл
16
app/preference.go
Обычный файл
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"github.com/mattermost/platform/model"
|
||||
)
|
||||
|
||||
func GetPreferencesForUser(userId string) (model.Preferences, *model.AppError) {
|
||||
if result := <-Srv.Store.Preference().GetAll(userId); result.Err != nil {
|
||||
return nil, result.Err
|
||||
} else {
|
||||
return result.Data.(model.Preferences), nil
|
||||
}
|
||||
}
|
||||
@@ -92,3 +92,55 @@ func InvalidateAllCaches() {
|
||||
func SessionCacheLength() int {
|
||||
return sessionCache.Len()
|
||||
}
|
||||
|
||||
func RevokeSessionsForDeviceId(userId string, deviceId string, currentSessionId string) *model.AppError {
|
||||
if result := <-Srv.Store.Session().GetSessions(userId); result.Err != nil {
|
||||
return result.Err
|
||||
} else {
|
||||
sessions := result.Data.([]*model.Session)
|
||||
for _, session := range sessions {
|
||||
if session.DeviceId == deviceId && session.Id != currentSessionId {
|
||||
l4g.Debug(utils.T("api.user.login.revoking.app_error"), session.Id, userId)
|
||||
if err := RevokeSession(session); err != nil {
|
||||
// Soft error so we still remove the other sessions
|
||||
l4g.Error(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func RevokeSessionById(sessionId string) *model.AppError {
|
||||
if result := <-Srv.Store.Session().Get(sessionId); result.Err != nil {
|
||||
return result.Err
|
||||
} else {
|
||||
return RevokeSession(result.Data.(*model.Session))
|
||||
}
|
||||
}
|
||||
|
||||
func RevokeSession(session *model.Session) *model.AppError {
|
||||
if session.IsOAuth {
|
||||
if err := RevokeAccessToken(session.Token); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if result := <-Srv.Store.Session().Remove(session.Id); result.Err != nil {
|
||||
return result.Err
|
||||
}
|
||||
}
|
||||
|
||||
RevokeWebrtcToken(session.Id)
|
||||
RemoveAllSessionsForUserId(session.UserId)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func AttachDeviceId(sessionId string, deviceId string, expiresAt int64) *model.AppError {
|
||||
if result := <-Srv.Store.Session().UpdateDeviceId(sessionId, deviceId, expiresAt); result.Err != nil {
|
||||
return result.Err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -80,3 +80,11 @@ func JoinUserToTeam(team *model.Team, user *model.User) *model.AppError {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetTeamsForUser(userId string) ([]*model.Team, *model.AppError) {
|
||||
if result := <-Srv.Store.Team().GetTeamsByUserId(userId); result.Err != nil {
|
||||
return nil, result.Err
|
||||
} else {
|
||||
return result.Data.([]*model.Team), nil
|
||||
}
|
||||
}
|
||||
|
||||
421
app/user.go
421
app/user.go
@@ -4,11 +4,105 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
l4g "github.com/alecthomas/log4go"
|
||||
"github.com/golang/freetype"
|
||||
"github.com/mattermost/platform/einterfaces"
|
||||
"github.com/mattermost/platform/model"
|
||||
"github.com/mattermost/platform/utils"
|
||||
)
|
||||
|
||||
func CreateUserWithHash(user *model.User, hash string, data string) (*model.User, *model.AppError) {
|
||||
props := model.MapFromJson(strings.NewReader(data))
|
||||
|
||||
if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) {
|
||||
return nil, model.NewLocAppError("CreateUserWithHash", "api.user.create_user.signup_link_invalid.app_error", nil, "")
|
||||
}
|
||||
|
||||
if t, err := strconv.ParseInt(props["time"], 10, 64); err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hours
|
||||
return nil, model.NewLocAppError("CreateUserWithHash", "api.user.create_user.signup_link_expired.app_error", nil, "")
|
||||
}
|
||||
|
||||
teamId := props["id"]
|
||||
|
||||
var team *model.Team
|
||||
if result := <-Srv.Store.Team().Get(teamId); result.Err != nil {
|
||||
return nil, result.Err
|
||||
} else {
|
||||
team = result.Data.(*model.Team)
|
||||
}
|
||||
|
||||
user.Email = props["email"]
|
||||
user.EmailVerified = true
|
||||
|
||||
var ruser *model.User
|
||||
var err *model.AppError
|
||||
if ruser, err = CreateUser(user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := JoinUserToTeam(team, ruser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
AddDirectChannels(team.Id, ruser)
|
||||
|
||||
return ruser, nil
|
||||
}
|
||||
|
||||
func CreateUserWithInviteId(user *model.User, inviteId string) (*model.User, *model.AppError) {
|
||||
var team *model.Team
|
||||
if result := <-Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil {
|
||||
return nil, result.Err
|
||||
} else {
|
||||
team = result.Data.(*model.Team)
|
||||
}
|
||||
|
||||
var ruser *model.User
|
||||
var err *model.AppError
|
||||
if ruser, err = CreateUser(user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := JoinUserToTeam(team, ruser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
AddDirectChannels(team.Id, ruser)
|
||||
|
||||
return ruser, nil
|
||||
}
|
||||
|
||||
func IsFirstUserAccount() bool {
|
||||
if SessionCacheLength() == 0 {
|
||||
if cr := <-Srv.Store.User().GetTotalUsersCount(); cr.Err != nil {
|
||||
l4g.Error(cr.Err)
|
||||
return false
|
||||
} else {
|
||||
count := cr.Data.(int64)
|
||||
if count <= 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func CreateUser(user *model.User) (*model.User, *model.AppError) {
|
||||
|
||||
user.Roles = model.ROLE_SYSTEM_USER.Id
|
||||
@@ -58,3 +152,330 @@ func CreateUser(user *model.User) (*model.User, *model.AppError) {
|
||||
return ruser, nil
|
||||
}
|
||||
}
|
||||
|
||||
func CreateOAuthUser(service string, userData io.Reader, teamId string) (*model.User, *model.AppError) {
|
||||
var user *model.User
|
||||
provider := einterfaces.GetOauthProvider(service)
|
||||
if provider == nil {
|
||||
return nil, model.NewLocAppError("CreateOAuthUser", "api.user.create_oauth_user.not_available.app_error", map[string]interface{}{"Service": strings.Title(service)}, "")
|
||||
} else {
|
||||
user = provider.GetUserFromJson(userData)
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
return nil, model.NewLocAppError("CreateOAuthUser", "api.user.create_oauth_user.create.app_error", map[string]interface{}{"Service": service}, "")
|
||||
}
|
||||
|
||||
suchan := Srv.Store.User().GetByAuth(user.AuthData, service)
|
||||
euchan := Srv.Store.User().GetByEmail(user.Email)
|
||||
|
||||
found := true
|
||||
count := 0
|
||||
for found {
|
||||
if found = IsUsernameTaken(user.Username); found {
|
||||
user.Username = user.Username + strconv.Itoa(count)
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
|
||||
if result := <-suchan; result.Err == nil {
|
||||
return nil, model.NewLocAppError("CreateOAuthUser", "api.user.create_oauth_user.already_used.app_error", map[string]interface{}{"Service": service}, "email="+user.Email)
|
||||
}
|
||||
|
||||
if result := <-euchan; result.Err == nil {
|
||||
authService := result.Data.(*model.User).AuthService
|
||||
if authService == "" {
|
||||
return nil, model.NewLocAppError("CreateOAuthUser", "api.user.create_oauth_user.already_attached.app_error",
|
||||
map[string]interface{}{"Service": service, "Auth": model.USER_AUTH_SERVICE_EMAIL}, "email="+user.Email)
|
||||
} else {
|
||||
return nil, model.NewLocAppError("CreateOAuthUser", "api.user.create_oauth_user.already_attached.app_error",
|
||||
map[string]interface{}{"Service": service, "Auth": authService}, "email="+user.Email)
|
||||
}
|
||||
}
|
||||
|
||||
user.EmailVerified = true
|
||||
|
||||
ruser, err := CreateUser(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(teamId) > 0 {
|
||||
err = JoinUserToTeamById(teamId, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = AddDirectChannels(teamId, user)
|
||||
if err != nil {
|
||||
l4g.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return ruser, nil
|
||||
}
|
||||
|
||||
// Check if the username is already used by another user. Return false if the username is invalid.
|
||||
func IsUsernameTaken(name string) bool {
|
||||
|
||||
if !model.IsValidUsername(name) {
|
||||
return false
|
||||
}
|
||||
|
||||
if result := <-Srv.Store.User().GetByUsername(name); result.Err != nil {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func GetUser(userId string) (*model.User, *model.AppError) {
|
||||
if result := <-Srv.Store.User().Get(userId); result.Err != nil {
|
||||
return nil, result.Err
|
||||
} else {
|
||||
return result.Data.(*model.User), nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetUserByUsername(username string) (*model.User, *model.AppError) {
|
||||
if result := <-Srv.Store.User().GetByUsername(username); result.Err != nil {
|
||||
return nil, result.Err
|
||||
} else {
|
||||
return result.Data.(*model.User), nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetUserByEmail(email string) (*model.User, *model.AppError) {
|
||||
if result := <-Srv.Store.User().GetByEmail(email); result.Err != nil {
|
||||
return nil, result.Err
|
||||
} else {
|
||||
return result.Data.(*model.User), nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetUserByAuth(authData *string, authService string) (*model.User, *model.AppError) {
|
||||
if result := <-Srv.Store.User().GetByAuth(authData, authService); result.Err != nil {
|
||||
return nil, result.Err
|
||||
} else {
|
||||
return result.Data.(*model.User), nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetUserForLogin(loginId string, onlyLdap bool) (*model.User, *model.AppError) {
|
||||
ldapAvailable := *utils.Cfg.LdapSettings.Enable && einterfaces.GetLdapInterface() != nil && utils.IsLicensed && *utils.License.Features.LDAP
|
||||
|
||||
if result := <-Srv.Store.User().GetForLogin(
|
||||
loginId,
|
||||
*utils.Cfg.EmailSettings.EnableSignInWithUsername && !onlyLdap,
|
||||
*utils.Cfg.EmailSettings.EnableSignInWithEmail && !onlyLdap,
|
||||
ldapAvailable,
|
||||
); result.Err != nil && result.Err.Id == "store.sql_user.get_for_login.multiple_users" {
|
||||
// don't fall back to LDAP in this case since we already know there's an LDAP user, but that it shouldn't work
|
||||
result.Err.StatusCode = http.StatusBadRequest
|
||||
return nil, result.Err
|
||||
} else if result.Err != nil {
|
||||
if !ldapAvailable {
|
||||
// failed to find user and no LDAP server to fall back on
|
||||
result.Err.StatusCode = http.StatusBadRequest
|
||||
return nil, result.Err
|
||||
}
|
||||
|
||||
// fall back to LDAP server to see if we can find a user
|
||||
if ldapUser, ldapErr := einterfaces.GetLdapInterface().GetUser(loginId); ldapErr != nil {
|
||||
ldapErr.StatusCode = http.StatusBadRequest
|
||||
return nil, ldapErr
|
||||
} else {
|
||||
return ldapUser, nil
|
||||
}
|
||||
} else {
|
||||
return result.Data.(*model.User), nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetUsers(offset int, limit int) (map[string]*model.User, *model.AppError) {
|
||||
if result := <-Srv.Store.User().GetAllProfiles(offset, limit); result.Err != nil {
|
||||
return nil, result.Err
|
||||
} else {
|
||||
return result.Data.(map[string]*model.User), nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetUsersEtag() string {
|
||||
return (<-Srv.Store.User().GetEtagForAllProfiles()).Data.(string)
|
||||
}
|
||||
|
||||
func GetUsersInTeam(teamId string, offset int, limit int) (map[string]*model.User, *model.AppError) {
|
||||
if result := <-Srv.Store.User().GetProfiles(teamId, offset, limit); result.Err != nil {
|
||||
return nil, result.Err
|
||||
} else {
|
||||
return result.Data.(map[string]*model.User), nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetUsersInTeamEtag(teamId string) string {
|
||||
return (<-Srv.Store.User().GetEtagForProfiles(teamId)).Data.(string)
|
||||
}
|
||||
|
||||
func GetUsersInChannel(channelId string, offset int, limit int) (map[string]*model.User, *model.AppError) {
|
||||
if result := <-Srv.Store.User().GetProfilesInChannel(channelId, offset, limit, false); result.Err != nil {
|
||||
return nil, result.Err
|
||||
} else {
|
||||
return result.Data.(map[string]*model.User), nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetUsersNotInChannel(teamId string, channelId string, offset int, limit int) (map[string]*model.User, *model.AppError) {
|
||||
if result := <-Srv.Store.User().GetProfilesNotInChannel(teamId, channelId, offset, limit); result.Err != nil {
|
||||
return nil, result.Err
|
||||
} else {
|
||||
return result.Data.(map[string]*model.User), nil
|
||||
}
|
||||
}
|
||||
|
||||
func ActivateMfa(userId, token string) *model.AppError {
|
||||
mfaInterface := einterfaces.GetMfaInterface()
|
||||
if mfaInterface == nil {
|
||||
err := model.NewLocAppError("ActivateMfa", "api.user.update_mfa.not_available.app_error", nil, "")
|
||||
err.StatusCode = http.StatusNotImplemented
|
||||
return err
|
||||
}
|
||||
|
||||
var user *model.User
|
||||
if result := <-Srv.Store.User().Get(userId); result.Err != nil {
|
||||
return result.Err
|
||||
} else {
|
||||
user = result.Data.(*model.User)
|
||||
}
|
||||
|
||||
if len(user.AuthService) > 0 && user.AuthService != model.USER_AUTH_SERVICE_LDAP {
|
||||
return model.NewLocAppError("ActivateMfa", "api.user.activate_mfa.email_and_ldap_only.app_error", nil, "")
|
||||
}
|
||||
|
||||
if err := mfaInterface.Activate(user, token); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeactivateMfa(userId string) *model.AppError {
|
||||
mfaInterface := einterfaces.GetMfaInterface()
|
||||
if mfaInterface == nil {
|
||||
err := model.NewLocAppError("DeactivateMfa", "api.user.update_mfa.not_available.app_error", nil, "")
|
||||
err.StatusCode = http.StatusNotImplemented
|
||||
return err
|
||||
}
|
||||
|
||||
if err := mfaInterface.Deactivate(userId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateProfileImage(username string, userId string) ([]byte, *model.AppError) {
|
||||
colors := []color.NRGBA{
|
||||
{197, 8, 126, 255},
|
||||
{227, 207, 18, 255},
|
||||
{28, 181, 105, 255},
|
||||
{35, 188, 224, 255},
|
||||
{116, 49, 196, 255},
|
||||
{197, 8, 126, 255},
|
||||
{197, 19, 19, 255},
|
||||
{250, 134, 6, 255},
|
||||
{227, 207, 18, 255},
|
||||
{123, 201, 71, 255},
|
||||
{28, 181, 105, 255},
|
||||
{35, 188, 224, 255},
|
||||
{116, 49, 196, 255},
|
||||
{197, 8, 126, 255},
|
||||
{197, 19, 19, 255},
|
||||
{250, 134, 6, 255},
|
||||
{227, 207, 18, 255},
|
||||
{123, 201, 71, 255},
|
||||
{28, 181, 105, 255},
|
||||
{35, 188, 224, 255},
|
||||
{116, 49, 196, 255},
|
||||
{197, 8, 126, 255},
|
||||
{197, 19, 19, 255},
|
||||
{250, 134, 6, 255},
|
||||
{227, 207, 18, 255},
|
||||
{123, 201, 71, 255},
|
||||
}
|
||||
|
||||
h := fnv.New32a()
|
||||
h.Write([]byte(userId))
|
||||
seed := h.Sum32()
|
||||
|
||||
initial := string(strings.ToUpper(username)[0])
|
||||
|
||||
fontBytes, err := ioutil.ReadFile(utils.FindDir("fonts") + utils.Cfg.FileSettings.InitialFont)
|
||||
if err != nil {
|
||||
return nil, model.NewLocAppError("CreateProfileImage", "api.user.create_profile_image.default_font.app_error", nil, err.Error())
|
||||
}
|
||||
font, err := freetype.ParseFont(fontBytes)
|
||||
if err != nil {
|
||||
return nil, model.NewLocAppError("CreateProfileImage", "api.user.create_profile_image.default_font.app_error", nil, err.Error())
|
||||
}
|
||||
|
||||
width := int(utils.Cfg.FileSettings.ProfileWidth)
|
||||
height := int(utils.Cfg.FileSettings.ProfileHeight)
|
||||
color := colors[int64(seed)%int64(len(colors))]
|
||||
dstImg := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||
srcImg := image.White
|
||||
draw.Draw(dstImg, dstImg.Bounds(), &image.Uniform{color}, image.ZP, draw.Src)
|
||||
size := float64((width + height) / 4)
|
||||
|
||||
c := freetype.NewContext()
|
||||
c.SetFont(font)
|
||||
c.SetFontSize(size)
|
||||
c.SetClip(dstImg.Bounds())
|
||||
c.SetDst(dstImg)
|
||||
c.SetSrc(srcImg)
|
||||
|
||||
pt := freetype.Pt(width/6, height*2/3)
|
||||
_, err = c.DrawString(initial, pt)
|
||||
if err != nil {
|
||||
return nil, model.NewLocAppError("CreateProfileImage", "api.user.create_profile_image.initial.app_error", nil, err.Error())
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
if imgErr := png.Encode(buf, dstImg); imgErr != nil {
|
||||
return nil, model.NewLocAppError("CreateProfileImage", "api.user.create_profile_image.encode.app_error", nil, imgErr.Error())
|
||||
} else {
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetProfileImage(user *model.User) ([]byte, *model.AppError) {
|
||||
var img []byte
|
||||
|
||||
if len(utils.Cfg.FileSettings.DriverName) == 0 {
|
||||
var err *model.AppError
|
||||
if img, err = CreateProfileImage(user.Username, user.Id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
path := "users/" + user.Id + "/profile.png"
|
||||
|
||||
if data, err := ReadFile(path); err != nil {
|
||||
if img, err = CreateProfileImage(user.Username, user.Id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user.LastPictureUpdate == 0 {
|
||||
if err := WriteFile(img, path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
img = data
|
||||
}
|
||||
}
|
||||
|
||||
return img, nil
|
||||
}
|
||||
|
||||
27
app/user_test.go
Обычный файл
27
app/user_test.go
Обычный файл
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsUsernameTaken(t *testing.T) {
|
||||
th := Setup().InitBasic()
|
||||
user := th.BasicUser
|
||||
taken := IsUsernameTaken(user.Username)
|
||||
|
||||
if !taken {
|
||||
t.Logf("the username '%v' should be taken", user.Username)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
newUsername := "randomUsername"
|
||||
taken = IsUsernameTaken(newUsername)
|
||||
|
||||
if taken {
|
||||
t.Logf("the username '%v' should not be taken", newUsername)
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
l4g "github.com/alecthomas/log4go"
|
||||
@@ -118,6 +119,10 @@ func handleWebhookEvents(post *model.Post, team *model.Team, channel *model.Chan
|
||||
}
|
||||
|
||||
func CreateWebhookPost(userId, teamId, channelId, text, overrideUsername, overrideIconUrl string, props model.StringInterface, postType string) (*model.Post, *model.AppError) {
|
||||
// parse links into Markdown format
|
||||
linkWithTextRegex := regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`)
|
||||
text = linkWithTextRegex.ReplaceAllString(text, "[${2}](${1})")
|
||||
|
||||
post := &model.Post{UserId: userId, ChannelId: channelId, Message: text, Type: postType}
|
||||
post.AddProp("from_webhook", "true")
|
||||
|
||||
@@ -135,12 +140,41 @@ func CreateWebhookPost(userId, teamId, channelId, text, overrideUsername, overri
|
||||
}
|
||||
}
|
||||
|
||||
post.Message = parseSlackLinksToMarkdown(post.Message)
|
||||
|
||||
if len(props) > 0 {
|
||||
for key, val := range props {
|
||||
if key == "attachments" {
|
||||
parseSlackAttachment(post, val)
|
||||
if list, success := val.([]interface{}); success {
|
||||
// parse attachment links into Markdown format
|
||||
for i, aInt := range list {
|
||||
attachment := aInt.(map[string]interface{})
|
||||
if aText, ok := attachment["text"].(string); ok {
|
||||
aText = linkWithTextRegex.ReplaceAllString(aText, "[${2}](${1})")
|
||||
attachment["text"] = aText
|
||||
list[i] = attachment
|
||||
}
|
||||
if aText, ok := attachment["pretext"].(string); ok {
|
||||
aText = linkWithTextRegex.ReplaceAllString(aText, "[${2}](${1})")
|
||||
attachment["pretext"] = aText
|
||||
list[i] = attachment
|
||||
}
|
||||
if fVal, ok := attachment["fields"]; ok {
|
||||
if fields, ok := fVal.([]interface{}); ok {
|
||||
// parse attachment field links into Markdown format
|
||||
for j, fInt := range fields {
|
||||
field := fInt.(map[string]interface{})
|
||||
if fValue, ok := field["value"].(string); ok {
|
||||
fValue = linkWithTextRegex.ReplaceAllString(fValue, "[${2}](${1})")
|
||||
field["value"] = fValue
|
||||
fields[j] = field
|
||||
}
|
||||
}
|
||||
attachment["fields"] = fields
|
||||
list[i] = attachment
|
||||
}
|
||||
}
|
||||
}
|
||||
post.AddProp(key, list)
|
||||
}
|
||||
} else if key != "override_icon_url" && key != "override_username" && key != "from_webhook" {
|
||||
post.AddProp(key, val)
|
||||
}
|
||||
|
||||
33
app/webtrc.go
Обычный файл
33
app/webtrc.go
Обычный файл
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/platform/model"
|
||||
"github.com/mattermost/platform/utils"
|
||||
)
|
||||
|
||||
func RevokeWebrtcToken(sessionId string) {
|
||||
token := base64.StdEncoding.EncodeToString([]byte(sessionId))
|
||||
data := make(map[string]string)
|
||||
data["janus"] = "remove_token"
|
||||
data["token"] = token
|
||||
data["transaction"] = model.NewId()
|
||||
data["admin_secret"] = *utils.Cfg.WebrtcSettings.GatewayAdminSecret
|
||||
|
||||
rq, _ := http.NewRequest("POST", *utils.Cfg.WebrtcSettings.GatewayAdminUrl, strings.NewReader(model.MapToJson(data)))
|
||||
rq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// we do not care about the response
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: *utils.Cfg.ServiceSettings.EnableInsecureOutgoingConnections},
|
||||
}
|
||||
httpClient := &http.Client{Transport: tr}
|
||||
httpClient.Do(rq)
|
||||
}
|
||||
@@ -781,7 +781,7 @@ func cmdResetMfa() {
|
||||
}
|
||||
}
|
||||
|
||||
if err := api.DeactivateMfa(user.Id); err != nil {
|
||||
if err := app.DeactivateMfa(user.Id); err != nil {
|
||||
l4g.Error("%v", err)
|
||||
flushLogAndExit(1)
|
||||
}
|
||||
|
||||
@@ -298,7 +298,7 @@ func resetUserMfaCmdF(cmd *cobra.Command, args []string) error {
|
||||
return errors.New("Unable to find user '" + args[i] + "'")
|
||||
}
|
||||
|
||||
if err := api.DeactivateMfa(user.Id); err != nil {
|
||||
if err := app.DeactivateMfa(user.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
12
i18n/en.json
12
i18n/en.json
@@ -301,19 +301,19 @@
|
||||
},
|
||||
{
|
||||
"id": "api.channel.post_update_channel_displayname_message_and_forget.create_post.error",
|
||||
"translation": "Failed to post displayname update message %v"
|
||||
"translation": "Failed to post displayname update message"
|
||||
},
|
||||
{
|
||||
"id": "api.channel.post_update_channel_displayname_message_and_forget.retrieve_user.error",
|
||||
"translation": "Failed to retrieve user while trying to save update channel displayname message %v"
|
||||
"translation": "Failed to retrieve user while trying to save update channel displayname message"
|
||||
},
|
||||
{
|
||||
"id": "api.channel.post_update_channel_displayname_message_and_forget.updated_from",
|
||||
"translation": "%s updated the channel display name from: %s to: %s"
|
||||
},
|
||||
{
|
||||
"id": "api.channel.post_update_channel_header_message_and_forget.join_leave.error",
|
||||
"translation": "Failed to post join/leave message %v"
|
||||
"id": "api.channel.post_update_channel_header_message_and_forget.post.error",
|
||||
"translation": "Failed to post update channel header message"
|
||||
},
|
||||
{
|
||||
"id": "api.channel.post_update_channel_header_message_and_forget.removed",
|
||||
@@ -333,7 +333,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.channel.post_user_add_remove_message_and_forget.error",
|
||||
"translation": "Failed to post join/leave message %v"
|
||||
"translation": "Failed to post join/leave message"
|
||||
},
|
||||
{
|
||||
"id": "api.channel.remove.default.app_error",
|
||||
@@ -2245,7 +2245,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.user.add_direct_channels_and_forget.failed.error",
|
||||
"translation": "Failed to add direct channel preferences for user user_id=%s, team_id=%s, err=%v"
|
||||
"translation": "Failed to add direct channel preferences for user user_id={{.UserId}}, team_id={{.TeamId}}, err={{.Error}}"
|
||||
},
|
||||
{
|
||||
"id": "api.user.authorize_oauth_user.bad_response.app_error",
|
||||
|
||||
Ссылка в новой задаче
Block a user