diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 38f2bb09ad..c2b32df06c 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -80,7 +80,7 @@ }, { "ImportPath": "github.com/nfnt/resize", - "Rev": "f2d1b73023c55bdfb4fa2ed385d2bec95bce3692" + "Rev": "dc93e1b98c579d90ee2fa15c1fd6dac34f6e7899" }, { "ImportPath": "github.com/stretchr/objx", diff --git a/Godeps/_workspace/src/github.com/nfnt/resize/converter.go b/Godeps/_workspace/src/github.com/nfnt/resize/converter.go index 84bd284ba3..b3dd06b8d5 100644 --- a/Godeps/_workspace/src/github.com/nfnt/resize/converter.go +++ b/Godeps/_workspace/src/github.com/nfnt/resize/converter.go @@ -131,11 +131,11 @@ func resizeRGBA(in *image.RGBA, out *image.NRGBA, scale float64, coeffs []int16, // reverse alpha-premultiplication. if a != 0 { - r *= 0xffff + r *= 0xff r /= a - g *= 0xffff + g *= 0xff g /= a - b *= 0xffff + b *= 0xff b /= a } diff --git a/Godeps/_workspace/src/github.com/nfnt/resize/resize.go b/Godeps/_workspace/src/github.com/nfnt/resize/resize.go index 4d4ff6e3e4..c1672432ee 100644 --- a/Godeps/_workspace/src/github.com/nfnt/resize/resize.go +++ b/Godeps/_workspace/src/github.com/nfnt/resize/resize.go @@ -167,7 +167,7 @@ func Resize(width, height uint, img image.Image, interp InterpolationFunction) i // accessing the YCbCr arrays in a tight loop is slow. // converting the image to ycc increases performance by 2x. temp := newYCC(image.Rect(0, 0, input.Bounds().Dy(), int(width)), input.SubsampleRatio) - result := newYCC(image.Rect(0, 0, int(width), int(height)), input.SubsampleRatio) + result := newYCC(image.Rect(0, 0, int(width), int(height)), image.YCbCrSubsampleRatio444) coeffs, offset, filterLength := createWeights8(temp.Bounds().Dy(), taps, blur, scaleX, kernel) in := imageYCbCrToYCC(input) @@ -409,7 +409,7 @@ func resizeNearest(width, height uint, scaleX, scaleY float64, img image.Image, // accessing the YCbCr arrays in a tight loop is slow. // converting the image to ycc increases performance by 2x. temp := newYCC(image.Rect(0, 0, input.Bounds().Dy(), int(width)), input.SubsampleRatio) - result := newYCC(image.Rect(0, 0, int(width), int(height)), input.SubsampleRatio) + result := newYCC(image.Rect(0, 0, int(width), int(height)), image.YCbCrSubsampleRatio444) coeffs, offset, filterLength := createWeightsNearest(temp.Bounds().Dy(), taps, blur, scaleX) in := imageYCbCrToYCC(input) diff --git a/Godeps/_workspace/src/github.com/nfnt/resize/resize_test.go b/Godeps/_workspace/src/github.com/nfnt/resize/resize_test.go index ee31ac494f..6f69113163 100644 --- a/Godeps/_workspace/src/github.com/nfnt/resize/resize_test.go +++ b/Godeps/_workspace/src/github.com/nfnt/resize/resize_test.go @@ -46,7 +46,7 @@ func Test_CorrectResize(t *testing.T) { } } -func Test_SameColor(t *testing.T) { +func Test_SameColorWithRGBA(t *testing.T) { img := image.NewRGBA(image.Rect(0, 0, 20, 20)) for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ { for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ { @@ -56,9 +56,99 @@ func Test_SameColor(t *testing.T) { out := Resize(10, 10, img, Lanczos3) for y := out.Bounds().Min.Y; y < out.Bounds().Max.Y; y++ { for x := out.Bounds().Min.X; x < out.Bounds().Max.X; x++ { - color := img.At(x, y).(color.RGBA) + color := out.At(x, y).(color.NRGBA) if color.R != 0x80 || color.G != 0x80 || color.B != 0x80 || color.A != 0xFF { - t.Fail() + t.Errorf("%+v", color) + } + } + } +} + +func Test_SameColorWithNRGBA(t *testing.T) { + img := image.NewNRGBA(image.Rect(0, 0, 20, 20)) + for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ { + for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ { + img.SetNRGBA(x, y, color.NRGBA{0x80, 0x80, 0x80, 0xFF}) + } + } + out := Resize(10, 10, img, Lanczos3) + for y := out.Bounds().Min.Y; y < out.Bounds().Max.Y; y++ { + for x := out.Bounds().Min.X; x < out.Bounds().Max.X; x++ { + color := out.At(x, y).(color.NRGBA) + if color.R != 0x80 || color.G != 0x80 || color.B != 0x80 || color.A != 0xFF { + t.Errorf("%+v", color) + } + } + } +} + +func Test_SameColorWithRGBA64(t *testing.T) { + img := image.NewRGBA64(image.Rect(0, 0, 20, 20)) + for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ { + for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ { + img.SetRGBA64(x, y, color.RGBA64{0x8000, 0x8000, 0x8000, 0xFFFF}) + } + } + out := Resize(10, 10, img, Lanczos3) + for y := out.Bounds().Min.Y; y < out.Bounds().Max.Y; y++ { + for x := out.Bounds().Min.X; x < out.Bounds().Max.X; x++ { + color := out.At(x, y).(color.NRGBA64) + if color.R != 0x8000 || color.G != 0x8000 || color.B != 0x8000 || color.A != 0xFFFF { + t.Errorf("%+v", color) + } + } + } +} + +func Test_SameColorWithNRGBA64(t *testing.T) { + img := image.NewNRGBA64(image.Rect(0, 0, 20, 20)) + for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ { + for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ { + img.SetNRGBA64(x, y, color.NRGBA64{0x8000, 0x8000, 0x8000, 0xFFFF}) + } + } + out := Resize(10, 10, img, Lanczos3) + for y := out.Bounds().Min.Y; y < out.Bounds().Max.Y; y++ { + for x := out.Bounds().Min.X; x < out.Bounds().Max.X; x++ { + color := out.At(x, y).(color.NRGBA64) + if color.R != 0x8000 || color.G != 0x8000 || color.B != 0x8000 || color.A != 0xFFFF { + t.Errorf("%+v", color) + } + } + } +} + +func Test_SameColorWithGray(t *testing.T) { + img := image.NewGray(image.Rect(0, 0, 20, 20)) + for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ { + for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ { + img.SetGray(x, y, color.Gray{0x80}) + } + } + out := Resize(10, 10, img, Lanczos3) + for y := out.Bounds().Min.Y; y < out.Bounds().Max.Y; y++ { + for x := out.Bounds().Min.X; x < out.Bounds().Max.X; x++ { + color := out.At(x, y).(color.Gray) + if color.Y != 0x80 { + t.Errorf("%+v", color) + } + } + } +} + +func Test_SameColorWithGray16(t *testing.T) { + img := image.NewGray16(image.Rect(0, 0, 20, 20)) + for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ { + for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ { + img.SetGray16(x, y, color.Gray16{0x8000}) + } + } + out := Resize(10, 10, img, Lanczos3) + for y := out.Bounds().Min.Y; y < out.Bounds().Max.Y; y++ { + for x := out.Bounds().Min.X; x < out.Bounds().Max.X; x++ { + color := out.At(x, y).(color.Gray16) + if color.Y != 0x8000 { + t.Errorf("%+v", color) } } } diff --git a/Makefile b/Makefile index 8793ba98ad..222d4ffe4f 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,8 @@ GOPATH ?= $(GOPATH:) GOFLAGS ?= $(GOFLAGS:) BUILD_NUMBER ?= $(BUILD_NUMBER:) +GO=$(GOPATH)/bin/godep go + ifeq ($(BUILD_NUMBER),) BUILD_NUMBER := dev endif @@ -21,29 +23,27 @@ travis: @echo building for travis rm -Rf $(DIST_ROOT) - @go clean $(GOFLAGS) -i ./... - + @$(GO) clean $(GOFLAGS) -i ./... + @cd web/react/ && npm install - @go build $(GOFLAGS) ./... + @$(GO) build $(GOFLAGS) ./... @mkdir -p logs - @go test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=180s ./api || exit 1 - @go test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=12s ./model || exit 1 - @go test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s ./store || exit 1 - @go test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s ./utils || exit 1 - @go test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s ./web || exit 1 + @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=180s ./api || exit 1 + @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=12s ./model || exit 1 + @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s ./store || exit 1 + @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s ./utils || exit 1 + @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s ./web || exit 1 build: - @go build $(GOFLAGS) ./... + @$(GO) build $(GOFLAGS) ./... install: @go get $(GOFLAGS) github.com/tools/godep @if [ $(shell docker ps -a | grep -ci mattermost-mysql) -eq 0 ]; then \ - echo restoring go libs using godep; \ - $(GOPATH)/bin/godep restore; \ echo starting mattermost-mysql; \ docker run --name mattermost-mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=mostest \ -e MYSQL_USER=mmuser -e MYSQL_PASSWORD=mostest -e MYSQL_DATABASE=mattermost_test -d mysql > /dev/null; \ @@ -64,37 +64,37 @@ install: test: install @mkdir -p logs - @go test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=180s ./api || exit 1 - @go test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=12s ./model || exit 1 - @go test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s ./store || exit 1 - @go test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s ./utils || exit 1 - @go test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s ./web || exit 1 + @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=180s ./api || exit 1 + @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=12s ./model || exit 1 + @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s ./store || exit 1 + @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s ./utils || exit 1 + @$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s ./web || exit 1 benchmark: install @mkdir -p logs - @go test $(GOFLAGS) -test.v -run=NO_TESTS -bench=$(BENCH) ./api || exit 1 + @$(GO) test $(GOFLAGS) -test.v -run=NO_TESTS -bench=$(BENCH) ./api || exit 1 cover: install rm -Rf $(DIST_RESULTS) mkdir -p $(DIST_RESULTS) - @go test $(GOFLAGS) -coverprofile=$(DIST_RESULTS)/api.cover.out github.com/mattermost/platform/api - @go test $(GOFLAGS) -coverprofile=$(DIST_RESULTS)/model.cover.out github.com/mattermost/platform/model - @go test $(GOFLAGS) -coverprofile=$(DIST_RESULTS)/store.cover.out github.com/mattermost/platform/store - @go test $(GOFLAGS) -coverprofile=$(DIST_RESULTS)/utils.cover.out github.com/mattermost/platform/utils - @go test $(GOFLAGS) -coverprofile=$(DIST_RESULTS)/web.cover.out github.com/mattermost/platform/web + @$(GO) test $(GOFLAGS) -coverprofile=$(DIST_RESULTS)/api.cover.out github.com/mattermost/platform/api + @$(GO) test $(GOFLAGS) -coverprofile=$(DIST_RESULTS)/model.cover.out github.com/mattermost/platform/model + @$(GO) test $(GOFLAGS) -coverprofile=$(DIST_RESULTS)/store.cover.out github.com/mattermost/platform/store + @$(GO) test $(GOFLAGS) -coverprofile=$(DIST_RESULTS)/utils.cover.out github.com/mattermost/platform/utils + @$(GO) test $(GOFLAGS) -coverprofile=$(DIST_RESULTS)/web.cover.out github.com/mattermost/platform/web cd $(DIST_RESULTS) && \ echo "mode: set" > coverage.out && cat *.cover.out | grep -v mode: | sort -r | \ awk '{if($$1 != last) {print $$0;last=$$1}}' >> coverage.out - cd $(DIST_RESULTS) && go tool cover -html=coverage.out -o=coverage.html + cd $(DIST_RESULTS) && $(GO) tool cover -html=coverage.out -o=coverage.html rm -f $(DIST_RESULTS)/*.cover.out clean: rm -Rf $(DIST_ROOT) - @go clean $(GOFLAGS) -i ./... + @$(GO) clean $(GOFLAGS) -i ./... @if [ $(shell docker ps -a | grep -ci mattermost-mysql) -eq 1 ]; then \ echo stopping mattermost-mysql; \ @@ -124,7 +124,7 @@ run: install @cd web/react && npm start & @echo starting go web server - @go run $(GOFLAGS) mattermost.go -config=config.json & + @$(GO) run $(GOFLAGS) mattermost.go -config=config.json & @echo starting compass watch @cd web/sass-files && compass watch & @@ -161,8 +161,8 @@ cleandb: dist: install - @go build $(GOFLAGS) -i -a ./... - @go install $(GOFLAGS) -a ./... + @$(GO) build $(GOFLAGS) -i -a ./... + @$(GO) install $(GOFLAGS) -a ./... mkdir -p $(DIST_PATH)/bin cp $(GOPATH)/bin/platform $(DIST_PATH)/bin diff --git a/README.md b/README.md index 65a3e7c663..f0857e99de 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -**Mattermost Preview** +**Mattermost Alpha** **Team Communication Service** -**Version 0.5.0** +**Development Build** About Mattermost @@ -22,7 +22,7 @@ Learn More Installing the Mattermost ========================= -You're installing "Mattermost Preview", a pre-released 0.5.0 version intended for an early look at what we're building. While SpinPunch runs this version internally, it's not recommended for production deployments since we can't guarantee API stability or backwards compatibility until our 1.0.0 version release. +You're installing "Mattermost Alpha", a pre-released version intended for an early look at what we're building. While SpinPunch runs this version internally, it's not recommended for production deployments since we can't guarantee API stability or backwards compatibility until our production release. That said, any issues at all, please let us know on the Mattermost forum at: http://forum.mattermost.org diff --git a/api/channel.go b/api/channel.go index 4d8dbad099..123fd8a355 100644 --- a/api/channel.go +++ b/api/channel.go @@ -710,6 +710,11 @@ func removeChannelMember(c *Context, w http.ResponseWriter, r *http.Request) { return } + message := model.NewMessage(c.Session.TeamId, "", userId, model.ACTION_USER_REMOVED) + message.Add("channel_id",id) + message.Add("remover", c.Session.UserId) + PublishAndForget(message) + c.LogAudit("name=" + channel.Name + " user_id=" + userId) result := make(map[string]string) diff --git a/api/context.go b/api/context.go index ac9dffcbc9..16da0a6eb4 100644 --- a/api/context.go +++ b/api/context.go @@ -101,6 +101,12 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set(model.HEADER_REQUEST_ID, c.RequestId) w.Header().Set(model.HEADER_VERSION_ID, utils.Cfg.ServiceSettings.Version) + // Instruct the browser not to display us in an iframe for anti-clickjacking + if !h.isApi { + w.Header().Set("X-Frame-Options", "DENY") + w.Header().Set("Content-Security-Policy", "frame-ancestors none") + } + sessionId := "" // attempt to parse the session token from the header diff --git a/api/file.go b/api/file.go index 82cee9d1e7..3ef50fbbd5 100644 --- a/api/file.go +++ b/api/file.go @@ -33,7 +33,7 @@ func InitFile(r *mux.Router) { sr := r.PathPrefix("/files").Subrouter() sr.Handle("/upload", ApiUserRequired(uploadFile)).Methods("POST") - sr.Handle("/get/{channel_id:[A-Za-z0-9]+}/{user_id:[A-Za-z0-9]+}/{filename:([A-Za-z0-9]+/)?.+\\.[A-Za-z0-9]{3,}}", ApiAppHandler(getFile)).Methods("GET") + sr.Handle("/get/{channel_id:[A-Za-z0-9]+}/{user_id:[A-Za-z0-9]+}/{filename:([A-Za-z0-9]+/)?.+(\\.[A-Za-z0-9]{3,})?}", ApiAppHandler(getFile)).Methods("GET") sr.Handle("/get_public_link", ApiUserRequired(getPublicLink)).Methods("POST") } @@ -142,7 +142,7 @@ func fireAndForgetHandleImages(filenames []string, fileData [][]byte, teamId, ch go func() { var thumbnail image.Image if imgConfig.Width > int(utils.Cfg.ImageSettings.ThumbnailWidth) { - thumbnail = resize.Resize(utils.Cfg.ImageSettings.ThumbnailWidth, utils.Cfg.ImageSettings.ThumbnailHeight, img, resize.NearestNeighbor) + thumbnail = resize.Resize(utils.Cfg.ImageSettings.ThumbnailWidth, utils.Cfg.ImageSettings.ThumbnailHeight, img, resize.Lanczos3) } else { thumbnail = img } @@ -164,7 +164,7 @@ func fireAndForgetHandleImages(filenames []string, fileData [][]byte, teamId, ch go func() { var preview image.Image if imgConfig.Width > int(utils.Cfg.ImageSettings.PreviewWidth) { - preview = resize.Resize(utils.Cfg.ImageSettings.PreviewWidth, utils.Cfg.ImageSettings.PreviewHeight, img, resize.NearestNeighbor) + preview = resize.Resize(utils.Cfg.ImageSettings.PreviewWidth, utils.Cfg.ImageSettings.PreviewHeight, img, resize.Lanczos3) } else { preview = img } diff --git a/api/templates/email_change_body.html b/api/templates/email_change_body.html index f8f3845e78..439fffd5b0 100644 --- a/api/templates/email_change_body.html +++ b/api/templates/email_change_body.html @@ -8,7 +8,7 @@ - @@ -32,7 +32,7 @@ -
+
+

diff --git a/api/templates/find_teams_body.html b/api/templates/find_teams_body.html index 6eaaf56e07..a73ed0ad4a 100644 --- a/api/templates/find_teams_body.html +++ b/api/templates/find_teams_body.html @@ -8,7 +8,7 @@
- @@ -40,7 +40,7 @@ -
+
+

diff --git a/api/templates/invite_body.html b/api/templates/invite_body.html index 46189fae57..ad0658e3d1 100644 --- a/api/templates/invite_body.html +++ b/api/templates/invite_body.html @@ -8,7 +8,7 @@
- @@ -35,7 +35,7 @@ -
+
+

diff --git a/api/templates/password_change_body.html b/api/templates/password_change_body.html index 515c0a7d9d..1d4a6e1c86 100644 --- a/api/templates/password_change_body.html +++ b/api/templates/password_change_body.html @@ -8,7 +8,7 @@
- @@ -32,7 +32,7 @@ -
+
+

diff --git a/api/templates/post_body.html b/api/templates/post_body.html index c0f4375d8e..0aa913db56 100644 --- a/api/templates/post_body.html +++ b/api/templates/post_body.html @@ -8,7 +8,7 @@
- @@ -35,7 +35,7 @@ -
+
+

diff --git a/api/templates/reset_body.html b/api/templates/reset_body.html index af9f6b4e8b..4c2fec1e74 100644 --- a/api/templates/reset_body.html +++ b/api/templates/reset_body.html @@ -8,7 +8,7 @@
- @@ -35,7 +35,7 @@ -
+
+

diff --git a/api/templates/signup_team_body.html b/api/templates/signup_team_body.html index 5a5ae4d477..5e60a042bc 100644 --- a/api/templates/signup_team_body.html +++ b/api/templates/signup_team_body.html @@ -8,7 +8,7 @@
- @@ -38,7 +38,7 @@ -
+
+

diff --git a/api/templates/verify_body.html b/api/templates/verify_body.html index 67ded9c20e..1a68c16f50 100644 --- a/api/templates/verify_body.html +++ b/api/templates/verify_body.html @@ -8,7 +8,7 @@
- @@ -35,7 +35,7 @@ -
+
+

diff --git a/api/templates/welcome_body.html b/api/templates/welcome_body.html index 7107bc2e06..cc4d95fb11 100644 --- a/api/templates/welcome_body.html +++ b/api/templates/welcome_body.html @@ -8,7 +8,7 @@
- @@ -32,7 +32,7 @@ -
+
+

diff --git a/model/message.go b/model/message.go index 52ee69e8f9..ec4817b2a2 100644 --- a/model/message.go +++ b/model/message.go @@ -16,6 +16,7 @@ const ( ACTION_VIEWED = "viewed" ACTION_NEW_USER = "new_user" ACTION_USER_ADDED = "user_added" + ACTION_USER_REMOVED = "user_removed" ) type Message struct { diff --git a/model/session.go b/model/session.go index 9fd3b9ec37..c812f83e23 100644 --- a/model/session.go +++ b/model/session.go @@ -10,9 +10,9 @@ import ( const ( SESSION_TOKEN = "MMSID" - SESSION_TIME_WEB_IN_DAYS = 365 + SESSION_TIME_WEB_IN_DAYS = 30 SESSION_TIME_WEB_IN_SECS = 60 * 60 * 24 * SESSION_TIME_WEB_IN_DAYS - SESSION_TIME_MOBILE_IN_DAYS = 365 + SESSION_TIME_MOBILE_IN_DAYS = 30 SESSION_TIME_MOBILE_IN_SECS = 60 * 60 * 24 * SESSION_TIME_MOBILE_IN_DAYS SESSION_CACHE_IN_SECS = 60 * 10 SESSION_CACHE_SIZE = 10000 diff --git a/model/utils.go b/model/utils.go index 093a54e38c..c7f991da20 100644 --- a/model/utils.go +++ b/model/utils.go @@ -319,6 +319,6 @@ func ClearMentionTags(post string) string { } var UrlRegex = regexp.MustCompile(`^((?:[a-z]+:\/\/)?(?:(?:[a-z0-9\-]+\.)+(?:[a-z]{2}|aero|arpa|biz|com|coop|edu|gov|info|int|jobs|mil|museum|name|nato|net|org|pro|travel|local|internal))(:[0-9]{1,5})?(?:\/[a-z0-9_\-\.~]+)*(\/([a-z0-9_\-\.]*)(?:\?[a-z0-9+_~\-\.%=&]*)?)?(?:#[a-zA-Z0-9!$&'()*+.=-_~:@/?]*)?)(?:\s+|$)$`) -var PartialUrlRegex = regexp.MustCompile(`/([A-Za-z0-9]{26})/([A-Za-z0-9]{26})/((?:[A-Za-z0-9]{26})?.+\.[A-Za-z0-9]{3,})`) +var PartialUrlRegex = regexp.MustCompile(`/([A-Za-z0-9]{26})/([A-Za-z0-9]{26})/((?:[A-Za-z0-9]{26})?.+(?:\.[A-Za-z0-9]{3,})?)`) var SplitRunes = map[rune]bool{',': true, ' ': true, '.': true, '!': true, '?': true, ':': true, ';': true, '\n': true, '<': true, '>': true, '(': true, ')': true, '{': true, '}': true, '[': true, ']': true, '+': true, '/': true, '\\': true} diff --git a/store/sql_channel_store.go b/store/sql_channel_store.go index 8961d5d97a..f64558a922 100644 --- a/store/sql_channel_store.go +++ b/store/sql_channel_store.go @@ -120,7 +120,7 @@ func (s SqlChannelStore) Update(channel *model.Channel) StoreChannel { if count, err := s.GetMaster().Update(channel); err != nil { if IsUniqueConstraintError(err.Error(), "Name", "channels_name_teamid_key") { dupChannel := model.Channel{} - s.GetReplica().SelectOne(&dupChannel, "SELECT * FROM Channels WHERE TeamId=? AND Name=? AND DeleteAt > 0", channel.TeamId, channel.Name) + s.GetReplica().SelectOne(&dupChannel, "SELECT * FROM Channels WHERE TeamId = :TeamId AND Name= :Name AND DeleteAt > 0", map[string]interface{}{"TeamId": channel.TeamId, "Name": channel.Name}) if dupChannel.DeleteAt > 0 { result.Err = model.NewAppError("SqlChannelStore.Update", "A channel with that name was previously created", "id="+channel.Id+", "+err.Error()) } else { diff --git a/store/sql_post_store.go b/store/sql_post_store.go index 56c174e4c2..ede69d1255 100644 --- a/store/sql_post_store.go +++ b/store/sql_post_store.go @@ -38,7 +38,7 @@ func NewSqlPostStore(sqlStore *SqlStore) PostStore { func (s SqlPostStore) UpgradeSchemaIfNeeded() { // These execs are for upgrading currently created databases to full utf8mb4 compliance - // Will be removed as seen fit for upgrading + // Will be removed as seen fit for upgrading s.GetMaster().Exec("ALTER TABLE Posts charset=utf8mb4") s.GetMaster().Exec("ALTER TABLE Posts MODIFY COLUMN Message varchar(4000) CHARACTER SET utf8mb4") } @@ -80,14 +80,14 @@ func (s SqlPostStore) Save(post *model.Post) StoreChannel { time := model.GetMillis() if post.Type != model.POST_JOIN_LEAVE { - s.GetMaster().Exec("UPDATE Channels SET LastPostAt = ?, TotalMsgCount = TotalMsgCount + 1 WHERE Id = ?", time, post.ChannelId) + s.GetMaster().Exec("UPDATE Channels SET LastPostAt = :LastPostAt, TotalMsgCount = TotalMsgCount + 1 WHERE Id = :ChannelId", map[string]interface{}{"LastPostAt": time, "ChannelId": post.ChannelId}) } else { // don't update TotalMsgCount for unimportant messages so that the channel isn't marked as unread - s.GetMaster().Exec("UPDATE Channels SET LastPostAt = ? WHERE Id = ?", time, post.ChannelId) + s.GetMaster().Exec("UPDATE Channels SET LastPostAt = :LastPostAt WHERE Id = :ChannelId", map[string]interface{}{"LastPostAt": time, "ChannelId": post.ChannelId}) } if len(post.RootId) > 0 { - s.GetMaster().Exec("UPDATE Posts SET UpdateAt = ? WHERE Id = ?", time, post.RootId) + s.GetMaster().Exec("UPDATE Posts SET UpdateAt = :UpdateAt WHERE Id = :RootId", map[string]interface{}{"UpdateAt": time, "RootId": post.RootId}) } result.Data = post @@ -126,10 +126,10 @@ func (s SqlPostStore) Update(oldPost *model.Post, newMessage string, newHashtags result.Err = model.NewAppError("SqlPostStore.Update", "We couldn't update the Post", "id="+editPost.Id+", "+err.Error()) } else { time := model.GetMillis() - s.GetMaster().Exec("UPDATE Channels SET LastPostAt = ? WHERE Id = ?", time, editPost.ChannelId) + s.GetMaster().Exec("UPDATE Channels SET LastPostAt = :LastPostAt WHERE Id = :ChannelId", map[string]interface{}{"LastPostAt": time, "ChannelId": editPost.ChannelId}) if len(editPost.RootId) > 0 { - s.GetMaster().Exec("UPDATE Posts SET UpdateAt = ? WHERE Id = ?", time, editPost.RootId) + s.GetMaster().Exec("UPDATE Posts SET UpdateAt = :UpdateAt WHERE Id = :RootId", map[string]interface{}{"UpdateAt": time, "RootId": editPost.RootId}) } // mark the old post as deleted diff --git a/store/sql_store.go b/store/sql_store.go index 606b2cbc16..0d4f76a722 100644 --- a/store/sql_store.go +++ b/store/sql_store.go @@ -224,7 +224,7 @@ func (ss SqlStore) CreateFullTextIndexIfNotExists(indexName string, tableName st func (ss SqlStore) createIndexIfNotExists(indexName string, tableName string, columnName string, fullText bool) { if utils.Cfg.SqlSettings.DriverName == "postgres" { - _, err := ss.GetMaster().SelectStr("SELECT to_regclass($1)", indexName) + _, err := ss.GetMaster().SelectStr("SELECT $1::regclass", indexName) // It should fail if the index does not exist if err == nil { return diff --git a/store/sql_user_store.go b/store/sql_user_store.go index 150b3147a3..2afbdcd417 100644 --- a/store/sql_user_store.go +++ b/store/sql_user_store.go @@ -187,7 +187,7 @@ func (us SqlUserStore) UpdateLastPictureUpdate(userId string) StoreChannel { curTime := model.GetMillis() - if _, err := us.GetMaster().Exec("UPDATE Users SET LastPictureUpdate = ?, UpdateAt = ? WHERE Id = ?", curTime, curTime, userId); err != nil { + if _, err := us.GetMaster().Exec("UPDATE Users SET LastPictureUpdate = :Time, UpdateAt = :Time WHERE Id = :UserId", map[string]interface{}{"Time": curTime, "UserId": userId}); err != nil { result.Err = model.NewAppError("SqlUserStore.UpdateUpdateAt", "We couldn't update the update_at", "user_id="+userId) } else { result.Data = userId diff --git a/web/react/components/activity_log_modal.jsx b/web/react/components/activity_log_modal.jsx index 7cce807a93..90f139e8b1 100644 --- a/web/react/components/activity_log_modal.jsx +++ b/web/react/components/activity_log_modal.jsx @@ -102,8 +102,9 @@ module.exports = React.createClass({
-

Active Devices

+

Active Sessions

+

Sessions are created when you log in with your email and password to a new browser on a device. Sessions let you use Mattermost for up to 30 days without having to log in again. If you want to log out sooner, use the "Logout" button below to end a session.

{ activityList } diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index 87895588ee..3275202101 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -32,7 +32,7 @@ module.exports = React.createClass({ post.message = this.state.messageText; // if this is a reply, trim off any carets from the beginning of a message - if (this.state.rootId && post.message.startsWith("^")) { + if (this.state.rootId && post.message[0] === "^") { post.message = post.message.replace(/^\^+\s*/g, ""); } diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx index 7871f52b7d..9f598ecb36 100644 --- a/web/react/components/post_body.jsx +++ b/web/react/components/post_body.jsx @@ -126,7 +126,7 @@ module.exports = React.createClass({ } else if (i < Constants.MAX_DISPLAY_FILES) { postFiles.push(
- + diff --git a/web/react/components/removed_from_channel_modal.jsx b/web/react/components/removed_from_channel_modal.jsx new file mode 100644 index 0000000000..a8889a92a4 --- /dev/null +++ b/web/react/components/removed_from_channel_modal.jsx @@ -0,0 +1,64 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var ChannelStore = require('../stores/channel_store.jsx'); +var UserStore = require('../stores/user_store.jsx'); +var BrowserStore = require('../stores/browser_store.jsx') +var utils = require('../utils/utils.jsx'); + +module.exports = React.createClass({ + handleShow: function() { + var newState = {}; + if(BrowserStore.getItem("channel-removed-state")) { + newState = BrowserStore.getItem("channel-removed-state"); + BrowserStore.removeItem("channel-removed-state"); + } + + this.setState(newState); + }, + handleClose: function() { + var townSquare = ChannelStore.getByName("town-square"); + utils.switchChannel(townSquare); + + this.setState({channelName: "", remover: ""}) + }, + componentDidMount: function() { + $(this.getDOMNode()).on('show.bs.modal',this.handleShow); + $(this.getDOMNode()).on('hidden.bs.modal',this.handleClose); + }, + componentWillUnmount: function() { + $(this.getDOMNode()).off('show.bs.modal',this.handleShow); + $(this.getDOMNode()).off('hidden.bs.modal',this.handleClose); + }, + getInitialState: function() { + return {channelName: "", remover: ""} + }, + render: function() { + var currentUser = UserStore.getCurrentUser(); + var channelName = this.state.channelName ? this.state.channelName : "the channel" + var remover = this.state.remover ? this.state.remover : "Someone" + + if (currentUser != null) { + return ( + + ); + } else { + return
; + } + } +}); \ No newline at end of file diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index 3cf67e410e..5b8d6c5429 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -7,6 +7,7 @@ var AsyncClient = require('../utils/async_client.jsx'); var SocketStore = require('../stores/socket_store.jsx'); var UserStore = require('../stores/user_store.jsx'); var TeamStore = require('../stores/team_store.jsx'); +var BrowserStore = require('../stores/browser_store.jsx') var utils = require('../utils/utils.jsx'); var SidebarHeader = require('./sidebar_header.jsx'); var SearchBox = require('./search_bar.jsx'); @@ -197,6 +198,19 @@ module.exports = React.createClass({ if (UserStore.getCurrentId() === msg.user_id) { AsyncClient.getChannels(true); } + } else if(msg.action === "user_removed") { + if(msg.user_id === UserStore.getCurrentId()) { + AsyncClient.getChannels(true); + + if(msg.props.channel_id === ChannelStore.getCurrentId() && $('#removed_from_channel').length > 0) { + var sentState = {}; + sentState.channelName = ChannelStore.getCurrent().display_name; + sentState.remover = UserStore.getProfile(msg.props.remover).username; + + BrowserStore.setItem('channel-removed-state',sentState); + $('#removed_from_channel').modal('show'); + } + } } }, updateTitle: function() { diff --git a/web/react/components/textbox.jsx b/web/react/components/textbox.jsx index ad50b7920e..bbd1f84b60 100644 --- a/web/react/components/textbox.jsx +++ b/web/react/components/textbox.jsx @@ -36,7 +36,6 @@ module.exports = React.createClass({ this.resize(); this.processMentions(); - this.updateTextdiv(); }, componentWillUnmount: function() { PostStore.removeAddMentionListener(this._onChange); @@ -87,7 +86,6 @@ module.exports = React.createClass({ this.processMentions(); this.doProcessMentions = false; } - this.updateTextdiv(); this.resize(); }, componentWillReceiveProps: function(nextProps) { @@ -117,17 +115,6 @@ module.exports = React.createClass({ }); }, 1); }, - updateTextdiv: function() { - var html = utils.insertHtmlEntities(this.refs.message.getDOMNode().value); - for (var k in this.mentions) { - var m = this.mentions[k]; - var re = new RegExp('( |^)@' + m + '( |$|\n)', 'm'); - html = html.replace(re, '$1@'+m+'$2'); - } - var re2 = new RegExp('(^$)(?![.\n])', 'gm'); - html = html.replace(re2, '

'); - $(this.refs.textdiv.getDOMNode()).html(html); - }, handleChange: function() { this.props.onUserInput(this.refs.message.getDOMNode().value); this.resize(); @@ -181,7 +168,7 @@ module.exports = React.createClass({ } }, processMentions: function() { - /* First, find all the possible mentions, highlight them in the HTML and add + /* First, find all the possible mentions and add them all to a list of mentions */ var text = utils.insertHtmlEntities(this.refs.message.getDOMNode().value); @@ -192,9 +179,7 @@ module.exports = React.createClass({ var matches = text.match(re1); if (!matches) { - $(this.refs.textdiv.getDOMNode()).text(text); this.updateMentionTab(null, []); - this.mentions = []; return; } @@ -207,7 +192,7 @@ module.exports = React.createClass({ } /* Figure out what the user is currently typing. If it's a mention then we don't - want to highlight it and add it to the mention list yet, so we remove it if + want to add it to the mention list yet, so we remove it if there is only one occurence of that mention so far. */ var caret = utils.getCaretPosition(this.refs.message.getDOMNode()); @@ -225,14 +210,13 @@ module.exports = React.createClass({ typingMention = text.substring(atIndex+1, caret); } - var re3 = new RegExp('@' + typingMention + '( |$|\n)', 'g'); + var re2 = new RegExp('@' + typingMention + '( |$|\n)', 'g'); - if ((text.match(re3) || []).length === 1 && mentions.indexOf(typingMention) !== -1) { + if ((text.match(re2) || []).length === 1 && mentions.indexOf(typingMention) !== -1) { mentions.splice(mentions.indexOf(typingMention), 1); } this.updateMentionTab(null, mentions); - this.mentions = mentions; }, checkForNewMention: function(text) { var caret = utils.getCaretPosition(this.refs.message.getDOMNode()); @@ -287,15 +271,9 @@ module.exports = React.createClass({ elm.value = cmd; this.handleChange(); }, - scroll: function() { - var e = this.refs.message.getDOMNode(); - var d = this.refs.textdiv.getDOMNode(); - $(d).scrollTop($(e).scrollTop()); - }, resize: function() { var e = this.refs.message.getDOMNode(); var w = this.refs.wrapper.getDOMNode(); - var d = this.refs.textdiv.getDOMNode(); var lht = parseInt($(e).css('lineHeight'),10); var lines = e.scrollHeight / lht; @@ -303,15 +281,11 @@ module.exports = React.createClass({ if (e.scrollHeight - mod < 167) { $(e).css({'height':'auto','overflow-y':'hidden'}).height(e.scrollHeight - mod); - $(d).css({'height':'auto','overflow-y':'hidden'}).height(e.scrollHeight - mod); $(w).css({'height':'auto'}).height(e.scrollHeight+2); } else { $(e).css({'height':'auto','overflow-y':'scroll'}).height(167); - $(d).css({'height':'auto','overflow-y':'scroll'}).height(167); $(w).css({'height':'auto'}).height(167); } - - $(d).scrollTop($(e).scrollTop()); }, handleFocus: function() { var elm = this.refs.message.getDOMNode(); @@ -332,8 +306,7 @@ module.exports = React.createClass({ return (
-
-