diff --git a/CHANGELOG.md b/CHANGELOG.md index 5876dc894a..7aac49ff25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ Documentation - Updated software and hardware requirements documentation - Re-organized install instructions out of README and into separate files +- Added Code Contribution Guidelines +- Added new hardware sizing recommendations - Consolidated licensing information into LICENSE.txt and NOTICE.txt Performance @@ -33,7 +35,8 @@ Code Quality ### Bug Fixes -- Fixed performance issue with slow typing on iOS +- Fixed performance issue with slow typing on iOS +- Fixed issue so that SSO option automatically set EmailVerified=true (it was false previously) ### Contributors diff --git a/Makefile b/Makefile index 305926e5bc..ea9fc21d43 100644 --- a/Makefile +++ b/Makefile @@ -78,9 +78,10 @@ travis: mv $(DIST_PATH)/web/static/js/bundle.min.js $(DIST_PATH)/web/static/js/bundle-$(BUILD_NUMBER).min.js - @sed -i'.bak' 's|react-with-addons-0.13.1.js|react-with-addons-0.13.1.min.js|g' $(DIST_PATH)/web/templates/head.html + @sed -i'.bak' 's|react-with-addons-0.13.3.js|react-with-addons-0.13.3.min.js|g' $(DIST_PATH)/web/templates/head.html @sed -i'.bak' 's|jquery-1.11.1.js|jquery-1.11.1.min.js|g' $(DIST_PATH)/web/templates/head.html - @sed -i'.bak' 's|bootstrap-3.3.1.js|bootstrap-3.3.1.min.js|g' $(DIST_PATH)/web/templates/head.html + @sed -i'.bak' 's|bootstrap-3.3.5.js|bootstrap-3.3.5.min.js|g' $(DIST_PATH)/web/templates/head.html + @sed -i'.bak' 's|react-bootstrap-0.25.1.js|react-bootstrap-0.25.1.min.js|g' $(DIST_PATH)/web/templates/head.html @sed -i'.bak' 's|perfect-scrollbar.js|perfect-scrollbar.min.js|g' $(DIST_PATH)/web/templates/head.html @sed -i'.bak' 's|bundle.js|bundle-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html rm $(DIST_PATH)/web/templates/*.bak @@ -231,9 +232,10 @@ dist: install mv $(DIST_PATH)/web/static/js/bundle.min.js $(DIST_PATH)/web/static/js/bundle-$(BUILD_NUMBER).min.js - @sed -i'.bak' 's|react-with-addons-0.13.1.js|react-with-addons-0.13.1.min.js|g' $(DIST_PATH)/web/templates/head.html + @sed -i'.bak' 's|react-with-addons-0.13.3.js|react-with-addons-0.13.3.min.js|g' $(DIST_PATH)/web/templates/head.html @sed -i'.bak' 's|jquery-1.11.1.js|jquery-1.11.1.min.js|g' $(DIST_PATH)/web/templates/head.html - @sed -i'.bak' 's|bootstrap-3.3.1.js|bootstrap-3.3.1.min.js|g' $(DIST_PATH)/web/templates/head.html + @sed -i'.bak' 's|bootstrap-3.3.5.js|bootstrap-3.3.5.min.js|g' $(DIST_PATH)/web/templates/head.html + @sed -i'.bak' 's|react-bootstrap-0.25.1.js|react-bootstrap-0.25.1.min.js|g' $(DIST_PATH)/web/templates/head.html @sed -i'.bak' 's|perfect-scrollbar.js|perfect-scrollbar.min.js|g' $(DIST_PATH)/web/templates/head.html @sed -i'.bak' 's|bundle.js|bundle-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html rm $(DIST_PATH)/web/templates/*.bak diff --git a/README.md b/README.md index de0f4b0793..8cbddc1c62 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Mattermost is an open source, on-prem Slack-alternative. -It modernizes team communication without locking in your data to a single provider. Offer your end users messaging and file sharing across PCs and phones with archiving and instant search--without losing control of your data. +It offers modern communication from behind your firewall, including messaging and file sharing across PCs and phones with archiving and instant search. ## All team communication in one place, searchable and accessible anywhere @@ -26,12 +26,6 @@ Please see the [features pages of the Mattermost website](http://www.mattermost. - Attach sound, video and image files from mobile devices - Define team-specific branding and color themes across your devices -#### Enterprise Compatible - -- Self-host Mattermost entirely within your organization's insfrastructure -- GitLab Mattermost omnibus supports install to over 100,000 organizations using GitLab -- LDAP/Active Directory connectivity to Mattermost enabled via GitLab Single-Sign-On - ## Learn More - [Product Vision and Target Audiences](http://www.mattermost.org/vision/) - What we're solving and for whom are we building @@ -48,9 +42,9 @@ There are multiple ways to install Mattermost depending on your needs. #### Quick Start Install for Product Evaluation -- [Local Machine Install and Upgrade with Docker](doc/install/single-container-install.md) - Explore product functionality using a single-container Docker install on a local machine, including Mac OSX, Ubuntu, or Arch Linux). Optionally set up email and upgrade your instance using DockerHub. +- [Local Machine Install with Docker](doc/install/single-container-install.md) - Explore product functionality using a single-container Docker install on a local machine, including Mac OSX, Ubuntu, or Arch Linux). Optionally set up email and upgrade your instance using DockerHub. -- [AWS EBS Install and Upgrade with Docker](doc/install/aws-ebs-setup.md) - Explore product functionality using a single-container Docker install for Amazon Web Services Elastic Beanstalk. Optionally set up email and upgrade your instance using DockerHub. +- [AWS EBS Install with Docker](doc/install/aws-ebs-setup.md) - Explore product functionality using a single-container Docker install for Amazon Web Services Elastic Beanstalk. Optionally set up email and upgrade your instance using DockerHub. #### Development Install @@ -63,3 +57,38 @@ Prior to production installation, please review [Mattermost system requirements] - [GitLab Mattermost Production Installation](https://gitlab.com/gitlab-org/gitlab-mattermost) - Install Mattermost for production environments bundled with GitLab, a leading open source Git repository, using an omnibus package for Ubuntu 12.04, Ubuntu 14.04, Debian 7, Debian 8, and CentOS 6 (and RedHat/Oracle/Scientific Linux 6), CentOS 7 (and RedHat/Oracle/Scientific Linux 7). For technical questions and answers, please visit the [Mattermost forum](http://forum.mattermost.org). + +## Get Involved with Mattermost + +Joining the Mattermost community is a great way to build relationships with other talented and like-minded professionals, increase awareness of the interesting work you are doing, and sharpen your skills. Here are some of the ways that you can make a difference in the Mattermost ecosystem: + +#### Discuss + +- Visit the [Mattermost Forum](http://forum.mattermost.org/) for technical questions and answers. +- [Join](https://groups.google.com/a/mattermost.com/forum/#!forum/general/join) the [Mattermost User Mailing List](https://groups.google.com/a/mattermost.com/forum/#!forum/general) for general, open-ended questions and software announcements. +- [Join](https://groups.google.com/a/mattermost.com/forum/#!forum/developer/join) the [Mattermost Developers Mailing List](https://groups.google.com/a/mattermost.com/forum/#!forum/developer) for discussing contributing code to Mattermost. + +#### Review + +- Share feedback on [Mattermost Design Feedback Requests](http://forum.mattermost.org/c/feature-ideas/specifications), which offer early previews of designs community comments and feedback. +- Visit the [Feature Ideas Forum](http://mattermost.uservoice.com/forums/306457-general) and consider upvoting community feature ideas, which are reviewed for each monthly release. + +#### Share + +- Blog about your Mattermost experiences and use cases, either on your personal blog, the [Mattermost forum](http://forum.mattermost.org), or contribute a guest post to the [Mattermost blog](http://www.mattermost.org/category/blog/). If you write about Mattermost. please contact our community team at info@mattermost.com for help promoting your content. +- Tweet to share with your community and friends why you use Mattermost. Follow [@MattermostHQ](https://twitter.com/mattermosthq) on Twitter and conversations on [#mattermost](https://twitter.com/search?q=%23mattermost&src=typd). + +#### Write + +- As with most open source projects, Mattermost documentation is maintained in a public repository. You can propose changes by [submitting pull requests (no programming skills required)](http://forum.mattermost.org/t/help-improve-mattermost-documentation/194). We highly welcome you to add improvements, write guides and tutorials, and expand on sections. +- Prior to contributing, please review [Mattermost Documentation Guidelines](http://www.mattermost.org/documentation-guidelines/), which include standards on writing Mattermost documentation for a global audience, who might not use English as their first language. + +#### Contribute + +- Share [feature ideas](http://www.mattermost.org/feature-requests/) with the Mattermost community +- Review the [Mattermost Code Contribution Guidelines](doc/developer/code-contribution.md) to submit patches for the core product +- Consider building tools that help developers and IT professionals manage Mattermost more effectively (API documentation coming in Beta2) + +#### Have other ideas or suggestions? + +If there’s some other way you’d like to contribute, please contact us at info@mattermost.com. We’d love to meet you! diff --git a/api/channel.go b/api/channel.go index b403667198..63acaa8d1c 100644 --- a/api/channel.go +++ b/api/channel.go @@ -191,7 +191,7 @@ func updateChannel(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !strings.Contains(channelMember.Roles, model.CHANNEL_ROLE_ADMIN) && !strings.Contains(c.Session.Roles, model.ROLE_ADMIN) { + if !strings.Contains(channelMember.Roles, model.CHANNEL_ROLE_ADMIN) && !strings.Contains(c.Session.Roles, model.ROLE_TEAM_ADMIN) { c.Err = model.NewAppError("updateChannel", "You do not have the appropriate permissions", "") c.Err.StatusCode = http.StatusForbidden return @@ -514,7 +514,7 @@ func deleteChannel(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !strings.Contains(channelMember.Roles, model.CHANNEL_ROLE_ADMIN) && !strings.Contains(c.Session.Roles, model.ROLE_ADMIN) { + if !strings.Contains(channelMember.Roles, model.CHANNEL_ROLE_ADMIN) && !strings.Contains(c.Session.Roles, model.ROLE_TEAM_ADMIN) { c.Err = model.NewAppError("deleteChannel", "You do not have the appropriate permissions", "") c.Err.StatusCode = http.StatusForbidden return @@ -756,7 +756,7 @@ func removeChannelMember(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !strings.Contains(channelMember.Roles, model.CHANNEL_ROLE_ADMIN) && !strings.Contains(c.Session.Roles, model.ROLE_ADMIN) { + if !strings.Contains(channelMember.Roles, model.CHANNEL_ROLE_ADMIN) && !strings.Contains(c.Session.Roles, model.ROLE_TEAM_ADMIN) { c.Err = model.NewAppError("updateChannel", "You do not have the appropriate permissions ", "") c.Err.StatusCode = http.StatusForbidden return diff --git a/api/context.go b/api/context.go index ea5677f95d..fc7d8f23d1 100644 --- a/api/context.go +++ b/api/context.go @@ -106,6 +106,9 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if !h.isApi { w.Header().Set("X-Frame-Options", "DENY") w.Header().Set("Content-Security-Policy", "frame-ancestors none") + } else { + // All api response bodies will be JSON formatted + w.Header().Set("Content-Type", "application/json") } sessionId := "" @@ -308,7 +311,7 @@ func (c *Context) IsTeamAdmin(userId string) bool { return false } else { user := uresult.Data.(*model.User) - return model.IsInRole(c.Session.Roles, model.ROLE_ADMIN) && user.TeamId == c.Session.TeamId + return model.IsInRole(c.Session.Roles, model.ROLE_TEAM_ADMIN) && user.TeamId == c.Session.TeamId } } diff --git a/api/file.go b/api/file.go index 1d8244fac4..692558acf3 100644 --- a/api/file.go +++ b/api/file.go @@ -349,6 +349,7 @@ func getFile(c *Context, w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "max-age=2592000, public") w.Header().Set("Content-Length", strconv.Itoa(len(f))) + w.Header().Set("Content-Type", "") // need to provide proper Content-Type in the future w.Write(f) } diff --git a/api/post.go b/api/post.go index f969dd031e..bd31e0210a 100644 --- a/api/post.go +++ b/api/post.go @@ -716,7 +716,7 @@ func deletePost(c *Context, w http.ResponseWriter, r *http.Request) { return } - if post.UserId != c.Session.UserId && !model.IsInRole(c.Session.Roles, model.ROLE_ADMIN) { + if post.UserId != c.Session.UserId && !model.IsInRole(c.Session.Roles, model.ROLE_TEAM_ADMIN) { c.Err = model.NewAppError("deletePost", "You do not have the appropriate permissions", "") c.Err.StatusCode = http.StatusForbidden return diff --git a/api/slackimport.go b/api/slackimport.go index 1d037a934c..4e6c01dbba 100644 --- a/api/slackimport.go +++ b/api/slackimport.go @@ -50,6 +50,15 @@ func SlackConvertTimeStamp(ts string) int64 { return timeStamp * 1000 // Convert to milliseconds } +func SlackConvertChannelName(channelName string) string { + newName := strings.Trim(channelName, "_-") + if len(newName) == 1 { + return "slack-channel-" + newName + } + + return newName +} + func SlackParseChannels(data io.Reader) []SlackChannel { decoder := json.NewDecoder(data) @@ -172,7 +181,7 @@ func SlackAddChannels(teamId string, slackchannels []SlackChannel, posts map[str TeamId: teamId, Type: model.CHANNEL_OPEN, DisplayName: sChannel.Name, - Name: sChannel.Name, + Name: SlackConvertChannelName(sChannel.Name), Description: sChannel.Topic["value"], } mChannel := ImportChannel(&newChannel) diff --git a/api/team.go b/api/team.go index 9288b86cb6..8258fa9290 100644 --- a/api/team.go +++ b/api/team.go @@ -477,7 +477,7 @@ func InviteMembers(c *Context, team *model.Team, user *model.User, invites []str sender := user.GetDisplayName() senderRole := "" - if model.IsInRole(user.Roles, model.ROLE_ADMIN) || model.IsInRole(user.Roles, model.ROLE_SYSTEM_ADMIN) { + if model.IsInRole(user.Roles, model.ROLE_TEAM_ADMIN) || model.IsInRole(user.Roles, model.ROLE_SYSTEM_ADMIN) { senderRole = "administrator" } else { senderRole = "member" @@ -536,7 +536,7 @@ func updateTeamDisplayName(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !model.IsInRole(c.Session.Roles, model.ROLE_ADMIN) { + if !model.IsInRole(c.Session.Roles, model.ROLE_TEAM_ADMIN) { c.Err = model.NewAppError("updateTeamDisplayName", "You do not have the appropriate permissions", "userId="+c.Session.UserId) c.Err.StatusCode = http.StatusForbidden return @@ -576,7 +576,7 @@ func updateValetFeature(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !model.IsInRole(c.Session.Roles, model.ROLE_ADMIN) { + if !model.IsInRole(c.Session.Roles, model.ROLE_TEAM_ADMIN) { c.Err = model.NewAppError("updateValetFeature", "You do not have the appropriate permissions", "userId="+c.Session.UserId) c.Err.StatusCode = http.StatusForbidden return diff --git a/api/user.go b/api/user.go index f32bbbe13c..c87b89c7a2 100644 --- a/api/user.go +++ b/api/user.go @@ -170,7 +170,7 @@ func CreateUser(c *Context, team *model.Team, user *model.User) *model.User { channelRole := "" if team.Email == user.Email { - user.Roles = model.ROLE_ADMIN + user.Roles = model.ROLE_TEAM_ADMIN channelRole = model.CHANNEL_ROLE_ADMIN } else { user.Roles = "" @@ -945,7 +945,7 @@ func updateRoles(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !model.IsInRole(c.Session.Roles, model.ROLE_ADMIN) && !c.IsSystemAdmin() { + if !model.IsInRole(c.Session.Roles, model.ROLE_TEAM_ADMIN) && !c.IsSystemAdmin() { c.Err = model.NewAppError("updateRoles", "You do not have the appropriate permissions", "userId="+user_id) c.Err.StatusCode = http.StatusForbidden return @@ -984,7 +984,7 @@ func UpdateRoles(c *Context, user *model.User, roles string) *model.User { // make sure there is at least 1 other active admin if !model.IsInRole(roles, model.ROLE_SYSTEM_ADMIN) { - if model.IsInRole(user.Roles, model.ROLE_ADMIN) && !model.IsInRole(roles, model.ROLE_ADMIN) { + if model.IsInRole(user.Roles, model.ROLE_TEAM_ADMIN) && !model.IsInRole(roles, model.ROLE_TEAM_ADMIN) { if result := <-Srv.Store.User().GetProfiles(user.TeamId); result.Err != nil { c.Err = result.Err return nil @@ -992,7 +992,7 @@ func UpdateRoles(c *Context, user *model.User, roles string) *model.User { activeAdmins := -1 profileUsers := result.Data.(map[string]*model.User) for _, profileUser := range profileUsers { - if profileUser.DeleteAt == 0 && model.IsInRole(profileUser.Roles, model.ROLE_ADMIN) { + if profileUser.DeleteAt == 0 && model.IsInRole(profileUser.Roles, model.ROLE_TEAM_ADMIN) { activeAdmins = activeAdmins + 1 } } @@ -1042,14 +1042,14 @@ func updateActive(c *Context, w http.ResponseWriter, r *http.Request) { return } - if !model.IsInRole(c.Session.Roles, model.ROLE_ADMIN) && !c.IsSystemAdmin() { + if !model.IsInRole(c.Session.Roles, model.ROLE_TEAM_ADMIN) && !c.IsSystemAdmin() { c.Err = model.NewAppError("updateActive", "You do not have the appropriate permissions", "userId="+user_id) c.Err.StatusCode = http.StatusForbidden return } // make sure there is at least 1 other active admin - if !active && model.IsInRole(user.Roles, model.ROLE_ADMIN) { + if !active && model.IsInRole(user.Roles, model.ROLE_TEAM_ADMIN) { if result := <-Srv.Store.User().GetProfiles(user.TeamId); result.Err != nil { c.Err = result.Err return @@ -1057,7 +1057,7 @@ func updateActive(c *Context, w http.ResponseWriter, r *http.Request) { activeAdmins := -1 profileUsers := result.Data.(map[string]*model.User) for _, profileUser := range profileUsers { - if profileUser.DeleteAt == 0 && model.IsInRole(profileUser.Roles, model.ROLE_ADMIN) { + if profileUser.DeleteAt == 0 && model.IsInRole(profileUser.Roles, model.ROLE_TEAM_ADMIN) { activeAdmins = activeAdmins + 1 } } diff --git a/api/user_test.go b/api/user_test.go index 8c037fdf36..fe5a4a27fb 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -509,7 +509,7 @@ func TestUserUpdate(t *testing.T) { user.TeamId = "12345678901234567890123456" user.LastActivityAt = time2 user.LastPingAt = time2 - user.Roles = model.ROLE_ADMIN + user.Roles = model.ROLE_TEAM_ADMIN user.LastPasswordUpdate = 123 if result, err := Client.UpdateUser(user); err != nil { diff --git a/doc/README.md b/doc/README.md index 36d16b7447..491c75b9ba 100644 --- a/doc/README.md +++ b/doc/README.md @@ -16,9 +16,10 @@ ### Configuration - [GitLab SSO Configuration](integrations/sso/gitlab-sso.md) -- [SMTP Email Setup](smtp-email-setup.md) +- [SMTP Email Setup](config/smtp-email-setup.md) ## Developer Documentation -- [Developer Machine Setup](doc/install/dev-setup.md) +- [Code Contribution Guidelines](developer/code-contribution.md) +- [Developer Machine Setup](install/dev-setup.md) - [Mattermost Style Guide](developer/style-guide.md) diff --git a/doc/developer/code-contribution.md b/doc/developer/code-contribution.md new file mode 100644 index 0000000000..1a6537287e --- /dev/null +++ b/doc/developer/code-contribution.md @@ -0,0 +1,44 @@ +# Code Contribution Guidelines + +Thank you for your interest in contributing to Mattermost. This guide provides an overview of important information for contributors to know. + +## Choose a Ticket + +1. Review the list of [Good First Contribution](https://mattermost.atlassian.net/issues/?filter=10206) tickets listed in Jira. + +2. These projects are intended to be a straight forward first pull requests from new contributors. +If you don't find something appropriate for your interests, please see the full list of tickets [Accepting Pull Requests](https://mattermost.atlassian.net/issues/?filter=10101). + +3. If you have any questions at all about a ticket, please post to the [Contributor Discussion section](http://forum.mattermost.org/) of the Mattermost forum, or email the [Mattermost Developer Mailing list](https://groups.google.com/a/mattermost.com/forum/#!forum/developer/join). + +## Install Mattermost and set up a Fork + +1. Follow [developer setup instructions](https://github.com/mattermost/platform/blob/master/doc/install/dev-setup.md) to install Mattermost. + +2. Create a branch with set to the ID of the ticket you're working on, for example ```PLT-394```, using command: + +``` +git checkout -b +``` + +## Programming and Testing + +1. Please review the [Mattermost Style Guide](developer/style-guide.md) prior to making changes. + +2. Please make sure to thoroughly test your change before submitting a pull request. + +## Submitting a Pull Request + +1. Please add yourself to the contributor list prior to submitting by completing the [contributor license agreement](http://www.mattermost.org/mattermost-contributor-agreement/). + +2. When you submit your pull request please include the Ticket ID at the beginning of your pull request comment, followed by a colon. + + For example, for a ticket ID `PLT-394` start your comment with: `PLT-394:`. See [previously closed pull requests](https://github.com/mattermost/platform/pulls?q=is%3Apr+is%3Aclosed) for examples. + +3. Once submitted, your pull request will be checked via an automated build process and will be reviewed by the Mattermost core team, who may either accept the PR or follow-up with feedback. + +4. If you've included your mailing address in Step 1, you'll be receiving a [Limited Edition Mattermost Mug](http://forum.mattermost.org/t/limited-edition-mattermost-mugs/143) as a thank you gift after your first pull request has been accepted. + + + + diff --git a/doc/install/dev-setup.md b/doc/install/dev-setup.md index a088bbbc2e..b90b6a3511 100644 --- a/doc/install/dev-setup.md +++ b/doc/install/dev-setup.md @@ -10,7 +10,7 @@ Developer Machine Setup 2. Get your Docker IP address with `boot2docker ip` 3. Add a line to your /etc/hosts that goes ` dockerhost` 4. Run `boot2docker shellinit` and copy the export statements to your ~/.bash_profile -2. Download Go from http://golang.org/dl/ +2. Download Go (version 1.4.2) from http://golang.org/dl/ 3. Set up your Go workspace 1. `mkdir ~/go` 2. Add the following to your ~/.bash_profile @@ -22,13 +22,13 @@ Developer Machine Setup 1. Download Homebrew from http://brew.sh/ 2. `brew install node` 5. Install Compass - 1. Make sure you have the latest verison of Ruby - 2. `gem install compass` + 1. Run `ruby -v` and check the ruby version is 1.8.7 or higher + 2. `sudo gem install compass` 6. Download Mattermost `cd ~/go` `mkdir -p src/github.com/mattermost` `cd src/github.com/mattermost` - `git clone github.com/mattermost/platform.git` + `git clone https://github.com/mattermost/platform.git` `cd platform` 7. Run unit tests on Mattermost using `make test` to make sure the installation was successful 8. If tests passed, you can now run Mattermost using `make run` @@ -50,7 +50,7 @@ Any issues? Please let us know on our forums at: http://forum.mattermost.org `127.0.0.1 dockerhost` 3. Install build essentials 1. `apt-get install build-essential` -4. Download Go from http://golang.org/dl/ +4. Download Go (version 1.4.2) from http://golang.org/dl/ 5. Set up your Go workspace and add Go to the PATH 1. `mkdir ~/go` 2. Add the following to your ~/.bashrc @@ -74,7 +74,7 @@ Any issues? Please let us know on our forums at: http://forum.mattermost.org `cd ~/go` `mkdir -p src/github.com/mattermost` `cd src/github.com/mattermost` - `git clone github.com/mattermost/platform.git` + `git clone https://github.com/mattermost/platform.git` `cd platform` 9. Run unit tests on Mattermost using `make test` to make sure the installation was successful 10. If tests passed, you can now run Mattermost using `make run` diff --git a/docker/dev/README.md b/docker/dev/README.md index a4b6fb9bb0..f737a15549 100644 --- a/docker/dev/README.md +++ b/docker/dev/README.md @@ -1,28 +1,13 @@ -**Mattermost Alpha** -**Team Communication Service** -**Development Build** +Mattermost +========== - -About Mattermost -================ +http:/mattermost.org Mattermost is an open-source team communication service. It brings team messaging and file sharing into one place, accessible across PCs and phones, with archiving and search. - - Installing Mattermost ===================== -You're installing "Mattermost Alpha", a pre-released version providing an early look at what we're building. While the core team runs this version internally, it's not recommended for production since we can't guarantee API stability or backwards compatibility. - -That said, any issues at all, please let us know on the Mattermost forum. - -Notes: -- For Alpha, Docker is intentionally setup as a single container, since production deployment is not yet recommended. - -Local Machine Setup (Docker) ------------------------------ - To run an instance of the latest version of mattermost on your local machine you can run: `docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform` @@ -35,9 +20,4 @@ To run an instance of the latest code from the master branch on GitHub you can r `docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform:dev` - -License -------- - -Mattermost is licensed under an "Apache-wrapped AGPL" model inspired by MongoDB. Similar to MongoDB, you can run and link to the system using Configuration Files and Admin Tools licensed under Apache, version 2.0, as described in the LICENSE file, as an explicit exception to the terms of the GNU Affero General Public License (AGPL) that applies to most of the remaining source files. See individual files for details. - +Any questions, please visit http://forum.mattermost.org diff --git a/mattermost.go b/mattermost.go index 08963f0168..499abcd92d 100644 --- a/mattermost.go +++ b/mattermost.go @@ -328,9 +328,9 @@ Usage: Example: platform -create_user -team_name="name" -email="user@example.com" -password="mypassword" - -assign_role Assigns role to a user. It requres the -team_name, - -email and -role flag. If you're assigning the - "system_admin" role it must be for a user on the + -assign_role Assigns role to a user. It requres the -role, + -email and -team_name flag. If you're assigning the + role="system_admin" role it must be for a user on the team_name="admin" Example: platform -assign_role -team_name="name" -email="user@example.com" -role="admin" diff --git a/model/team.go b/model/team.go index 6006f738c6..8b4f82830c 100644 --- a/model/team.go +++ b/model/team.go @@ -158,7 +158,7 @@ func IsReservedTeamName(s string) bool { func IsValidTeamName(s string) bool { - if !IsValidAlphaNum(s) { + if !IsValidAlphaNum(s, false) { return false } diff --git a/model/user.go b/model/user.go index 1a169f763f..fdc519b990 100644 --- a/model/user.go +++ b/model/user.go @@ -13,7 +13,7 @@ import ( ) const ( - ROLE_ADMIN = "admin" + ROLE_TEAM_ADMIN = "admin" ROLE_SYSTEM_ADMIN = "system_admin" USER_AWAY_TIMEOUT = 5 * 60 * 1000 // 5 minutes USER_OFFLINE_TIMEOUT = 1 * 60 * 1000 // 1 minute @@ -289,7 +289,7 @@ func isValidRole(role string) bool { return true } - if role == ROLE_ADMIN { + if role == ROLE_TEAM_ADMIN { return true } diff --git a/model/utils.go b/model/utils.go index 17d1c6317e..d5122e805e 100644 --- a/model/utils.go +++ b/model/utils.go @@ -202,7 +202,7 @@ func GetSubDomain(s string) (string, string) { func IsValidChannelIdentifier(s string) bool { - if !IsValidAlphaNum(s) { + if !IsValidAlphaNum(s, true) { return false } @@ -213,10 +213,16 @@ func IsValidChannelIdentifier(s string) bool { return true } +var validAlphaNumUnderscore = regexp.MustCompile(`^[a-z0-9]+([a-z\-\_0-9]+|(__)?)[a-z0-9]+$`) var validAlphaNum = regexp.MustCompile(`^[a-z0-9]+([a-z\-0-9]+|(__)?)[a-z0-9]+$`) -func IsValidAlphaNum(s string) bool { - match := validAlphaNum.MatchString(s) +func IsValidAlphaNum(s string, allowUnderscores bool) bool { + var match bool + if allowUnderscores { + match = validAlphaNumUnderscore.MatchString(s) + } else { + match = validAlphaNum.MatchString(s) + } if !match { return false diff --git a/store/sql_user_store.go b/store/sql_user_store.go index be1d29df0a..52d670d569 100644 --- a/store/sql_user_store.go +++ b/store/sql_user_store.go @@ -74,7 +74,7 @@ func (us SqlUserStore) Save(user *model.User) StoreChannel { close(storeChannel) return } else if int(count) > utils.Cfg.TeamSettings.MaxUsersPerTeam { - result.Err = model.NewAppError("SqlUserStore.Save", "You've reached the limit of the number of allowed accounts.", "teamId="+user.TeamId) + result.Err = model.NewAppError("SqlUserStore.Save", "This team has reached the maxmium number of allowed accounts. Contact your systems administrator to set a higher limit.", "teamId="+user.TeamId) storeChannel <- result close(storeChannel) return diff --git a/utils/textgeneration.go b/utils/textgeneration.go index 043d7bb8ae..6d353cc246 100644 --- a/utils/textgeneration.go +++ b/utils/textgeneration.go @@ -141,17 +141,14 @@ var FUZZY_STRINGS_NAMES = []string{ // Strings that should pass as acceptable emails var FUZZY_STRINGS_EMAILS = []string{ "sue@thatmightbe", - "sue@thatmightbe.", "sue@thatmightbe.c", "sue@thatmightbe.co", "su+san@thatmightbe.com", - "a@b.中国", "1@2.am", "a@b.co.uk", "a@b.cancerresearch", "su+s+an@thatmightbe.com", "per.iod@thatmightbe.com", - "per..iods@thatmightbe.com", } // Lovely giberish for all to use diff --git a/web/react/.eslintrc b/web/react/.eslintrc index 53cc759139..c0d0bb2006 100644 --- a/web/react/.eslintrc +++ b/web/react/.eslintrc @@ -18,7 +18,8 @@ "es6": true }, "globals": { - "React": false + "React": false, + "ReactBootstrap": false }, "rules": { "comma-dangle": [2, "never"], diff --git a/web/react/components/change_url_modal.jsx b/web/react/components/change_url_modal.jsx new file mode 100644 index 0000000000..28fa70c1f4 --- /dev/null +++ b/web/react/components/change_url_modal.jsx @@ -0,0 +1,177 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var Modal = ReactBootstrap.Modal; +var Utils = require('../utils/utils.jsx'); + +export default class ChangeUrlModal extends React.Component { + constructor(props) { + super(props); + + this.onURLChanged = this.onURLChanged.bind(this); + this.doSubmit = this.doSubmit.bind(this); + this.doCancel = this.doCancel.bind(this); + + this.state = { + currentURL: props.currentURL, + urlError: '', + userEdit: false + }; + } + componentWillReceiveProps(nextProps) { + // This check prevents the url being deleted when we re-render + // because of user status check + if (!this.state.userEdit) { + this.setState({ + currentURL: nextProps.currentURL + }); + } + } + componentDidUpdate(prevProps) { + if (this.props.show === true && prevProps.show === false) { + React.findDOMNode(this.refs.urlinput).select(); + } + } + onURLChanged(e) { + const url = e.target.value.trim(); + this.setState({currentURL: url.replace(/[^A-Za-z0-9-_]/g, '').toLowerCase(), userEdit: true}); + } + getURLError(url) { + let error = []; //eslint-disable-line prefer-const + if (url.length < 2) { + error.push({'Must be longer than two characters'}
); + } + if (url.charAt(0) === '-' || url.charAt(0) === '_') { + error.push({'Must start with a letter or number'}
); + } + if (url.length > 1 && (url.charAt(url.length - 1) === '-' || url.charAt(url.length - 1) === '_')) { + error.push({'Must end with a letter or number'}
); + } + if (url.indexOf('__') > -1) { + error.push({'Can not contain two underscores in a row.'}
); + } + + // In case of error we don't detect + if (error.length === 0) { + error.push({'Invalid URL'}
); + } + return error; + } + doSubmit(e) { + e.preventDefault(); + + const url = React.findDOMNode(this.refs.urlinput).value; + const cleanedURL = Utils.cleanUpUrlable(url); + if (cleanedURL !== url || url.length < 2 || url.indexOf('__') > -1) { + this.setState({urlError: this.getURLError(url)}); + return; + } + this.setState({urlError: '', userEdit: false}); + this.props.onModalSubmit(url); + } + doCancel() { + this.setState({urlError: '', userEdit: false}); + this.props.onModalDismissed(); + } + render() { + let urlClass = 'input-group input-group--limit'; + let urlError = null; + let serverError = null; + + if (this.state.urlError) { + urlClass += ' has-error'; + urlError = (

{this.state.urlError}

); + } + + if (this.props.serverError) { + serverError =

{this.props.serverError}

; + } + + const fullTeamUrl = Utils.getTeamURLFromAddressBar(); + const teamURL = Utils.getShortenedTeamURL(); + + return ( + + + {this.props.title} + +
+ +
{this.props.description}
+
+ +
+
+ + {teamURL} + + +
+ {urlError} + {serverError} +
+
+
+ + + + +
+
+ ); + } +} + +ChangeUrlModal.defaultProps = { + show: false, + title: 'Change URL', + desciption: '', + urlLabel: 'URL', + submitButtonText: 'Submit', + currentURL: '', + serverError: '' +}; + +ChangeUrlModal.propTypes = { + show: React.PropTypes.bool.isRequired, + title: React.PropTypes.string, + description: React.PropTypes.string, + urlLabel: React.PropTypes.string, + submitButtonText: React.PropTypes.string, + currentURL: React.PropTypes.string, + serverError: React.PropTypes.string, + onModalSubmit: React.PropTypes.func.isRequired, + onModalDismissed: React.PropTypes.func.isRequired +}; diff --git a/web/react/components/file_upload.jsx b/web/react/components/file_upload.jsx index 534f0136ee..3cb2841717 100644 --- a/web/react/components/file_upload.jsx +++ b/web/react/components/file_upload.jsx @@ -20,12 +20,11 @@ export default class FileUpload extends React.Component { } fileUploadSuccess(channelId, data) { - var parsedData = $.parseJSON(data); - this.props.onFileUpload(parsedData.filenames, parsedData.client_ids, channelId); + this.props.onFileUpload(data.filenames, data.client_ids, channelId); var requests = this.state.requests; - for (var j = 0; j < parsedData.client_ids.length; j++) { - delete requests[parsedData.client_ids[j]]; + for (var j = 0; j < data.client_ids.length; j++) { + delete requests[data.client_ids[j]]; } this.setState({requests: requests}); } @@ -53,7 +52,7 @@ export default class FileUpload extends React.Component { } // generate a unique id that can be used by other components to refer back to this upload - var clientId = utils.generateId(); + let clientId = utils.generateId(); // prepare data to be uploaded var formData = new FormData(); diff --git a/web/react/components/new_channel.jsx b/web/react/components/new_channel.jsx deleted file mode 100644 index 1a11fc652c..0000000000 --- a/web/react/components/new_channel.jsx +++ /dev/null @@ -1,211 +0,0 @@ -// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. -// See License.txt for license information. - -var utils = require('../utils/utils.jsx'); -var client = require('../utils/client.jsx'); -var asyncClient = require('../utils/async_client.jsx'); -var UserStore = require('../stores/user_store.jsx'); - -export default class NewChannelModal extends React.Component { - constructor() { - super(); - - this.handleSubmit = this.handleSubmit.bind(this); - this.displayNameKeyUp = this.displayNameKeyUp.bind(this); - this.handleClose = this.handleClose.bind(this); - - this.state = {channelType: ''}; - } - handleSubmit(e) { - e.preventDefault(); - - var channel = {}; - var state = {serverError: ''}; - - channel.display_name = React.findDOMNode(this.refs.display_name).value.trim(); - if (!channel.display_name) { - state.displayNameError = 'This field is required'; - state.inValid = true; - } else if (channel.display_name.length > 22) { - state.displayNameError = 'This field must be less than 22 characters'; - state.inValid = true; - } else { - state.displayNameError = ''; - } - - channel.name = React.findDOMNode(this.refs.channel_name).value.trim(); - if (!channel.name) { - state.nameError = 'This field is required'; - state.inValid = true; - } else if (channel.name.length > 22) { - state.nameError = 'This field must be less than 22 characters'; - state.inValid = true; - } else { - var cleanedName = utils.cleanUpUrlable(channel.name); - if (cleanedName !== channel.name) { - state.nameError = "Must be lowercase alphanumeric characters, allowing '-' but not starting or ending with '-'"; - state.inValid = true; - } else { - state.nameError = ''; - } - } - - this.setState(state); - - if (state.inValid) { - return; - } - - var cu = UserStore.getCurrentUser(); - channel.team_id = cu.team_id; - - channel.description = React.findDOMNode(this.refs.channel_desc).value.trim(); - channel.type = this.state.channelType; - - client.createChannel(channel, - function success(data) { - $(React.findDOMNode(this.refs.modal)).modal('hide'); - - asyncClient.getChannel(data.id); - utils.switchChannel(data); - - React.findDOMNode(this.refs.display_name).value = ''; - React.findDOMNode(this.refs.channel_name).value = ''; - React.findDOMNode(this.refs.channel_desc).value = ''; - }.bind(this), - function error(err) { - state.serverError = err.message; - state.inValid = true; - this.setState(state); - }.bind(this) - ); - } - displayNameKeyUp() { - var displayName = React.findDOMNode(this.refs.display_name).value.trim(); - var channelName = utils.cleanUpUrlable(displayName); - React.findDOMNode(this.refs.channel_name).value = channelName; - } - componentDidMount() { - var self = this; - $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', function onModalShow(e) { - var button = e.relatedTarget; - self.setState({channelType: $(button).attr('data-channeltype')}); - }); - $(React.findDOMNode(this.refs.modal)).on('hidden.bs.modal', this.handleClose); - } - componentWillUnmount() { - $(React.findDOMNode(this.refs.modal)).off('hidden.bs.modal', this.handleClose); - } - handleClose() { - $(React.findDOMNode(this)).find('.form-control').each(function clearForms() { - this.value = ''; - }); - - this.setState({channelType: '', displayNameError: '', nameError: '', serverError: '', inValid: false}); - } - render() { - var displayNameError = null; - var nameError = null; - var serverError = null; - var displayNameClass = 'form-group'; - var nameClass = 'form-group'; - - if (this.state.displayNameError) { - displayNameError = ; - displayNameClass += ' has-error'; - } - if (this.state.nameError) { - nameError = ; - nameClass += ' has-error'; - } - if (this.state.serverError) { - serverError =
; - } - - var channelTerm = 'Channel'; - if (this.state.channelType === 'P') { - channelTerm = 'Group'; - } - - return ( -