Merge branch 'master' into PLT-44

Этот коммит содержится в:
=Corey Hulen
2015-10-20 14:50:03 -07:00
родитель 1fc12dd8ba e0cda76eb8
Коммит e1f9459029
32 изменённых файлов: 776 добавлений и 155 удалений

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

@@ -59,6 +59,26 @@ Messaging and Notifications
- Slack import is unstable due to change in Slack export format
- Uploading a .flac file breaks the file previewer on iOS
### Compatibility
#### Config.json Changes from v1.0 to v1.1
##### Service Settings
Multiple settings were added to [`config.json`](./config/config.json) and System Console UI. Prior to upgrading the Mattermost binaries from the previous versions, these options would need to be manually updated in existing config.json file. This is a list of changes and their new default values in a fresh install:
- Under `ServiceSettings` in `config.json`:
- Added: `"EnablePostIconOverride": false` to control whether webhooks can override profile pictures
- Added: `"EnablePostUsernameOverride": false` to control whether webhooks can override profile pictures
- Added: `"EnableSecurityFixAlert": true` to control whether the system is alerted to security updates
#### Database Changes from v1.0 to v1.1
The following is for informational purposes only, no action needed. Mattermost automatically upgrades database tables from the previous version's schema using only additions. Sessions table is dropped and rebuilt, no team data is affected by this.
##### ChannelMembers Table
1. Removed `NotifyLevel` column
2. Added `NotifyProps` column with type `varchar(2000)` and default value `{}`
### Contributors
Many thanks to our external contributors. In no particular order:
@@ -180,6 +200,148 @@ Licensing
- Fixed issue so that SSO option automatically set EmailVerified=true (it was false previously)
### Compatibility
A large number of settings were changed in [`config.json`](./config/config.json) and a System Console UI was added. This is a very large change due to Mattermost releasing as v1.0 and it's unlikely a change of this size would happen again.
Prior to upgrading the Mattermost binaries from the previous versions, the below options would need to be manually updated in existing config.json file to migrate successfully. This is a list of changes and their new default values in a fresh install:
#### Config.json Changes from v0.7 to v1.0
##### Service Settings
- Under `ServiceSettings` in [`config.json`](./config/config.json):
- **Moved:** `"SiteName": "Mattermost"` which was added to `TeamSettings`
- **Removed:** `"Mode" : "dev"` which deprecates a high level dev mode, now replaced by granular controls
- **Renamed:** `"AllowTesting" : false` to `"EnableTesting": false` which allows the use of `/loadtest` slash commands during development
- **Removed:** `"UseSSL": false` boolean replaced by `"ConnectionSecurity": ""` under `Security` with new options: _None_ (`""`), _TLS_ (`"TLS"`) and _StartTLS_ ('"StartTLS"`)
- **Renamed**: `"Port": "8065"` to `"ListenAddress": ":8065"` to define address on which to listen. Must be prepended with a colon.
- **Removed:** `"Version": "developer"` removed and version information now stored in `model/version.go`
- **Removed:** `"Shards": {}` which was not used
- **Moved:** `"InviteSalt": "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6"` to `EmailSettings`
- **Moved:** `"PublicLinkSalt": "TO3pTyXIZzwHiwyZgGql7lM7DG3zeId4"` to `FileSettings`
- **Renamed and Moved** `"ResetSalt": "IPxFzSfnDFsNsRafZxz8NaYqFKhf9y2t"` to `"PasswordResetSalt": "vZ4DcKyVVRlKHHJpexcuXzojkE5PZ5eL"` and moved to `EmailSettings`
- **Removed:** `"AnalyticsUrl": ""` which was not used
- **Removed:** `"UseLocalStorage": true` which is replaced by `"DriverName": "local"` in `FileSettings`
- **Renamed and Moved:** `"StorageDirectory": "./data/"` to `Directory` and moved to `FileSettings`
- **Renamed:** `"AllowedLoginAttempts": 10` to `"MaximumLoginAttempts": 10`
- **Renamed, Reversed and Moved:** `"DisableEmailSignUp": false` renamed `"EnableSignUpWithEmail": true`, reversed meaning of `true`, and moved to `EmailSettings`
- **Added:** `"EnableOAuthServiceProvider": false` to enable OAuth2 service provider functionality
- **Added:** `"EnableIncomingWebhooks": false` to enable incoming webhooks feature
##### Team Settings
- Under `TeamSettings` in [`config.json`](./config/config.json):
- **Renamed:** `"AllowPublicLink": true` renamed to `"EnablePublicLink": true` and moved to `FileSettings`
- **Removed:** `AllowValetDefault` which was a guest account feature that is deprecated
- **Removed:** `"TermsLink": "/static/help/configure_links.html"` removed since option didn't need configuration
- **Removed:** `"PrivacyLink": "/static/help/configure_links.html"` removed since option didn't need configuration
- **Removed:** `"AboutLink": "/static/help/configure_links.html"` removed since option didn't need configuration
- **Removed:** `"HelpLink": "/static/help/configure_links.html"` removed since option didn't need configuration
- **Removed:** `"ReportProblemLink": "/static/help/configure_links.html"` removed since option didn't need configuration
- **Removed:** `"TourLink": "/static/help/configure_links.html"` removed since option didn't need configuration
- **Removed:** `"DefaultThemeColor": "#2389D7"` removed since theme colors changed from 1 to 18, default theme color option may be added back later after theme color design stablizes
- **Renamed:** `"DisableTeamCreation": false` to `"EnableUserCreation": true` and reversed
- **Added:** ` "EnableUserCreation": true` added to disable ability to create new user accounts in the system
##### SSO Settings
- Under `SSOSettings` in [`config.json`](./config/config.json):
- **Renamed Category:** `SSOSettings` to `GitLabSettings`
- **Renamed:** `"Allow": false` to `"Enable": false` to enable GitLab SSO
##### AWS Settings
- Under `AWSSettings` in [`config.json`](./config/config.json):
- This section was removed and settings moved to `FileSettings`
- **Renamed and Moved:** `"S3AccessKeyId": ""` renamed `"AmazonS3AccessKeyId": "",` and moved to `FileSettings`
- **Renamed and Moved:** `"S3SecretAccessKey": ""` renamed `"AmazonS3SecretAccessKey": "",` and moved to `FileSettings`
- **Renamed and Moved:** `"S3Bucket": ""` renamed `"AmazonS3Bucket": "",` and moved to `FileSettings`
- **Renamed and Moved:** `"S3Region": ""` renamed `"AmazonS3Region": "",` and moved to `FileSettings`
##### Image Settings
- Under `ImageSettings` in [`config.json`](./config/config.json):
- **Renamed:** `"ImageSettings"` section to `"FileSettings"`
- **Added:** `"DriverName" : "local"` to specify the file storage method, `amazons3` can also be used to setup S3
##### EmailSettings
- Under `EmailSettings` in [`config.json`](./config/config.json):
- **Removed:** `"ByPassEmail": "true"` which is replaced with `SendEmailNotifications` and `RequireEmailVerification`
- **Added:** `"SendEmailNotifications" : "false"` to control whether email notifications are sent
- **Added:** `"RequireEmailVerification" : "false"` to control if users need to verify their emails
- **Replaced:** `"UseTLS": "false"` with `"ConnectionSecurity": ""` with options: _None_ (`""`), _TLS_ (`"TLS"`) and _StartTLS_ ('"StartTLS"`)
- **Replaced:** `"UseStartTLS": "false"` with `"ConnectionSecurity": ""` with options: _None_ (`""`), _TLS_ (`"TLS"`) and _StartTLS_ ('"StartTLS"`)
##### Privacy Settings
- Under `PrivacySettings` in [`config.json`](./config/config.json):
- **Removed:** `"ShowPhoneNumber": "true"` which was not used
- **Removed:** `"ShowSkypeId" : "true"` which was not used
### Database Changes from v0.7 to v1.0
The following is for informational purposes only, no action needed. Mattermost automatically upgrades database tables from the previous version's schema using only additions. Sessions table is dropped and rebuilt, no team data is affected by this.
##### Users Table
1. Added `ThemeProps` column with type `varchar(2000)` and default value `{}`
##### Teams Table
1. Removed `AllowValet` column
##### Sessions Table
1. Renamed `Id` column `Token`
2. Renamed `AltId` column `Id`
3. Added `IsOAuth` column with type `tinyint(1)` and default value `0`
##### OAuthAccessData Table
1. Added new table `OAuthAccessData`
2. Added `AuthCode` column with type `varchar(128)`
3. Added `Token` column with type `varchar(26)` as the primary key
4. Added `RefreshToken` column with type `varchar(26)`
5. Added `RedirectUri` column with type `varchar(256)`
6. Added index on `AuthCode` column
##### OAuthApps Table
1. Added new table `OAuthApps`
2. Added `Id` column with type `varchar(26)` as primary key
2. Added `CreatorId` column with type `varchar(26)`
2. Added `CreateAt` column with type `bigint(20)`
2. Added `UpdateAt` column with type `bigint(20)`
2. Added `ClientSecret` column with type `varchar(128)`
2. Added `Name` column with type `varchar(64)`
2. Added `Description` column with type `varchar(512)`
2. Added `CallbackUrls` column with type `varchar(1024)`
2. Added `Homepage` column with type `varchar(256)`
3. Added index on `CreatorId` column
##### OAuthAuthData Table
1. Added new table `OAuthAuthData`
2. Added `ClientId` column with type `varchar(26)`
2. Added `UserId` column with type `varchar(26)`
2. Added `Code` column with type `varchar(128)` as primary key
2. Added `ExpiresIn` column with type `int(11)`
2. Added `CreateAt` column with type `bigint(20)`
2. Added `State` column with type `varchar(128)`
2. Added `Scope` column with type `varchar(128)`
##### IncomingWebhooks Table
1. Added new table `IncomingWebhooks`
2. Added `Id` column with type `varchar(26)` as primary key
2. Added `CreateAt` column with type `bigint(20)`
2. Added `UpdateAt` column with type `bigint(20)`
2. Added `DeleteAt` column with type `bigint(20)`
2. Added `UserId` column with type `varchar(26)`
2. Added `ChannelId` column with type `varchar(26)`
2. Added `TeamId` column with type `varchar(26)`
3. Added index on `UserId` column
3. Added index on `TeamId` column
##### Systems Table
1. Added new table `Systems`
2. Added `Name` column with type `varchar(64)` as primary key
3. Added `Value column with type `varchar(1024)`
### Contributors
Many thanks to our external contributors. In no particular order:

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

@@ -146,12 +146,12 @@ func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) {
resStruct.ClientIds = append(resStruct.ClientIds, clientId)
}
fireAndForgetHandleImages(imageNameList, imageDataList, c.Session.TeamId, channelId, c.Session.UserId)
handleImagesAndForget(imageNameList, imageDataList, c.Session.TeamId, channelId, c.Session.UserId)
w.Write([]byte(resStruct.ToJson()))
}
func fireAndForgetHandleImages(filenames []string, fileData [][]byte, teamId, channelId, userId string) {
func handleImagesAndForget(filenames []string, fileData [][]byte, teamId, channelId, userId string) {
go func() {
dest := "teams/" + teamId + "/channels/" + channelId + "/users/" + userId + "/"
@@ -311,7 +311,7 @@ func getFileInfo(c *Context, w http.ResponseWriter, r *http.Request) {
} else {
fileData := make(chan []byte)
asyncGetFile(path, fileData)
getFileAndForget(path, fileData)
f := <-fileData
@@ -378,7 +378,7 @@ func getFile(c *Context, w http.ResponseWriter, r *http.Request) {
}
fileData := make(chan []byte)
asyncGetFile(path, fileData)
getFileAndForget(path, fileData)
if len(hash) > 0 && len(data) > 0 && len(teamId) == 26 {
if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.FileSettings.PublicLinkSalt)) {
@@ -423,7 +423,7 @@ func getFile(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write(f)
}
func asyncGetFile(path string, fileData chan []byte) {
func getFileAndForget(path string, fileData chan []byte) {
go func() {
data, getErr := readFile(path)
if getErr != nil {

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

@@ -201,7 +201,7 @@ func handlePostEventsAndForget(c *Context, post *model.Post, triggerWebhooks boo
channel = result.Data.(*model.Channel)
}
fireAndForgetNotifications(c, post, team, channel)
sendNotificationsAndForget(c, post, team, channel)
var user *model.User
if result := <-uchan; result.Err != nil {
@@ -299,7 +299,7 @@ func handleWebhookEventsAndForget(c *Context, post *model.Post, team *model.Team
}
func fireAndForgetNotifications(c *Context, post *model.Post, team *model.Team, channel *model.Channel) {
func sendNotificationsAndForget(c *Context, post *model.Post, team *model.Team, channel *model.Channel) {
go func() {
// Get a list of user names (to be used as keywords) and ids for the given team
@@ -434,7 +434,7 @@ func fireAndForgetNotifications(c *Context, post *model.Post, team *model.Team,
}
for id := range toEmailMap {
fireAndForgetMentionUpdate(post.ChannelId, id)
updateMentionCountAndForget(post.ChannelId, id)
}
}
@@ -530,7 +530,7 @@ func fireAndForgetNotifications(c *Context, post *model.Post, team *model.Team,
alreadySeen[session.DeviceId] = session.DeviceId
utils.FireAndForgetSendAppleNotify(session.DeviceId, subjectPage.Render(), 1)
utils.SendAppleNotifyAndForget(session.DeviceId, subjectPage.Render(), 1)
}
}
}
@@ -562,7 +562,7 @@ func fireAndForgetNotifications(c *Context, post *model.Post, team *model.Team,
}()
}
func fireAndForgetMentionUpdate(channelId, userId string) {
func updateMentionCountAndForget(channelId, userId string) {
go func() {
if result := <-Srv.Store.Channel().IncrementMentionCount(channelId, userId); result.Err != nil {
l4g.Error("Failed to update mention count for user_id=%v on channel_id=%v err=%v", userId, channelId, result.Err)
@@ -820,16 +820,16 @@ func searchPosts(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
hashtagTerms, plainTerms := model.ParseHashtags(terms)
plainSearchParams, hashtagSearchParams := model.ParseSearchParams(terms)
var hchan store.StoreChannel
if len(hashtagTerms) != 0 {
hchan = Srv.Store.Post().Search(c.Session.TeamId, c.Session.UserId, hashtagTerms, true)
if hashtagSearchParams != nil {
hchan = Srv.Store.Post().Search(c.Session.TeamId, c.Session.UserId, hashtagSearchParams)
}
var pchan store.StoreChannel
if len(plainTerms) != 0 {
pchan = Srv.Store.Post().Search(c.Session.TeamId, c.Session.UserId, terms, false)
if plainSearchParams != nil {
pchan = Srv.Store.Post().Search(c.Session.TeamId, c.Session.UserId, plainSearchParams)
}
mainList := &model.PostList{}

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

@@ -406,6 +406,128 @@ func TestSearchHashtagPosts(t *testing.T) {
}
}
func TestSearchPostsInChannel(t *testing.T) {
Setup()
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User)
store.Must(Srv.Store.User().VerifyEmail(user1.Id))
Client.LoginByEmail(team.Name, user1.Email, "pwd")
channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
post1 := &model.Post{ChannelId: channel1.Id, Message: "sgtitlereview with space"}
post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post)
channel2 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel)
post2 := &model.Post{ChannelId: channel2.Id, Message: "sgtitlereview\n with return"}
post2 = Client.Must(Client.CreatePost(post2)).Data.(*model.Post)
post3 := &model.Post{ChannelId: channel2.Id, Message: "other message with no return"}
post3 = Client.Must(Client.CreatePost(post3)).Data.(*model.Post)
if result := Client.Must(Client.SearchPosts("channel:")).Data.(*model.PostList); len(result.Order) != 0 {
t.Fatalf("wrong number of posts returned %v", len(result.Order))
}
if result := Client.Must(Client.SearchPosts("in:")).Data.(*model.PostList); len(result.Order) != 0 {
t.Fatalf("wrong number of posts returned %v", len(result.Order))
}
if result := Client.Must(Client.SearchPosts("channel:" + channel1.Name)).Data.(*model.PostList); len(result.Order) != 1 {
t.Fatalf("wrong number of posts returned %v", len(result.Order))
}
if result := Client.Must(Client.SearchPosts("in: " + channel2.Name)).Data.(*model.PostList); len(result.Order) != 2 {
t.Fatalf("wrong number of posts returned %v", len(result.Order))
}
if result := Client.Must(Client.SearchPosts("channel: " + channel2.Name)).Data.(*model.PostList); len(result.Order) != 2 {
t.Fatalf("wrong number of posts returned %v", len(result.Order))
}
if result := Client.Must(Client.SearchPosts("ChAnNeL: " + channel2.Name)).Data.(*model.PostList); len(result.Order) != 2 {
t.Fatalf("wrong number of posts returned %v", len(result.Order))
}
if result := Client.Must(Client.SearchPosts("sgtitlereview")).Data.(*model.PostList); len(result.Order) != 2 {
t.Fatalf("wrong number of posts returned %v", len(result.Order))
}
if result := Client.Must(Client.SearchPosts("sgtitlereview in:")).Data.(*model.PostList); len(result.Order) != 2 {
t.Fatalf("wrong number of posts returned %v", len(result.Order))
}
if result := Client.Must(Client.SearchPosts("sgtitlereview channel:" + channel1.Name)).Data.(*model.PostList); len(result.Order) != 1 {
t.Fatalf("wrong number of posts returned %v", len(result.Order))
}
if result := Client.Must(Client.SearchPosts("sgtitlereview in: " + channel2.Name)).Data.(*model.PostList); len(result.Order) != 1 {
t.Fatalf("wrong number of posts returned %v", len(result.Order))
}
if result := Client.Must(Client.SearchPosts("sgtitlereview channel: " + channel2.Name)).Data.(*model.PostList); len(result.Order) != 1 {
t.Fatalf("wrong number of posts returned %v", len(result.Order))
}
}
func TestSearchPostsFromUser(t *testing.T) {
Setup()
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User)
store.Must(Srv.Store.User().VerifyEmail(user1.Id))
Client.LoginByEmail(team.Name, user1.Email, "pwd")
channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
channel2 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel)
post1 := &model.Post{ChannelId: channel1.Id, Message: "sgtitlereview with space"}
post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post)
user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User)
store.Must(Srv.Store.User().VerifyEmail(user2.Id))
Client.LoginByEmail(team.Name, user2.Email, "pwd")
Client.Must(Client.JoinChannel(channel1.Id))
Client.Must(Client.JoinChannel(channel2.Id))
post2 := &model.Post{ChannelId: channel2.Id, Message: "sgtitlereview\n with return"}
post2 = Client.Must(Client.CreatePost(post2)).Data.(*model.Post)
if result := Client.Must(Client.SearchPosts("from: " + user1.Username)).Data.(*model.PostList); len(result.Order) != 1 {
t.Fatalf("wrong number of posts returned %v", len(result.Order))
}
// note that this includes the "User2 has joined the channel" system messages
if result := Client.Must(Client.SearchPosts("from: " + user2.Username)).Data.(*model.PostList); len(result.Order) != 3 {
t.Fatalf("wrong number of posts returned %v", len(result.Order))
}
if result := Client.Must(Client.SearchPosts("from: " + user2.Username + " sgtitlereview")).Data.(*model.PostList); len(result.Order) != 1 {
t.Fatalf("wrong number of posts returned %v", len(result.Order))
}
if result := Client.Must(Client.SearchPosts("from: " + user2.Username + " in:" + channel1.Name)).Data.(*model.PostList); len(result.Order) != 1 {
t.Fatalf("wrong number of posts returned %v", len(result.Order))
}
}
func TestGetPostsCache(t *testing.T) {
Setup()

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

@@ -198,7 +198,7 @@ func CreateUser(c *Context, team *model.Team, user *model.User) *model.User {
l4g.Error("Encountered an issue joining default channels user_id=%s, team_id=%s, err=%v", ruser.Id, ruser.TeamId, err)
}
fireAndForgetWelcomeEmail(ruser.Id, ruser.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team), user.EmailVerified)
sendWelcomeEmailAndForget(ruser.Id, ruser.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team), user.EmailVerified)
addDirectChannelsAndForget(ruser)
@@ -219,7 +219,7 @@ func CreateUser(c *Context, team *model.Team, user *model.User) *model.User {
}
}
func fireAndForgetWelcomeEmail(userId, email, teamName, teamDisplayName, siteURL, teamURL string, verified bool) {
func sendWelcomeEmailAndForget(userId, email, teamName, teamDisplayName, siteURL, teamURL string, verified bool) {
go func() {
subjectPage := NewServerTemplatePage("welcome_subject")
@@ -278,7 +278,7 @@ func addDirectChannelsAndForget(user *model.User) {
}()
}
func FireAndForgetVerifyEmail(userId, userEmail, teamName, teamDisplayName, siteURL, teamURL string) {
func SendVerifyEmailAndForget(userId, userEmail, teamName, teamDisplayName, siteURL, teamURL string) {
go func() {
link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, userEmail)
@@ -922,10 +922,10 @@ func updateUser(c *Context, w http.ResponseWriter, r *http.Request) {
l4g.Error(tresult.Err.Message)
} else {
team := tresult.Data.(*model.Team)
fireAndForgetEmailChangeEmail(rusers[1].Email, rusers[0].Email, team.DisplayName, c.GetTeamURLFromTeam(team), c.GetSiteURL())
sendEmailChangeEmailAndForget(rusers[1].Email, rusers[0].Email, team.DisplayName, c.GetTeamURLFromTeam(team), c.GetSiteURL())
if utils.Cfg.EmailSettings.RequireEmailVerification {
FireAndForgetEmailChangeVerifyEmail(rusers[0].Id, rusers[0].Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team))
SendEmailChangeVerifyEmailAndForget(rusers[0].Id, rusers[0].Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team))
}
}
}
@@ -1005,7 +1005,7 @@ func updatePassword(c *Context, w http.ResponseWriter, r *http.Request) {
l4g.Error(tresult.Err.Message)
} else {
team := tresult.Data.(*model.Team)
fireAndForgetPasswordChangeEmail(user.Email, team.DisplayName, c.GetTeamURLFromTeam(team), c.GetSiteURL(), "using the settings menu")
sendPasswordChangeEmailAndForget(user.Email, team.DisplayName, c.GetTeamURLFromTeam(team), c.GetSiteURL(), "using the settings menu")
}
data := make(map[string]string)
@@ -1342,13 +1342,13 @@ func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) {
c.LogAuditWithUserId(userId, "success")
}
fireAndForgetPasswordChangeEmail(user.Email, team.DisplayName, c.GetTeamURLFromTeam(team), c.GetSiteURL(), "using a reset password link")
sendPasswordChangeEmailAndForget(user.Email, team.DisplayName, c.GetTeamURLFromTeam(team), c.GetSiteURL(), "using a reset password link")
props["new_password"] = ""
w.Write([]byte(model.MapToJson(props)))
}
func fireAndForgetPasswordChangeEmail(email, teamDisplayName, teamURL, siteURL, method string) {
func sendPasswordChangeEmailAndForget(email, teamDisplayName, teamURL, siteURL, method string) {
go func() {
subjectPage := NewServerTemplatePage("password_change_subject")
@@ -1367,7 +1367,7 @@ func fireAndForgetPasswordChangeEmail(email, teamDisplayName, teamURL, siteURL,
}()
}
func fireAndForgetEmailChangeEmail(oldEmail, newEmail, teamDisplayName, teamURL, siteURL string) {
func sendEmailChangeEmailAndForget(oldEmail, newEmail, teamDisplayName, teamURL, siteURL string) {
go func() {
subjectPage := NewServerTemplatePage("email_change_subject")
@@ -1386,7 +1386,7 @@ func fireAndForgetEmailChangeEmail(oldEmail, newEmail, teamDisplayName, teamURL,
}()
}
func FireAndForgetEmailChangeVerifyEmail(userId, newUserEmail, teamName, teamDisplayName, siteURL, teamURL string) {
func SendEmailChangeVerifyEmailAndForget(userId, newUserEmail, teamName, teamDisplayName, siteURL, teamURL string) {
go func() {
link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, newUserEmail)

16
doc/install/Upgrade-Guide.md Обычный файл
Просмотреть файл

@@ -0,0 +1,16 @@
# Mattermost Upgrade Guide
### Upgrading Mattermost v0.7 to v1.1
If you've manually changed Mattermost v0.7 configuration by updating the `config.json` file, you'll need to port those changes to Mattermost v1.1:
1. Go to the `config.json` file that you manually updated and note any differences from the [default `config.json` file in Mattermost 0.7](https://github.com/mattermost/platform/blob/v0.7.0/config/config.json).
2. For each setting that you changed, check [the changelog documentation](https://github.com/mattermost/platform/blob/master/CHANGELOG.md#configjson-changes-from-v07-to-v10) on whether the configuration setting has changed between v0.7 and v1.1
3. Update your new [`config.json` file in Mattermost v1.1](https://github.com/mattermost/platform/blob/v1.1.0/config/config.json), based on your preferences and the changelog documentation above.
Optionally, you can use the new [System Console user interface](https://github.com/mattermost/platform/blob/master/doc/install/Configuration-Settings.md) to make changes to your new `config.json` file.

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

@@ -0,0 +1,9 @@
# [GitLab Integration Service for Mattermost](https://github.com/mattermost/mattermost-integration-gitlab)
This [open source integration service](https://github.com/mattermost/mattermost-integration-gitlab) let you configure real-time notifications on GitLab issues, merge requests and comments to be delivered to selected Mattermost channels.
The service can be installed on any Linux-based web server and instructions for **Heroku** and **Ubuntu 14.04** are included. Please see [Mattermost incoming webhooks documentation](https://github.com/mattermost/platform/blob/master/doc/integrations/webhooks/Incoming-Webhooks.md) for details on formatting options within the service.
The Mattermost community is invited to fork, extend and repurpose this service for other applications. If you'd like your integration featured on http://mattermost.org/webhooks, please mail info@mattermost.org or tweet to us at @mattermosthq.
![webhooks](https://gitlab.com/gitlab-org/omnibus-gitlab/uploads/677b0aa055693c4dcabad0ee580c61b8/730_gitlab_feature_request.png)

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

@@ -13,26 +13,26 @@ Suppose you wanted to create a notification of the status of a daily build, with
```
payload={"text": "
***
---
##### Build Break - Project X - December 12, 2015 - 15:32 GMT +0
| Component | Tests Run | Tests Failed |
|:-----------|:------------|:-----------------------------------------------|
| Server | 948 | :white_check_mark: 0 |
| Web Client | 123 | :warning: [2 (see details)](http://linktologs) |
| iOS Client | 78 | :warning: [3 (see details)](http://linktologs) |
***
---
"}
```
Which would render in a Mattermost message as follows:
***
---
##### Build Break - Project X - December 12, 2015 - 15:32 GMT +0
| Component | Tests Run | Tests Failed |
|:------------ |:---------------|:-----|
| Server | 948 | :white_check_mark: 0 |
| Web Client | 123 | :warning: [2 (see details)](http://linktologs) |
| iOS Client | 78 | :warning: [3 (see details)](http://linktologs) |
***
| Component | Tests Run | Tests Failed |
|:-----------|:------------|:-----------------------------------------------|
| Server | 948 | :white_check_mark: 0 |
| Web Client | 123 | :warning: [2 (see details)](http://linktologs) |
| iOS Client | 78 | :warning: [3 (see details)](http://linktologs) |
---
### Enabling Incoming Webhooks
Incoming webhooks should be enabled on your Mattermost instance by default, but if they are not you'll need to get your system administrator to enable them. If you are the system administrator you can enable them by doing the following:

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

@@ -66,7 +66,7 @@ func main() {
manualtesting.InitManualTesting()
}
securityAndDiagnosticsJob()
runSecurityAndDiagnosticsJobAndForget()
// wait for kill signal before attempting to gracefully shutdown
// the running service
@@ -78,7 +78,7 @@ func main() {
}
}
func securityAndDiagnosticsJob() {
func runSecurityAndDiagnosticsJobAndForget() {
go func() {
for {
if *utils.Cfg.ServiceSettings.EnableSecurityFixAlert {

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

@@ -16,7 +16,7 @@ const (
type AccessData struct {
AuthCode string `json:"auth_code"`
Token string `json"token"`
Token string `json:"token"`
RefreshToken string `json:"refresh_token"`
RedirectUri string `json:"redirect_uri"`
}

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

@@ -185,8 +185,8 @@ func (o *Config) IsValid() *AppError {
return NewAppError("Config.IsValid", "Invalid maximum users per team for team settings. Must be a positive number.", "")
}
if len(o.SqlSettings.AtRestEncryptKey) != 32 {
return NewAppError("Config.IsValid", "Invalid at rest encrypt key for SQL settings. Must be 32 chars.", "")
if len(o.SqlSettings.AtRestEncryptKey) < 32 {
return NewAppError("Config.IsValid", "Invalid at rest encrypt key for SQL settings. Must be 32 chars or more.", "")
}
if !(o.SqlSettings.DriverName == DATABASE_DRIVER_MYSQL || o.SqlSettings.DriverName == DATABASE_DRIVER_POSTGRES) {
@@ -233,20 +233,20 @@ func (o *Config) IsValid() *AppError {
return NewAppError("Config.IsValid", "Invalid thumbnail width for file settings. Must be a positive number.", "")
}
if len(o.FileSettings.PublicLinkSalt) != 32 {
return NewAppError("Config.IsValid", "Invalid public link salt for file settings. Must be 32 chars.", "")
if len(o.FileSettings.PublicLinkSalt) < 32 {
return NewAppError("Config.IsValid", "Invalid public link salt for file settings. Must be 32 chars or more.", "")
}
if !(o.EmailSettings.ConnectionSecurity == CONN_SECURITY_NONE || o.EmailSettings.ConnectionSecurity == CONN_SECURITY_TLS || o.EmailSettings.ConnectionSecurity == CONN_SECURITY_STARTTLS) {
return NewAppError("Config.IsValid", "Invalid connection security for email settings. Must be '', 'TLS', or 'STARTTLS'", "")
}
if len(o.EmailSettings.InviteSalt) != 32 {
return NewAppError("Config.IsValid", "Invalid invite salt for email settings. Must be 32 chars.", "")
if len(o.EmailSettings.InviteSalt) < 32 {
return NewAppError("Config.IsValid", "Invalid invite salt for email settings. Must be 32 chars or more.", "")
}
if len(o.EmailSettings.PasswordResetSalt) != 32 {
return NewAppError("Config.IsValid", "Invalid password reset salt for email settings. Must be 32 chars.", "")
if len(o.EmailSettings.PasswordResetSalt) < 32 {
return NewAppError("Config.IsValid", "Invalid password reset salt for email settings. Must be 32 chars or more.", "")
}
if o.RateLimitSettings.MemoryStoreSize <= 0 {

130
model/search_params.go Обычный файл
Просмотреть файл

@@ -0,0 +1,130 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"strings"
)
type SearchParams struct {
Terms string
IsHashtag bool
InChannel string
FromUser string
}
var searchFlags = [...]string{"from", "channel", "in"}
func splitWords(text string) []string {
words := []string{}
for _, word := range strings.Fields(text) {
word = puncStart.ReplaceAllString(word, "")
word = puncEnd.ReplaceAllString(word, "")
if len(word) != 0 {
words = append(words, word)
}
}
return words
}
func parseSearchFlags(input []string) ([]string, map[string]string) {
words := []string{}
flags := make(map[string]string)
skipNextWord := false
for i, word := range input {
if skipNextWord {
skipNextWord = false
continue
}
isFlag := false
if colon := strings.Index(word, ":"); colon != -1 {
flag := word[:colon]
value := word[colon+1:]
for _, searchFlag := range searchFlags {
// check for case insensitive equality
if strings.EqualFold(flag, searchFlag) {
if value != "" {
flags[searchFlag] = value
isFlag = true
} else if i < len(input)-1 {
flags[searchFlag] = input[i+1]
skipNextWord = true
isFlag = true
}
if isFlag {
break
}
}
}
}
if !isFlag {
words = append(words, word)
}
}
return words, flags
}
func ParseSearchParams(text string) (*SearchParams, *SearchParams) {
words, flags := parseSearchFlags(splitWords(text))
hashtagTerms := []string{}
plainTerms := []string{}
for _, word := range words {
if validHashtag.MatchString(word) {
hashtagTerms = append(hashtagTerms, word)
} else {
plainTerms = append(plainTerms, word)
}
}
inChannel := flags["channel"]
if inChannel == "" {
inChannel = flags["in"]
}
fromUser := flags["from"]
var plainParams *SearchParams
if len(plainTerms) > 0 {
plainParams = &SearchParams{
Terms: strings.Join(plainTerms, " "),
IsHashtag: false,
InChannel: inChannel,
FromUser: fromUser,
}
}
var hashtagParams *SearchParams
if len(hashtagTerms) > 0 {
hashtagParams = &SearchParams{
Terms: strings.Join(hashtagTerms, " "),
IsHashtag: true,
InChannel: inChannel,
FromUser: fromUser,
}
}
// special case for when no terms are specified but we still have a filter
if plainParams == nil && hashtagParams == nil && (inChannel != "" || fromUser != "") {
plainParams = &SearchParams{
Terms: "",
IsHashtag: false,
InChannel: inChannel,
FromUser: fromUser,
}
}
return plainParams, hashtagParams
}

70
model/search_params_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,70 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"testing"
)
func TestParseSearchFlags(t *testing.T) {
if words, flags := parseSearchFlags(splitWords("")); len(words) != 0 {
t.Fatal("got words from empty input")
} else if len(flags) != 0 {
t.Fatal("got flags from empty input")
}
if words, flags := parseSearchFlags(splitWords("word")); len(words) != 1 || words[0] != "word" {
t.Fatalf("got incorrect words %v", words)
} else if len(flags) != 0 {
t.Fatalf("got incorrect flags %v", flags)
}
if words, flags := parseSearchFlags(splitWords("apple banana cherry")); len(words) != 3 || words[0] != "apple" || words[1] != "banana" || words[2] != "cherry" {
t.Fatalf("got incorrect words %v", words)
} else if len(flags) != 0 {
t.Fatalf("got incorrect flags %v", flags)
}
if words, flags := parseSearchFlags(splitWords("apple banana from:chan")); len(words) != 2 || words[0] != "apple" || words[1] != "banana" {
t.Fatalf("got incorrect words %v", words)
} else if len(flags) != 1 || flags["from"] != "chan" {
t.Fatalf("got incorrect flags %v", flags)
}
if words, flags := parseSearchFlags(splitWords("apple banana from: chan")); len(words) != 2 || words[0] != "apple" || words[1] != "banana" {
t.Fatalf("got incorrect words %v", words)
} else if len(flags) != 1 || flags["from"] != "chan" {
t.Fatalf("got incorrect flags %v", flags)
}
if words, flags := parseSearchFlags(splitWords("apple banana in: chan")); len(words) != 2 || words[0] != "apple" || words[1] != "banana" {
t.Fatalf("got incorrect words %v", words)
} else if len(flags) != 1 || flags["in"] != "chan" {
t.Fatalf("got incorrect flags %v", flags)
}
if words, flags := parseSearchFlags(splitWords("apple banana channel:chan")); len(words) != 2 || words[0] != "apple" || words[1] != "banana" {
t.Fatalf("got incorrect words %v", words)
} else if len(flags) != 1 || flags["channel"] != "chan" {
t.Fatalf("got incorrect flags %v", flags)
}
if words, flags := parseSearchFlags(splitWords("fruit: cherry")); len(words) != 2 || words[0] != "fruit:" || words[1] != "cherry" {
t.Fatalf("got incorrect words %v", words)
} else if len(flags) != 0 {
t.Fatalf("got incorrect flags %v", flags)
}
if words, flags := parseSearchFlags(splitWords("channel:")); len(words) != 1 || words[0] != "channel:" {
t.Fatalf("got incorrect words %v", words)
} else if len(flags) != 0 {
t.Fatalf("got incorrect flags %v", flags)
}
if words, flags := parseSearchFlags(splitWords("channel: first in: second from:")); len(words) != 1 || words[0] != "from:" {
t.Fatalf("got incorrect words %v", words)
} else if len(flags) != 2 || flags["channel"] != "first" || flags["in"] != "second" {
t.Fatalf("got incorrect flags %v", flags)
}
}

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

@@ -242,10 +242,10 @@ func Etag(parts ...interface{}) string {
var validHashtag = regexp.MustCompile(`^(#[A-Za-z]+[A-Za-z0-9_\-]*[A-Za-z0-9])$`)
var puncStart = regexp.MustCompile(`^[.,()&$!\[\]{}"':;\\]+`)
var puncEnd = regexp.MustCompile(`[.,()&$#!\[\]{}"':;\\]+$`)
var puncEnd = regexp.MustCompile(`[.,()&$#!\[\]{}"';\\]+$`)
func ParseHashtags(text string) (string, string) {
words := strings.Split(strings.Replace(text, "\n", " ", -1), " ")
words := strings.Fields(text)
hashtagString := ""
plainString := ""

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

@@ -407,15 +407,23 @@ var specialSearchChar = []string{
"@",
}
func (s SqlPostStore) Search(teamId string, userId string, terms string, isHashtagSearch bool) StoreChannel {
func (s SqlPostStore) Search(teamId string, userId string, params *model.SearchParams) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
termMap := map[string]bool{}
terms := params.Terms
if terms == "" && params.InChannel == "" && params.FromUser == "" {
result.Data = []*model.Post{}
storeChannel <- result
return
}
searchType := "Message"
if isHashtagSearch {
if params.IsHashtag {
searchType = "Hashtags"
for _, term := range strings.Split(terms, " ") {
termMap[term] = true
@@ -430,63 +438,85 @@ func (s SqlPostStore) Search(teamId string, userId string, terms string, isHasht
var posts []*model.Post
if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES {
// Parse text for wildcards
if wildcard, err := regexp.Compile("\\*($| )"); err == nil {
terms = wildcard.ReplaceAllLiteralString(terms, "* ")
}
}
searchQuery := `
SELECT
*
FROM
Posts
WHERE
DeleteAt = 0
POST_FILTER
AND ChannelId IN (
SELECT
Id
FROM
Channels,
ChannelMembers
WHERE
Id = ChannelId
AND TeamId = :TeamId
AND UserId = :UserId
AND DeleteAt = 0
CHANNEL_FILTER)
SEARCH_CLAUSE
ORDER BY CreateAt DESC
LIMIT 100`
if params.InChannel != "" {
searchQuery = strings.Replace(searchQuery, "CHANNEL_FILTER", "AND Name = :InChannel", 1)
} else {
searchQuery = strings.Replace(searchQuery, "CHANNEL_FILTER", "", 1)
}
if params.FromUser != "" {
searchQuery = strings.Replace(searchQuery, "POST_FILTER", `
AND UserId IN (
SELECT
Id
FROM
Users
WHERE
TeamId = :TeamId
AND Username = :FromUser)`, 1)
} else {
searchQuery = strings.Replace(searchQuery, "POST_FILTER", "", 1)
}
if terms == "" {
// we've already confirmed that we have a channel or user to search for
searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", "", 1)
} else if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES {
// Parse text for wildcards
if wildcard, err := regexp.Compile("\\*($| )"); err == nil {
terms = wildcard.ReplaceAllLiteralString(terms, ":* ")
}
searchQuery := fmt.Sprintf(`SELECT
*
FROM
Posts
WHERE
DeleteAt = 0
AND ChannelId IN (SELECT
Id
FROM
Channels,
ChannelMembers
WHERE
Id = ChannelId AND TeamId = $1
AND UserId = $2
AND DeleteAt = 0)
AND %s @@ to_tsquery($3)
ORDER BY CreateAt DESC
LIMIT 100`, searchType)
terms = strings.Join(strings.Fields(terms), " | ")
_, err := s.GetReplica().Select(&posts, searchQuery, teamId, userId, terms)
if err != nil {
result.Err = model.NewAppError("SqlPostStore.Search", "We encounted an error while searching for posts", "teamId="+teamId+", err="+err.Error())
}
searchClause := fmt.Sprintf("AND %s @@ to_tsquery(:Terms)", searchType)
searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", searchClause, 1)
} else if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_MYSQL {
searchQuery := fmt.Sprintf(`SELECT
*
FROM
Posts
WHERE
DeleteAt = 0
AND ChannelId IN (SELECT
Id
FROM
Channels,
ChannelMembers
WHERE
Id = ChannelId AND TeamId = ?
AND UserId = ?
AND DeleteAt = 0)
AND MATCH (%s) AGAINST (? IN BOOLEAN MODE)
ORDER BY CreateAt DESC
LIMIT 100`, searchType)
searchClause := fmt.Sprintf("AND MATCH (%s) AGAINST (:Terms IN BOOLEAN MODE)", searchType)
searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", searchClause, 1)
}
_, err := s.GetReplica().Select(&posts, searchQuery, teamId, userId, terms)
if err != nil {
result.Err = model.NewAppError("SqlPostStore.Search", "We encounted an error while searching for posts", "teamId="+teamId+", err="+err.Error())
queryParams := map[string]interface{}{
"TeamId": teamId,
"UserId": userId,
"Terms": terms,
"InChannel": params.InChannel,
"FromUser": params.FromUser,
}
}
_, err := s.GetReplica().Select(&posts, searchQuery, queryParams)
if err != nil {
result.Err = model.NewAppError("SqlPostStore.Search", "We encounted an error while searching for posts", "teamId="+teamId+", err="+err.Error())
}
list := &model.PostList{Order: make([]string, 0, len(posts))}

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

@@ -525,57 +525,57 @@ func TestPostStoreSearch(t *testing.T) {
o5.Hashtags = "#secret #howdy"
o5 = (<-store.Post().Save(o5)).Data.(*model.Post)
r1 := (<-store.Post().Search(teamId, userId, "corey", false)).Data.(*model.PostList)
r1 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "corey", IsHashtag: false})).Data.(*model.PostList)
if len(r1.Order) != 1 && r1.Order[0] != o1.Id {
t.Fatal("returned wrong search result")
}
r3 := (<-store.Post().Search(teamId, userId, "new", false)).Data.(*model.PostList)
r3 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "new", IsHashtag: false})).Data.(*model.PostList)
if len(r3.Order) != 2 && r3.Order[0] != o1.Id {
t.Fatal("returned wrong search result")
}
r4 := (<-store.Post().Search(teamId, userId, "john", false)).Data.(*model.PostList)
r4 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "john", IsHashtag: false})).Data.(*model.PostList)
if len(r4.Order) != 1 && r4.Order[0] != o2.Id {
t.Fatal("returned wrong search result")
}
r5 := (<-store.Post().Search(teamId, userId, "matter*", false)).Data.(*model.PostList)
r5 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "matter*", IsHashtag: false})).Data.(*model.PostList)
if len(r5.Order) != 1 && r5.Order[0] != o1.Id {
t.Fatal("returned wrong search result")
}
r6 := (<-store.Post().Search(teamId, userId, "#hashtag", true)).Data.(*model.PostList)
r6 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "#hashtag", IsHashtag: true})).Data.(*model.PostList)
if len(r6.Order) != 1 && r6.Order[0] != o4.Id {
t.Fatal("returned wrong search result")
}
r7 := (<-store.Post().Search(teamId, userId, "#secret", true)).Data.(*model.PostList)
r7 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "#secret", IsHashtag: true})).Data.(*model.PostList)
if len(r7.Order) != 1 && r7.Order[0] != o5.Id {
t.Fatal("returned wrong search result")
}
r8 := (<-store.Post().Search(teamId, userId, "@thisshouldmatchnothing", true)).Data.(*model.PostList)
r8 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "@thisshouldmatchnothing", IsHashtag: true})).Data.(*model.PostList)
if len(r8.Order) != 0 {
t.Fatal("returned wrong search result")
}
r9 := (<-store.Post().Search(teamId, userId, "mattermost jersey", false)).Data.(*model.PostList)
r9 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "mattermost jersey", IsHashtag: false})).Data.(*model.PostList)
if len(r9.Order) != 2 {
t.Fatal("returned wrong search result")
}
r10 := (<-store.Post().Search(teamId, userId, "matter* jer*", false)).Data.(*model.PostList)
r10 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "matter* jer*", IsHashtag: false})).Data.(*model.PostList)
if len(r10.Order) != 2 {
t.Fatal("returned wrong search result")
}
r11 := (<-store.Post().Search(teamId, userId, "message blargh", false)).Data.(*model.PostList)
r11 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "message blargh", IsHashtag: false})).Data.(*model.PostList)
if len(r11.Order) != 1 {
t.Fatal("returned wrong search result")
}
r12 := (<-store.Post().Search(teamId, userId, "blargh>", false)).Data.(*model.PostList)
r12 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "blargh>", IsHashtag: false})).Data.(*model.PostList)
if len(r12.Order) != 1 {
t.Fatal("returned wrong search result")
}

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

