diff --git a/README.md b/README.md index 53259ea10a..a0aeb974cf 100644 --- a/README.md +++ b/README.md @@ -34,22 +34,35 @@ Local Machine Setup (Docker) ### Ubuntu ### 1. Follow the instructions at https://docs.docker.com/installation/ubuntulinux/ or use the summery below. + `sudo apt-get update` + `sudo apt-get install wget` + `wget -qO- https://get.docker.com/ | sh` + `sudo usermod -aG docker ` + `sudo service docker start` + `newgrp docker` + 2. Run `docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform:helium 3. When docker is done fetching the image, open http://localhost:8065/ in your browser ### Arch ### 1. Install docker using the following commands + `pacman -S docker` + `systemctl enable docker.service` + `systemctl start docker.service` + `gpasswd -a docker` + `newgrp docker` + 2. docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform:helium 3. When docker is done fetching the image, open http://localhost:8065/ in your browser diff --git a/api/post.go b/api/post.go index 36607c2315..3acc955515 100644 --- a/api/post.go +++ b/api/post.go @@ -302,7 +302,7 @@ func fireAndForgetNotifications(post *model.Post, teamId, teamUrl string) { // Build a map as a list of unique user_ids that are mentioned in this post splitF := func(c rune) bool { - return c == ',' || c == ' ' || c == '.' || c == '!' || c == '?' || c == ':' || c == '<' || c == '>' + return model.SplitRunes[c] } splitMessage := strings.FieldsFunc(strings.Replace(post.Message, "
", " ", -1), splitF) for _, word := range splitMessage { diff --git a/api/user.go b/api/user.go index 83e29b28e5..6af737df37 100644 --- a/api/user.go +++ b/api/user.go @@ -567,7 +567,7 @@ func getAudits(c *Context, w http.ResponseWriter, r *http.Request) { } } -func createProfileImage(username string, userId string) *image.RGBA { +func createProfileImage(username string, userId string) ([]byte, *model.AppError) { colors := []color.NRGBA{ {197, 8, 126, 255}, @@ -634,16 +634,17 @@ func createProfileImage(username string, userId string) *image.RGBA { gc.Translate(width, height) gc.SetFillColor(image.White) gc.FillString(initials) - return i + + buf := new(bytes.Buffer) + + if imgErr := png.Encode(buf, i); imgErr != nil { + return nil, model.NewAppError("getProfileImage", "Could not encode default profile image", imgErr.Error()) + } else { + return buf.Bytes(), nil + } } func getProfileImage(c *Context, w http.ResponseWriter, r *http.Request) { - if !utils.IsS3Configured() { - c.Err = model.NewAppError("getProfileImage", "Unable to get image. Amazon S3 not configured. ", "") - c.Err.StatusCode = http.StatusNotImplemented - return - } - params := mux.Vars(r) id := params["id"] @@ -651,36 +652,41 @@ func getProfileImage(c *Context, w http.ResponseWriter, r *http.Request) { c.Err = result.Err return } else { - var auth aws.Auth - auth.AccessKey = utils.Cfg.AWSSettings.S3AccessKeyId - auth.SecretKey = utils.Cfg.AWSSettings.S3SecretAccessKey - - s := s3.New(auth, aws.Regions[utils.Cfg.AWSSettings.S3Region]) - bucket := s.Bucket(utils.Cfg.AWSSettings.S3Bucket) - - path := "teams/" + c.Session.TeamId + "/users/" + id + "/profile.png" - var img []byte + var err *model.AppError - if data, getErr := bucket.Get(path); getErr != nil { - rawImg := createProfileImage(result.Data.(*model.User).Username, id) - buf := new(bytes.Buffer) - - if imgErr := png.Encode(buf, rawImg); imgErr != nil { - c.Err = model.NewAppError("getProfileImage", "Could not encode default profile image", imgErr.Error()) - return - } else { - img = buf.Bytes() - } - - options := s3.Options{} - if err := bucket.Put(path, buf.Bytes(), "image", s3.Private, options); err != nil { - c.Err = model.NewAppError("getImage", "Couldn't upload default profile image", err.Error()) + if !utils.IsS3Configured() { + img, err = createProfileImage(result.Data.(*model.User).Username, id) + if err != nil { + c.Err = err return } - } else { - img = data + var auth aws.Auth + auth.AccessKey = utils.Cfg.AWSSettings.S3AccessKeyId + auth.SecretKey = utils.Cfg.AWSSettings.S3SecretAccessKey + + s := s3.New(auth, aws.Regions[utils.Cfg.AWSSettings.S3Region]) + bucket := s.Bucket(utils.Cfg.AWSSettings.S3Bucket) + + path := "teams/" + c.Session.TeamId + "/users/" + id + "/profile.png" + + if data, getErr := bucket.Get(path); getErr != nil { + img, err = createProfileImage(result.Data.(*model.User).Username, id) + if err != nil { + c.Err = err + return + } + + options := s3.Options{} + if err := bucket.Put(path, img, "image", s3.Private, options); err != nil { + c.Err = model.NewAppError("getImage", "Couldn't upload default profile image", err.Error()) + return + } + + } else { + img = data + } } if c.Session.UserId == id { diff --git a/api/user_test.go b/api/user_test.go index 4d5d2b3f0c..92ab216aa2 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -10,6 +10,7 @@ import ( "github.com/goamz/goamz/s3" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" + "image" "image/color" "io" "mime/multipart" @@ -324,14 +325,20 @@ func TestGetAudits(t *testing.T) { func TestUserCreateImage(t *testing.T) { Setup() - i := createProfileImage("Corey Hulen", "eo1zkdr96pdj98pjmq8zy35wba") - if i == nil { - t.Fatal("Failed to gen image") + b, err := createProfileImage("Corey Hulen", "eo1zkdr96pdj98pjmq8zy35wba") + if err != nil { + t.Fatal(err) + } + + rdr := bytes.NewReader(b) + img, _, err2 := image.Decode(rdr) + if err2 != nil { + t.Fatal(err) } colorful := color.RGBA{116, 49, 196, 255} - if i.RGBAAt(1, 1) != colorful { + if img.At(1, 1) != colorful { t.Fatal("Failed to create correct color") } diff --git a/model/utils.go b/model/utils.go index 2541247de4..262bda319e 100644 --- a/model/utils.go +++ b/model/utils.go @@ -319,3 +319,5 @@ 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(`/api/v1/files/(get|get_image)/([A-Za-z0-9]{26})/([A-Za-z0-9]{26})/(([A-Za-z0-9]+/)?.+\.[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/web/react/components/confirm_modal.jsx b/web/react/components/confirm_modal.jsx new file mode 100644 index 0000000000..3be13cf9bf --- /dev/null +++ b/web/react/components/confirm_modal.jsx @@ -0,0 +1,31 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +module.exports = React.createClass({ + handleConfirm: function() { + $('#'+this.props.parent_id).attr('data-confirm', 'true'); + $('#'+this.props.parent_id).modal('hide'); + $('#'+this.props.id).modal('hide'); + }, + render: function() { + return ( + + ); + } +}); + diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx index 1d2bbed84d..5980664de9 100644 --- a/web/react/components/invite_member_modal.jsx +++ b/web/react/components/invite_member_modal.jsx @@ -4,8 +4,37 @@ var utils = require('../utils/utils.jsx'); var Client =require('../utils/client.jsx'); var UserStore = require('../stores/user_store.jsx'); +var ConfirmModal = require('./confirm_modal.jsx'); module.exports = React.createClass({ + componentDidMount: function() { + var self = this; + $('#invite_member').on('hide.bs.modal', function(e) { + if ($('#invite_member').attr('data-confirm') === 'true') { + $('#invite_member').attr('data-confirm', 'false'); + return; + } + + var not_empty = false; + for (var i = 0; i < self.state.invite_ids.length; i++) { + var index = self.state.invite_ids[i]; + if (self.refs["email"+index].getDOMNode().value.trim() !== '') { + not_empty = true; + break; + } + } + + if (not_empty) { + $('#confirm_invite_modal').modal('show'); + e.preventDefault(); + } + + }); + + $('#invite_member').on('hidden.bs.modal', function() { + self.clearFields(); + }); + }, handleSubmit: function(e) { var invite_ids = this.state.invite_ids; var count = invite_ids.length; @@ -56,22 +85,8 @@ module.exports = React.createClass({ Client.inviteMembers(data, function() { + $(this.refs.modal.getDOMNode()).attr('data-confirm', 'true'); $(this.refs.modal.getDOMNode()).modal('hide'); - for (var i = 0; i < invite_ids.length; i++) { - var index = invite_ids[i]; - this.refs["email"+index].getDOMNode().value = ""; - if (config.AllowInviteNames) { - this.refs["first_name"+index].getDOMNode().value = ""; - this.refs["last_name"+index].getDOMNode().value = ""; - } - } - this.setState({ - invite_ids: [0], - id_count: 0, - email_errors: {}, - first_name_errors: {}, - last_name_errors: {} - }); }.bind(this), function(err) { this.setState({ server_error: err }); @@ -89,6 +104,26 @@ module.exports = React.createClass({ invite_ids.push(count); this.setState({ invite_ids: invite_ids, id_count: count }); }, + clearFields: function() { + var invite_ids = this.state.invite_ids; + + for (var i = 0; i < invite_ids.length; i++) { + var index = invite_ids[i]; + this.refs["email"+index].getDOMNode().value = ""; + if (config.AllowInviteNames) { + this.refs["first_name"+index].getDOMNode().value = ""; + this.refs["last_name"+index].getDOMNode().value = ""; + } + } + + this.setState({ + invite_ids: [0], + id_count: 0, + email_errors: {}, + first_name_errors: {}, + last_name_errors: {} + }); + }, removeInviteFields: function(index) { var invite_ids = this.state.invite_ids; var i = invite_ids.indexOf(index); @@ -147,29 +182,38 @@ module.exports = React.createClass({ var server_error = this.state.server_error ?
: null; return ( -