@@ -79,7 +79,14 @@ func NewSqlStore() Store {
// Check to see if it's the most current database schema version
if !model.IsCurrentVersion(schemaVersion) {
// If we are upgrading from the previous version then print a warning and continue
if model.IsPreviousVersion(schemaVersion) {
// Special case
isSchemaVersion07 := false
if schemaVersion == "0.7.1" || schemaVersion == "0.7.0" {
isSchemaVersion07 = true
}
if model.IsPreviousVersion(schemaVersion) || isSchemaVersion07 {
l4g.Warn("The database schema version of " + schemaVersion + " appears to be out of date")
l4g.Warn("Attempting to upgrade the database schema version to " + model.CurrentVersion)
} else {
@@ -91,6 +98,13 @@ func NewSqlStore() Store {
}
}
// REMOVE in 1.2
if sqlStore.DoesTableExist("Sessions") {
if sqlStore.DoesColumnExist("Sessions", "AltId") {
sqlStore.GetMaster().Exec("DROP TABLE IF EXISTS Sessions")
}
}
sqlStore.team = NewSqlTeamStore(sqlStore)
sqlStore.channel = NewSqlChannelStore(sqlStore)
sqlStore.post = NewSqlPostStore(sqlStore)

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

@@ -28,6 +28,8 @@ func NewSqlTeamStore(sqlStore *SqlStore) TeamStore {
}
func (s SqlTeamStore) UpgradeSchemaIfNeeded() {
// REMOVE in 1.2
s.RemoveColumnIfExists("Teams", "AllowValet")
}
func (s SqlTeamStore) CreateIndexesIfNotExists() {

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

@@ -41,6 +41,8 @@ func NewSqlUserStore(sqlStore *SqlStore) UserStore {
}
func (us SqlUserStore) UpgradeSchemaIfNeeded() {
// REMOVE in 1.2
us.CreateColumnIfNotExists("Users", "ThemeProps", "varchar(2000)", "character varying(2000)", "{}")
}
func (us SqlUserStore) CreateIndexesIfNotExists() {

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

@@ -84,7 +84,7 @@ type PostStore interface {
GetPosts(channelId string, offset int, limit int) StoreChannel
GetPostsSince(channelId string, time int64) StoreChannel
GetEtag(channelId string) StoreChannel
Search(teamId string, userId string, terms string, isHashtagSearch bool) StoreChannel
Search(teamId string, userId string, params *model.SearchParams) StoreChannel
GetForExport(channelId string) StoreChannel
}

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

@@ -10,7 +10,7 @@ import (
"github.com/mattermost/platform/model"
)
func FireAndForgetSendAppleNotify(deviceId string, message string, badge int) {
func SendAppleNotifyAndForget(deviceId string, message string, badge int) {
go func() {
if err := SendAppleNotify(deviceId, message, badge); err != nil {
l4g.Error(fmt.Sprintf("%v %v", err.Message, err.DetailedError))

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

@@ -32,6 +32,7 @@ export default class CreateComment extends React.Component {
this.removePreview = this.removePreview.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.getFileCount = this.getFileCount.bind(this);
this.handleResize = this.handleResize.bind(this);
PostStore.clearCommentDraftUploads();
@@ -40,13 +41,23 @@ export default class CreateComment extends React.Component {
messageText: draft.message,
uploadsInProgress: draft.uploadsInProgress,
previews: draft.previews,
submitting: false
submitting: false,
windowWidth: Utils.windowWidth()
};
}
componentDidMount() {
window.addEventListener('resize', this.handleResize);
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
}
handleResize() {
this.setState({windowWidth: Utils.windowWidth()});
}
componentDidUpdate(prevProps, prevState) {
if (prevState.uploadsInProgress < this.state.uploadsInProgress) {
$('.post-right__scroll').scrollTop($('.post-right__scroll')[0].scrollHeight);
if ($(window).width() > 768) {
if (this.state.windowWidth > 768) {
$('.post-right__scroll').perfectScrollbar('update');
}
}

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

@@ -37,6 +37,7 @@ export default class CreatePost extends React.Component {
this.onChange = this.onChange.bind(this);
this.getFileCount = this.getFileCount.bind(this);
this.handleArrowUp = this.handleArrowUp.bind(this);
this.handleResize = this.handleResize.bind(this);
PostStore.clearDraftUploads();
@@ -48,9 +49,17 @@ export default class CreatePost extends React.Component {
uploadsInProgress: draft.uploadsInProgress,
previews: draft.previews,
submitting: false,
initialText: draft.messageText
initialText: draft.messageText,
windowWidth: Utils.windowWidth(),
windowHeigth: Utils.windowHeight()
};
}
handleResize() {
this.setState({
windowWidth: Utils.windowWidth(),
windowHeight: Utils.windowHeight()
});
}
componentDidUpdate(prevProps, prevState) {
if (prevState.previews.length !== this.state.previews.length) {
this.resizePostHolder();
@@ -61,6 +70,11 @@ export default class CreatePost extends React.Component {
this.resizePostHolder();
return;
}
if (prevState.windowWidth !== this.state.windowWidth || prevState.windowHeight !== this.state.windowHeigth) {
this.resizePostHolder();
return;
}
}
getCurrentDraft() {
const draft = PostStore.getCurrentDraft();
@@ -194,10 +208,9 @@ export default class CreatePost extends React.Component {
PostStore.storeCurrentDraft(draft);
}
resizePostHolder() {
const height = $(window).height() - $(ReactDOM.findDOMNode(this.refs.topDiv)).height() - 50;
const height = this.state.windowHeigth - $(ReactDOM.findDOMNode(this.refs.topDiv)).height() - 50;
$('.post-list-holder-by-time').css('height', `${height}px`);
$(window).trigger('resize');
if ($(window).width() > 960) {
if (this.state.windowWidth > 960) {
$('#post_textbox').focus();
}
}
@@ -274,9 +287,11 @@ export default class CreatePost extends React.Component {
componentDidMount() {
ChannelStore.addChangeListener(this.onChange);
this.resizePostHolder();
window.addEventListener('resize', this.handleResize);
}
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onChange);
window.removeEventListener('resize', this.handleResize);
}
onChange() {
const channelId = ChannelStore.getCurrentId();

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

@@ -140,12 +140,11 @@ export default class MoreDirectChannels extends React.Component {
if (user.nickname) {
const separator = fullName ? ' - ' : '';
details.push(
<p
<span
key={`${user.nickname}__nickname`}
className='more-description'
>
{separator + user.nickname}
</p>
</span>
);
}
@@ -184,7 +183,9 @@ export default class MoreDirectChannels extends React.Component {
<div className='more-name'>
{user.username}
</div>
{details}
<div className='more-description'>
{details}
</div>
</td>
<td className='td--action lg'>
{joinButton}
@@ -242,7 +243,7 @@ export default class MoreDirectChannels extends React.Component {
onHide={this.handleHide}
>
<Modal.Header closeButton={true}>
<Modal.Title>{'Team Directory'}</Modal.Title>
<Modal.Title>{'Direct Messages'}</Modal.Title>
</Modal.Header>
<Modal.Body>
<div className='row filter-row'>

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

@@ -4,7 +4,7 @@
var PostStore = require('../stores/post_store.jsx');
var UserStore = require('../stores/user_store.jsx');
var PreferenceStore = require('../stores/preference_store.jsx');
var utils = require('../utils/utils.jsx');
var Utils = require('../utils/utils.jsx');
var SearchBox = require('./search_bar.jsx');
var CreateComment = require('./create_comment.jsx');
var RhsHeaderPost = require('./rhs_header_post.jsx');
@@ -20,8 +20,12 @@ export default class RhsThread extends React.Component {
this.onChange = this.onChange.bind(this);
this.onChangeAll = this.onChangeAll.bind(this);
this.forceUpdateInfo = this.forceUpdateInfo.bind(this);
this.handleResize = this.handleResize.bind(this);
this.state = this.getStateFromStores();
const state = this.getStateFromStores();
state.windowWidth = Utils.windowWidth();
state.windowHeight = Utils.windowHeight();
this.state = state;
}
getStateFromStores() {
var postList = PostStore.getSelectedPost();
@@ -47,9 +51,7 @@ export default class RhsThread extends React.Component {
PostStore.addChangeListener(this.onChangeAll);
PreferenceStore.addChangeListener(this.forceUpdateInfo);
this.resize();
$(window).resize(function resize() {
this.resize();
}.bind(this));
window.addEventListener('resize', this.handleResize);
}
componentDidUpdate() {
if ($('.post-right__scroll')[0]) {
@@ -61,6 +63,7 @@ export default class RhsThread extends React.Component {
PostStore.removeSelectedPostChangeListener(this.onChange);
PostStore.removeChangeListener(this.onChangeAll);
PreferenceStore.removeChangeListener(this.forceUpdateInfo);
window.removeEventListener('resize', this.handleResize);
}
forceUpdateInfo() {
if (this.state.postList) {
@@ -71,9 +74,15 @@ export default class RhsThread extends React.Component {
}
}
}
handleResize() {
this.setState({
windowWidth: Utils.windowWidth(),
windowHeight: Utils.windowHeight()
});
}
onChange() {
var newState = this.getStateFromStores();
if (!utils.areStatesEqual(newState, this.state)) {
if (!Utils.areStatesEqual(newState, this.state)) {
this.setState(newState);
}
}
@@ -103,15 +112,15 @@ export default class RhsThread extends React.Component {
}
var newState = this.getStateFromStores();
if (!utils.areStatesEqual(newState, this.state)) {
if (!Utils.areStatesEqual(newState, this.state)) {
this.setState(newState);
}
}
resize() {
var height = $(window).height() - $('#error_bar').outerHeight() - 100;
var height = this.state.windowHeight - $('#error_bar').outerHeight() - 100;
$('.post-right__scroll').css('height', height + 'px');
$('.post-right__scroll').scrollTop(100000);
if ($(window).width() > 768) {
if (this.state.windowWidth > 768) {
$('.post-right__scroll').perfectScrollbar();
$('.post-right__scroll').perfectScrollbar('update');
}

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

@@ -4,7 +4,7 @@
var PostStore = require('../stores/post_store.jsx');
var UserStore = require('../stores/user_store.jsx');
var SearchBox = require('./search_bar.jsx');
var utils = require('../utils/utils.jsx');
var Utils = require('../utils/utils.jsx');
var SearchResultsHeader = require('./search_results_header.jsx');
var SearchResultsItem = require('./search_results_item.jsx');
@@ -20,18 +20,19 @@ export default class SearchResults extends React.Component {
this.onChange = this.onChange.bind(this);
this.resize = this.resize.bind(this);
this.handleResize = this.handleResize.bind(this);
this.state = getStateFromStores();
const state = getStateFromStores();
state.windowWidth = Utils.windowWidth();
state.windowHeight = Utils.windowHeight();
this.state = state;
}
componentDidMount() {
this.mounted = true;
PostStore.addSearchChangeListener(this.onChange);
this.resize();
var self = this;
$(window).resize(function resize() {
self.resize();
});
window.addEventListener('resize', this.handleResize);
}
componentDidUpdate() {
@@ -41,22 +42,30 @@ export default class SearchResults extends React.Component {
componentWillUnmount() {
PostStore.removeSearchChangeListener(this.onChange);
this.mounted = false;
window.removeEventListener('resize', this.handleResize);
}
handleResize() {
this.setState({
windowWidth: Utils.windowWidth(),
windowHeight: Utils.windowHeight()
});
}
onChange() {
if (this.mounted) {
var newState = getStateFromStores();
if (!utils.areStatesEqual(newState, this.state)) {
if (!Utils.areStatesEqual(newState, this.state)) {
this.setState(newState);
}
}
}
resize() {
var height = $(window).height() - $('#error_bar').outerHeight() - 100;
var height = this.state.windowHeight - $('#error_bar').outerHeight() - 100;
$('#search-items-container').css('height', height + 'px');
$('#search-items-container').scrollTop(0);
if ($(window).width() > 768) {
if (this.state.windowWidth > 768) {
$('#search-items-container').perfectScrollbar();
}
}

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

@@ -29,9 +29,10 @@ export default class Sidebar extends React.Component {
this.onChange = this.onChange.bind(this);
this.onScroll = this.onScroll.bind(this);
this.onResize = this.onResize.bind(this);
this.updateUnreadIndicators = this.updateUnreadIndicators.bind(this);
this.handleLeaveDirectChannel = this.handleLeaveDirectChannel.bind(this);
this.updateScrollbar = this.updateScrollbar.bind(this);
this.handleResize = this.handleResize.bind(this);
this.showNewChannelModal = this.showNewChannelModal.bind(this);
this.hideNewChannelModal = this.hideNewChannelModal.bind(this);
@@ -46,6 +47,7 @@ export default class Sidebar extends React.Component {
state.newChannelModalType = '';
state.showDirectChannelsModal = false;
state.loadingDMChannel = -1;
state.windowWidth = Utils.windowWidth();
this.state = state;
}
@@ -129,14 +131,11 @@ export default class Sidebar extends React.Component {
TeamStore.addChangeListener(this.onChange);
PreferenceStore.addChangeListener(this.onChange);
if ($(window).width() > 768) {
$('.nav-pills__container').perfectScrollbar();
}
this.updateTitle();
this.updateUnreadIndicators();
this.updateScrollbar();
$(window).on('resize', this.onResize);
window.addEventListener('resize', this.handleResize);
}
shouldComponentUpdate(nextProps, nextState) {
if (!Utils.areStatesEqual(nextProps, this.props)) {
@@ -151,9 +150,10 @@ export default class Sidebar extends React.Component {
componentDidUpdate() {
this.updateTitle();
this.updateUnreadIndicators();
this.updateScrollbar();
}
componentWillUnmount() {
$(window).off('resize', this.onResize);
window.removeEventListener('resize', this.handleResize);
ChannelStore.removeChangeListener(this.onChange);
UserStore.removeChangeListener(this.onChange);
@@ -161,6 +161,18 @@ export default class Sidebar extends React.Component {
TeamStore.removeChangeListener(this.onChange);
PreferenceStore.removeChangeListener(this.onChange);
}
handleResize() {
this.setState({
windowWidth: Utils.windowWidth(),
windowHeight: Utils.windowHeight()
});
}
updateScrollbar() {
if (this.state.windowWidth > 768) {
$('.nav-pills__container').perfectScrollbar();
$('.nav-pills__container').perfectScrollbar('update');
}
}
onChange() {
var newState = this.getStateFromStores();
if (!Utils.areStatesEqual(newState, this.state)) {
@@ -186,9 +198,6 @@ export default class Sidebar extends React.Component {
onScroll() {
this.updateUnreadIndicators();
}
onResize() {
this.updateUnreadIndicators();
}
updateUnreadIndicators() {
const container = $(ReactDOM.findDOMNode(this.refs.container));

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

@@ -3,6 +3,7 @@
const emoticonPatterns = {
smile: /(^|\s)(:-?\))($|\s)/g, // :)
wink: /(^|\s)(;-?\))($|\s)/g, // ;)
open_mouth: /(^|\s)(:o)($|\s)/gi, // :o
scream: /(^|\s)(:-o)($|\s)/gi, // :-o
smirk: /(^|\s)(:-?])($|\s)/g, // :]

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

@@ -519,11 +519,11 @@ export function applyTheme(theme) {
changeCss('.modal .custom-textarea:focus', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1);
changeCss('.channel-intro, .settings-modal .settings-table .settings-content .divider-dark, hr, .settings-modal .settings-table .settings-links', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment, pre', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment, .post.post--comment.other--root .post-comment, .post.same--root .post-body, .modal .more-channel-table tbody>tr td, .member-div:first-child, .member-div, .access-history__table .access__report, .activity-log__table', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.1), 2);
changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment, .post.post--comment.other--root .post-comment, .post.same--root .post-body, .modal .more-table tbody>tr td, .member-div:first-child, .member-div, .access-history__table .access__report, .activity-log__table', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.1), 2);
changeCss('@media(max-width: 1440px){.post.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
changeCss('@media(max-width: 1440px){.post.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
changeCss('@media(max-width: 1800px){.inner__wrap.move--left .post.post--comment.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
changeCss('.post:hover, .modal .more-channel-table tbody>tr:hover td, .sidebar--right .sidebar--right__header, .settings-modal .settings-table .settings-content .section-min:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
changeCss('.post:hover, .modal .more-table tbody>tr:hover td, .sidebar--right .sidebar--right__header, .settings-modal .settings-table .settings-content .section-min:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
changeCss('.date-separator.hovered--before:after, .date-separator.hovered--after:before, .new-separator.hovered--after:before, .new-separator.hovered--before:after', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
changeCss('.command-name:hover, .mentions-name:hover, .mentions-focus, .dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1);
changeCss('code', 'background:' + changeOpacity(theme.centerChannelColor, 0.1), 1);
@@ -976,3 +976,11 @@ export function getShortenedTeamURL() {
}
return teamURL + '/';
}
export function windowWidth() {
return $(window).width();
}
export function windowHeight() {
return $(window).height();
}

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

@@ -194,7 +194,7 @@
position: relative;
max-width: 90%;
min-height: 100px;
min-width: 240px;
min-width: 320px;
@include border-radius(3px);
display: table;
margin: 0 auto;
@@ -338,7 +338,7 @@
.modal-direct-channels {
.user-list {
margin-top: 20px;
margin-top: 10px;
overflow: auto;
-webkit-overflow-scrolling: touch;
max-height: 500px;

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

@@ -267,6 +267,7 @@
}
}
.file-details {
width: 100%;
height: auto;
}
}
@@ -697,7 +698,7 @@
.modal-image {
.image-wrapper {
font-size: 12px;
min-width: 280px;
min-width: 250px;
.modal-close {
@include opacity(1);
}

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

@@ -454,9 +454,9 @@ func verifyEmail(c *api.Context, w http.ResponseWriter, r *http.Request) {
user := result.Data.(*model.User)
if user.LastActivityAt > 0 {
api.FireAndForgetEmailChangeVerifyEmail(user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team))
api.SendEmailChangeVerifyEmailAndForget(user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team))
} else {
api.FireAndForgetVerifyEmail(user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team))
api.SendVerifyEmailAndForget(user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team))
}
newAddress := strings.Replace(r.URL.String(), "&resend=true", "&resend_success=true", -1)