' +
- '' +
- parsed.value +
- '' +
- '' +
- 'diff --git a/.sass-lint.yml b/.sass-lint.yml new file mode 100644 index 0000000000..71a68af113 --- /dev/null +++ b/.sass-lint.yml @@ -0,0 +1,178 @@ +# sass-lint config generated by make-sass-lint-config v0.1.1 +# +# The following scss-lint Linters are not yet supported by sass-lint: +# DisableLinterReason, ElsePlacement, PropertyCount, SelectorDepth +# SpaceAroundOperator, TrailingWhitespace, UnnecessaryParentReference, Compass::* +# +# The following settings/values are unsupported by sass-lint: +# Linter Indentation, option "allow_non_nested_indentation" +# Linter Indentation, option "character" +# Linter NestingDepth, option "ignore_parent_selectors" +# Linter PropertySortOrder, option "min_properties" +# Linter PropertySortOrder, option "separate_groups" +# Linter SpaceBeforeBrace, option "allow_single_line_padding" +# Linter VendorPrefix, option "identifier_list" + +files: + include: '**/*.scss' +options: + formatter: stylish + merge-default-rules: false +rules: + bem-depth: + - 1 + - max-depth: 3 + border-zero: + - 1 + - convention: none + brace-style: + - 2 + - allow-single-line: true + class-name-format: + - 1 + - convention: hyphenatedbem + clean-import-paths: + - 1 + - filename-extension: false + leading-underscore: false + empty-line-between-blocks: + - 2 + - ignore-single-line-rulesets: true + extends-before-declarations: 1 + extends-before-mixins: 1 + final-newline: + - 1 + - include: true + force-attribute-nesting: 1 + force-element-nesting: 1 + force-pseudo-nesting: 1 + function-name-format: + - 1 + - allow-leading-underscore: true + convention: hyphenatedlowercase + hex-length: + - 1 + - style: short + hex-notation: + - 1 + - style: lowercase + id-name-format: + - 1 + - convention: hyphenatedbem + indentation: + - 2 + - size: 4 + leading-zero: + - 1 + - include: false + mixin-name-format: + - 0 + - allow-leading-underscore: true + convention: hyphenatedlowercase + mixins-before-declarations: 1 + nesting-depth: + - 1 + - max-depth: 4 + no-color-keyword: 1 + no-color-literals: 1 + no-css-comments: 1 + no-debug: 1 + no-duplicate-properties: 1 + no-empty-rulesets: 1 + no-extends: 0 + no-ids: 1 + no-important: 1 + no-invalid-hex: 1 + no-mergeable-selectors: 1 + no-misspelled-properties: + - 1 + - extra-properties: ['overflow-scrolling', 'font-smoothing'] + no-qualifying-elements: + - 1 + - allow-element-with-attribute: false + allow-element-with-class: false + allow-element-with-id: false + no-trailing-zero: 1 + no-transition-all: 0 + no-url-protocols: 1 + no-vendor-prefixes: + - 1 + - additional-identifiers: [] + excluded-identifiers: [] + placeholder-in-extend: 1 + placeholder-name-format: + - 1 + - convention: hyphenatedlowercase + property-sort-order: + - 1 + - ignore-custom-properties: false + property-units: + - 1 + - global: + - ch + - em + - ex + - rem + - cm + - in + - mm + - pc + - pt + - px + - q + - vh + - vw + - vmin + - vmax + - deg + - grad + - rad + - turn + - ms + - s + - Hz + - kHz + - dpi + - dpcm + - dppx + - '%' + per-property: {} + quotes: + - 1 + - style: single + shorthand-values: + - 1 + - allowed-shorthands: + - 1 + - 2 + - 3 + single-line-per-selector: 2 + space-after-bang: + - 2 + - include: false + space-after-colon: + - 2 + - include: true + space-after-comma: + - 1 + - include: true + space-before-bang: + - 2 + - include: true + space-before-brace: + - 2 + - include: true + space-before-colon: 1 + space-between-parens: + - 2 + - include: false + trailing-semicolon: 2 + url-quotes: 1 + variable-for-property: + - 0 + - properties: [] + variable-name-format: + - 1 + - allow-leading-underscore: true + convention: hyphenatedlowercase + zero-unit: 1 diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 808d8dab13..f94cafc1cb 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -5,6 +5,10 @@ "./..." ], "Deps": [ + { + "ImportPath": "github.com/NYTimes/gziphandler", + "Rev": "a88790d49798560db24af70fb6a10a66e2549a72" + }, { "ImportPath": "github.com/alecthomas/log4go", "Rev": "8e9057c3b25c409a34c0b9737cdc82cbcafeabce" diff --git a/Godeps/_workspace/src/github.com/NYTimes/gziphandler/LICENSE.md b/Godeps/_workspace/src/github.com/NYTimes/gziphandler/LICENSE.md new file mode 100644 index 0000000000..b7e2ecb63f --- /dev/null +++ b/Godeps/_workspace/src/github.com/NYTimes/gziphandler/LICENSE.md @@ -0,0 +1,13 @@ +Copyright (c) 2015 The New York Times Company + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this library except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/Godeps/_workspace/src/github.com/NYTimes/gziphandler/README.md b/Godeps/_workspace/src/github.com/NYTimes/gziphandler/README.md new file mode 100644 index 0000000000..b1d55e26ec --- /dev/null +++ b/Godeps/_workspace/src/github.com/NYTimes/gziphandler/README.md @@ -0,0 +1,52 @@ +Gzip Handler +============ + +This is a tiny Go package which wraps HTTP handlers to transparently gzip the +response body, for clients which support it. Although it's usually simpler to +leave that to a reverse proxy (like nginx or Varnish), this package is useful +when that's undesirable. + + +## Usage + +Call `GzipHandler` with any handler (an object which implements the +`http.Handler` interface), and it'll return a new handler which gzips the +response. For example: + +```go +package main + +import ( + "io" + "net/http" + "github.com/NYTimes/gziphandler" +) + +func main() { + withoutGz := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + io.WriteString(w, "Hello, World") + }) + + withGz := gziphandler.GzipHandler(withoutGz) + + http.Handle("/", withGz) + http.ListenAndServe("0.0.0.0:8000", nil) +} +``` + + +## Documentation + +The docs can be found at [godoc.org] [docs], as usual. + + +## License + +[Apache 2.0] [license]. + + + + +[docs]: https://godoc.org/github.com/nytimes/gziphandler +[license]: https://github.com/nytimes/gziphandler/blob/master/LICENSE.md diff --git a/Godeps/_workspace/src/github.com/NYTimes/gziphandler/gzip.go b/Godeps/_workspace/src/github.com/NYTimes/gziphandler/gzip.go new file mode 100644 index 0000000000..d0c85c6d3c --- /dev/null +++ b/Godeps/_workspace/src/github.com/NYTimes/gziphandler/gzip.go @@ -0,0 +1,140 @@ +package gziphandler + +import ( + "compress/gzip" + "fmt" + "net/http" + "strconv" + "strings" + "sync" +) + +const ( + vary = "Vary" + acceptEncoding = "Accept-Encoding" + contentEncoding = "Content-Encoding" +) + +type codings map[string]float64 + +// The default qvalue to assign to an encoding if no explicit qvalue is set. +// This is actually kind of ambiguous in RFC 2616, so hopefully it's correct. +// The examples seem to indicate that it is. +const DEFAULT_QVALUE = 1.0 + +var gzipWriterPool = sync.Pool{ + New: func() interface{} { return gzip.NewWriter(nil) }, +} + +// GzipResponseWriter provides an http.ResponseWriter interface, which gzips +// bytes before writing them to the underlying response. This doesn't set the +// Content-Encoding header, nor close the writers, so don't forget to do that. +type GzipResponseWriter struct { + gw *gzip.Writer + http.ResponseWriter +} + +// Write appends data to the gzip writer. +func (w GzipResponseWriter) Write(b []byte) (int, error) { + return w.gw.Write(b) +} + +// Flush flushes the underlying *gzip.Writer and then the underlying +// http.ResponseWriter if it is an http.Flusher. This makes GzipResponseWriter +// an http.Flusher. +func (w GzipResponseWriter) Flush() { + w.gw.Flush() + if fw, ok := w.ResponseWriter.(http.Flusher); ok { + fw.Flush() + } +} + +// GzipHandler wraps an HTTP handler, to transparently gzip the response body if +// the client supports it (via the Accept-Encoding header). +func GzipHandler(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Add(vary, acceptEncoding) + + if acceptsGzip(r) { + // Bytes written during ServeHTTP are redirected to this gzip writer + // before being written to the underlying response. + gzw := gzipWriterPool.Get().(*gzip.Writer) + defer gzipWriterPool.Put(gzw) + gzw.Reset(w) + defer gzw.Close() + + w.Header().Set(contentEncoding, "gzip") + h.ServeHTTP(GzipResponseWriter{gzw, w}, r) + } else { + h.ServeHTTP(w, r) + } + }) +} + +// acceptsGzip returns true if the given HTTP request indicates that it will +// accept a gzippped response. +func acceptsGzip(r *http.Request) bool { + acceptedEncodings, _ := parseEncodings(r.Header.Get(acceptEncoding)) + return acceptedEncodings["gzip"] > 0.0 +} + +// parseEncodings attempts to parse a list of codings, per RFC 2616, as might +// appear in an Accept-Encoding header. It returns a map of content-codings to +// quality values, and an error containing the errors encounted. It's probably +// safe to ignore those, because silently ignoring errors is how the internet +// works. +// +// See: http://tools.ietf.org/html/rfc2616#section-14.3 +func parseEncodings(s string) (codings, error) { + c := make(codings) + e := make([]string, 0) + + for _, ss := range strings.Split(s, ",") { + coding, qvalue, err := parseCoding(ss) + + if err != nil { + e = append(e, err.Error()) + + } else { + c[coding] = qvalue + } + } + + // TODO (adammck): Use a proper multi-error struct, so the individual errors + // can be extracted if anyone cares. + if len(e) > 0 { + return c, fmt.Errorf("errors while parsing encodings: %s", strings.Join(e, ", ")) + } + + return c, nil +} + +// parseCoding parses a single conding (content-coding with an optional qvalue), +// as might appear in an Accept-Encoding header. It attempts to forgive minor +// formatting errors. +func parseCoding(s string) (coding string, qvalue float64, err error) { + for n, part := range strings.Split(s, ";") { + part = strings.TrimSpace(part) + qvalue = DEFAULT_QVALUE + + if n == 0 { + coding = strings.ToLower(part) + + } else if strings.HasPrefix(part, "q=") { + qvalue, err = strconv.ParseFloat(strings.TrimPrefix(part, "q="), 64) + + if qvalue < 0.0 { + qvalue = 0.0 + + } else if qvalue > 1.0 { + qvalue = 1.0 + } + } + } + + if coding == "" { + err = fmt.Errorf("empty content-coding") + } + + return +} diff --git a/Makefile b/Makefile index 4ae2c3b1f8..ce12dbaf61 100644 --- a/Makefile +++ b/Makefile @@ -136,6 +136,7 @@ package: tar -C dist -czf $(DIST_PATH).tar.gz mattermost build-client: + mkdir -p webapp/dist/files cd webapp && make build go-test: @@ -196,15 +197,10 @@ clean: stop-docker rm -Rf $(DIST_ROOT) go clean $(GOFLAGS) -i ./... - rm -rf web/react/node_modules - rm -f web/static/js/bundle*.js - rm -f web/static/js/bundle*.js.map - rm -f web/static/js/libs*.js - rm -f web/static/css/styles.css + cd webapp && make clean rm -rf api/data rm -rf logs - rm -rf web/sass-files/.sass-cache rm -rf Godeps/_workspace/pkg/ @@ -220,14 +216,17 @@ nuke: | clean clean-docker touch $@ -run: start-docker run-server run-client +run: | start-docker run-client run-server run-server: .prepare-go @echo Starting go web server $(GO) run $(GOFLAGS) mattermost.go -config=config.json & -run-client: build-client - @echo Starting react processo +run-client: + @echo Starting client + + mkdir -p webapp/dist/files + cd webapp && make run @if [ "$(BUILD_ENTERPRISE)" = "true" ] && [ -d "$(ENTERPRISE_DIR)" ]; then \ cp ./config/config.json ./config/config.json.bak; \ diff --git a/api/command.go b/api/command.go index 99fd05d7a3..29cee070ec 100644 --- a/api/command.go +++ b/api/command.go @@ -44,7 +44,7 @@ func InitCommand(r *mux.Router) { sr := r.PathPrefix("/commands").Subrouter() sr.Handle("/execute", ApiUserRequired(executeCommand)).Methods("POST") - sr.Handle("/list", ApiUserRequired(listCommands)).Methods("GET") + sr.Handle("/list", ApiUserRequired(listCommands)).Methods("POST") sr.Handle("/create", ApiUserRequired(createCommand)).Methods("POST") sr.Handle("/list_team_commands", ApiUserRequired(listTeamCommands)).Methods("GET") @@ -76,7 +76,9 @@ func listCommands(c *Context, w http.ResponseWriter, r *http.Request) { } else { teamCmds := result.Data.([]*model.Command) for _, cmd := range teamCmds { - if cmd.AutoComplete && !seen[cmd.Id] { + if cmd.ExternalManagement { + commands = append(commands, autocompleteCommands(c, cmd, r)...) + } else if cmd.AutoComplete && !seen[cmd.Id] { cmd.Sanitize() seen[cmd.Trigger] = true commands = append(commands, cmd) @@ -88,6 +90,92 @@ func listCommands(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(model.CommandListToJson(commands))) } +func autocompleteCommands(c *Context, cmd *model.Command, r *http.Request) []*model.Command { + props := model.MapFromJson(r.Body) + command := strings.TrimSpace(props["command"]) + channelId := strings.TrimSpace(props["channelId"]) + parts := strings.Split(command, " ") + trigger := parts[0][1:] + message := strings.Join(parts[1:], " ") + + chanChan := Srv.Store.Channel().Get(channelId) + teamChan := Srv.Store.Team().Get(c.Session.TeamId) + userChan := Srv.Store.User().Get(c.Session.UserId) + + var team *model.Team + if tr := <-teamChan; tr.Err != nil { + c.Err = tr.Err + return make([]*model.Command, 0, 32) + } else { + team = tr.Data.(*model.Team) + } + + var user *model.User + if ur := <-userChan; ur.Err != nil { + c.Err = ur.Err + return make([]*model.Command, 0, 32) + } else { + user = ur.Data.(*model.User) + } + + var channel *model.Channel + if cr := <-chanChan; cr.Err != nil { + c.Err = cr.Err + return make([]*model.Command, 0, 32) + } else { + channel = cr.Data.(*model.Channel) + } + + l4g.Debug(fmt.Sprintf(utils.T("api.command.execute_command.debug"), trigger, c.Session.UserId)) + p := url.Values{} + p.Set("token", cmd.Token) + + p.Set("team_id", cmd.TeamId) + p.Set("team_domain", team.Name) + + p.Set("channel_id", channelId) + p.Set("channel_name", channel.Name) + + p.Set("user_id", c.Session.UserId) + p.Set("user_name", user.Username) + + p.Set("command", "/"+trigger) + p.Set("text", message) + p.Set("response_url", "not supported yet") + p.Set("suggest", "true") + + method := "POST" + if cmd.Method == model.COMMAND_METHOD_GET { + method = "GET" + } + + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: *utils.Cfg.ServiceSettings.EnableInsecureOutgoingConnections}, + } + client := &http.Client{Transport: tr} + + req, _ := http.NewRequest(method, cmd.URL, strings.NewReader(p.Encode())) + req.Header.Set("Accept", "application/json") + if cmd.Method == model.COMMAND_METHOD_POST { + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + } + + if resp, err := client.Do(req); err != nil { + c.Err = model.NewLocAppError("command", "api.command.execute_command.failed.app_error", map[string]interface{}{"Trigger": trigger}, err.Error()) + } else { + if resp.StatusCode == http.StatusOK { + response := model.CommandListFromJson(resp.Body) + + return response + + } else { + body, _ := ioutil.ReadAll(resp.Body) + c.Err = model.NewLocAppError("command", "api.command.execute_command.failed_resp.app_error", map[string]interface{}{"Trigger": trigger, "Status": resp.Status}, string(body)) + } + } + return make([]*model.Command, 0, 32) +} + func executeCommand(c *Context, w http.ResponseWriter, r *http.Request) { props := model.MapFromJson(r.Body) command := strings.TrimSpace(props["command"]) @@ -159,7 +247,7 @@ func executeCommand(c *Context, w http.ResponseWriter, r *http.Request) { teamCmds := result.Data.([]*model.Command) for _, cmd := range teamCmds { - if trigger == cmd.Trigger { + if trigger == cmd.Trigger || cmd.ExternalManagement { l4g.Debug(fmt.Sprintf(utils.T("api.command.execute_command.debug"), trigger, c.Session.UserId)) p := url.Values{} diff --git a/api/command_test.go b/api/command_test.go index 22e2bd6669..8ca8b65b12 100644 --- a/api/command_test.go +++ b/api/command_test.go @@ -24,7 +24,10 @@ func TestListCommands(t *testing.T) { Client.LoginByEmail(team.Name, user1.Email, "pwd") - if results, err := Client.ListCommands(); err != nil { + channel1 := &model.Channel{DisplayName: "AA", Name: "aa" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + if results, err := Client.ListCommands(channel1.Id, "/test"); err != nil { t.Fatal(err) } else { commands := results.Data.([]*model.Command) diff --git a/config/config.json b/config/config.json index 8b58e64e46..1d54edccfb 100644 --- a/config/config.json +++ b/config/config.json @@ -21,7 +21,8 @@ "SessionLengthSSOInDays": 30, "SessionCacheInMinutes": 10, "WebsocketSecurePort": 443, - "WebsocketPort": 80 + "WebsocketPort": 80, + "WebserverMode": "regular" }, "TeamSettings": { "SiteName": "Mattermost", @@ -144,4 +145,4 @@ "Directory": "./data/", "EnableDaily": false } -} \ No newline at end of file +} diff --git a/docker/2.1/docker-entry.sh b/docker/2.1/docker-entry.sh index 6bd2a1263b..14b9ccd9da 100755 --- a/docker/2.1/docker-entry.sh +++ b/docker/2.1/docker-entry.sh @@ -107,5 +107,5 @@ sleep 5 # ------------------------ echo starting platform -cd /mattermost/bin +cd /mattermost/mattermost/bin ./platform -config=/config_docker.json diff --git a/i18n/en.json b/i18n/en.json index a377734609..f14e0bbe0e 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -3131,14 +3131,14 @@ "id": "store.sql_user.get.app_error", "translation": "We encountered an error finding the account" }, - { - "id": "store.sql_user.get_by_auth.other.app_error", - "translation": "We encountered an error trying to find the account by authentication type." - }, { "id": "store.sql_user.get_by_auth.missing_account.app_error", "translation": "We couldn't find an existing account matching your authentication type for this team. This team may require an invite from the team owner to join." }, + { + "id": "store.sql_user.get_by_auth.other.app_error", + "translation": "We encountered an error trying to find the account by authentication type." + }, { "id": "store.sql_user.get_by_username.app_error", "translation": "We couldn't find an existing account matching your username for this team. This team may require an invite from the team owner to join." diff --git a/i18n/es.json b/i18n/es.json index 4622a1b124..894bf0c18e 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -3079,6 +3079,10 @@ "id": "store.sql_user.get_by_auth.missing_account.app_error", "translation": "No pudimos encontrar una cuenta existente que coincida con tu tipo de autenticación para este equipo. Es posible que necesites una invitación por parte del dueño del equipo para unirte." }, + { + "id": "store.sql_user.get_by_auth.other.app_error", + "translation": "Encontramos un error tratando de identificar la cuenta con este tipo de autenticación." + }, { "id": "store.sql_user.get_by_username.app_error", "translation": "No pudimos encontrar una cuenta existente que coincida con tu nombre de usuario para este equipo. Es posible que necesites una invitación por parte del dueño del equipo para unirte." diff --git a/model/client.go b/model/client.go index f5c8ad6417..68cf11414e 100644 --- a/model/client.go +++ b/model/client.go @@ -363,8 +363,11 @@ func (c *Client) Command(channelId string, command string, suggest bool) (*Resul } } -func (c *Client) ListCommands() (*Result, *AppError) { - if r, err := c.DoApiGet("/commands/list", "", ""); err != nil { +func (c *Client) ListCommands(channelId string, command string) (*Result, *AppError) { + m := make(map[string]string) + m["command"] = command + m["channelId"] = channelId + if r, err := c.DoApiPost("/commands/list", MapToJson(m)); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), diff --git a/model/command.go b/model/command.go index 56d88f13c5..8e0b31583f 100644 --- a/model/command.go +++ b/model/command.go @@ -14,22 +14,23 @@ const ( ) type Command struct { - Id string `json:"id"` - Token string `json:"token"` - CreateAt int64 `json:"create_at"` - UpdateAt int64 `json:"update_at"` - DeleteAt int64 `json:"delete_at"` - CreatorId string `json:"creator_id"` - TeamId string `json:"team_id"` - Trigger string `json:"trigger"` - Method string `json:"method"` - Username string `json:"username"` - IconURL string `json:"icon_url"` - AutoComplete bool `json:"auto_complete"` - AutoCompleteDesc string `json:"auto_complete_desc"` - AutoCompleteHint string `json:"auto_complete_hint"` - DisplayName string `json:"display_name"` - URL string `json:"url"` + Id string `json:"id"` + Token string `json:"token"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + DeleteAt int64 `json:"delete_at"` + CreatorId string `json:"creator_id"` + TeamId string `json:"team_id"` + ExternalManagement bool `json:"external_management"` + Trigger string `json:"trigger"` + Method string `json:"method"` + Username string `json:"username"` + IconURL string `json:"icon_url"` + AutoComplete bool `json:"auto_complete"` + AutoCompleteDesc string `json:"auto_complete_desc"` + AutoCompleteHint string `json:"auto_complete_hint"` + DisplayName string `json:"display_name"` + URL string `json:"url"` } func (o *Command) ToJson() string { diff --git a/model/config.go b/model/config.go index 8f5865f283..e89ca78fdf 100644 --- a/model/config.go +++ b/model/config.go @@ -21,6 +21,10 @@ const ( SERVICE_GITLAB = "gitlab" SERVICE_GOOGLE = "google" + + WEBSERVER_MODE_REGULAR = "regular" + WEBSERVER_MODE_GZIP = "gzip" + WEBSERVER_MODE_DISABLED = "disabled" ) type ServiceSettings struct { @@ -46,6 +50,7 @@ type ServiceSettings struct { SessionCacheInMinutes *int WebsocketSecurePort *int WebsocketPort *int + WebserverMode *string } type SSOSettings struct { @@ -391,6 +396,11 @@ func (o *Config) SetDefaults() { *o.ServiceSettings.AllowCorsFrom = "" } + if o.ServiceSettings.WebserverMode == nil { + o.ServiceSettings.WebserverMode = new(string) + *o.ServiceSettings.WebserverMode = "regular" + } + if o.ComplianceSettings.Enable == nil { o.ComplianceSettings.Enable = new(bool) *o.ComplianceSettings.Enable = false diff --git a/store/sql_command_store.go b/store/sql_command_store.go index 074a6e5884..a35737bd71 100644 --- a/store/sql_command_store.go +++ b/store/sql_command_store.go @@ -34,6 +34,7 @@ func NewSqlCommandStore(sqlStore *SqlStore) CommandStore { } func (s SqlCommandStore) UpgradeSchemaIfNeeded() { + s.CreateColumnIfNotExists("Commands", "ExternalManagement", "tinyint(1)", "boolean", "0") } func (s SqlCommandStore) CreateIndexesIfNotExists() { diff --git a/web/web.go b/web/web.go index 86b642f3b8..ff5040a4b2 100644 --- a/web/web.go +++ b/web/web.go @@ -7,6 +7,8 @@ import ( "net/http" "strings" + "github.com/NYTimes/gziphandler" + l4g "github.com/alecthomas/log4go" "github.com/mattermost/platform/api" "github.com/mattermost/platform/model" @@ -23,11 +25,17 @@ func InitWeb() { mainrouter := api.Srv.Router - staticDir := utils.FindDir(CLIENT_DIR) - l4g.Debug("Using client directory at %v", staticDir) - mainrouter.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir)))) + if *utils.Cfg.ServiceSettings.WebserverMode != "disabled" { + staticDir := utils.FindDir(CLIENT_DIR) + l4g.Debug("Using client directory at %v", staticDir) + if *utils.Cfg.ServiceSettings.WebserverMode == "gzip" { + mainrouter.PathPrefix("/static/").Handler(gziphandler.GzipHandler(http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir))))) + } else { + mainrouter.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir)))) + } - mainrouter.Handle("/{anything:.*}", api.AppHandlerIndependent(root)).Methods("GET") + mainrouter.Handle("/{anything:.*}", api.AppHandlerIndependent(root)).Methods("GET") + } } var browsersNotSupported string = "MSIE/8;MSIE/9;MSIE/10;Internet Explorer/8;Internet Explorer/9;Internet Explorer/10;Safari/7;Safari/8" diff --git a/webapp/Makefile b/webapp/Makefile index 99f896e53c..88ee625a41 100644 --- a/webapp/Makefile +++ b/webapp/Makefile @@ -1,4 +1,4 @@ -.PHONY: build test +.PHONY: build test run clean test: @echo Checking for style guide compliance @@ -12,7 +12,20 @@ test: touch $@ -build: .npminstall - @echo Building mattermost web client +build: | .npminstall test + @echo Building mattermost Webapp npm run build + +run: .npminstall + @echo Running mattermost Webapp for development + + npm run run + + +clean: + @echo Cleaning Webapp + + rm -rf dist + rm -rf node_modules + rm .npminstall diff --git a/webapp/components/center_panel.jsx b/webapp/components/center_panel.jsx index 17e5e43d9b..6c156f2a83 100644 --- a/webapp/components/center_panel.jsx +++ b/webapp/components/center_panel.jsx @@ -120,7 +120,10 @@ export default class CenterPanel extends React.Component { id='app-content' className='app__content' > -
+ ' +
- '' +
- parsed.value +
- '' +
- '' +
- '' +
- '' +
- (escaped ? code : TextFormatting.sanitizeHtml(code)) + '\n' +
- '' +
- ''
- );
+ // treat html as xml to prevent injection attacks
+ if (usedLanguage === 'html') {
+ usedLanguage = 'xml';
+ }
+
+ return syntaxHightlighting.formatCode(usedLanguage, code);
}
codespan(text) {
diff --git a/webapp/utils/syntax_hightlighting.jsx b/webapp/utils/syntax_hightlighting.jsx
new file mode 100644
index 0000000000..981ce6b359
--- /dev/null
+++ b/webapp/utils/syntax_hightlighting.jsx
@@ -0,0 +1,197 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import * as Utils from './utils.jsx';
+import * as TextFormatting from './text_formatting.jsx';
+import Constants from './constants.jsx';
+
+import hlJS from 'highlight.js/lib/highlight.js';
+
+import hljsActionscript from 'highlight.js/lib/languages/actionscript.js';
+import hljsApplescript from 'highlight.js/lib/languages/applescript.js';
+import hljsBash from 'highlight.js/lib/languages/bash.js';
+import hljsClojure from 'highlight.js/lib/languages/clojure.js';
+import hljsCoffeescript from 'highlight.js/lib/languages/coffeescript.js';
+import hljsCpp from 'highlight.js/lib/languages/cpp.js';
+import hljsCs from 'highlight.js/lib/languages/cs.js';
+import hljsCss from 'highlight.js/lib/languages/css.js';
+import hljsD from 'highlight.js/lib/languages/d.js';
+import hljsDart from 'highlight.js/lib/languages/dart.js';
+import hljsDelphi from 'highlight.js/lib/languages/delphi.js';
+import hljsDiff from 'highlight.js/lib/languages/diff.js';
+import hljsDjango from 'highlight.js/lib/languages/django.js';
+import hljsDockerfile from 'highlight.js/lib/languages/dockerfile.js';
+import hljsErlang from 'highlight.js/lib/languages/erlang.js';
+import hljsFortran from 'highlight.js/lib/languages/fortran.js';
+import hljsFsharp from 'highlight.js/lib/languages/fsharp.js';
+import hljsGcode from 'highlight.js/lib/languages/gcode.js';
+import hljsGo from 'highlight.js/lib/languages/go.js';
+import hljsGroovy from 'highlight.js/lib/languages/groovy.js';
+import hljsHandlebars from 'highlight.js/lib/languages/handlebars.js';
+import hljsHaskell from 'highlight.js/lib/languages/haskell.js';
+import hljsHaxe from 'highlight.js/lib/languages/haxe.js';
+import hljsJava from 'highlight.js/lib/languages/java.js';
+import hljsJavascript from 'highlight.js/lib/languages/javascript.js';
+import hljsJson from 'highlight.js/lib/languages/json.js';
+import hljsJulia from 'highlight.js/lib/languages/julia.js';
+import hljsKotlin from 'highlight.js/lib/languages/kotlin.js';
+import hljsLess from 'highlight.js/lib/languages/less.js';
+import hljsLisp from 'highlight.js/lib/languages/lisp.js';
+import hljsLua from 'highlight.js/lib/languages/lua.js';
+import hljsMakefile from 'highlight.js/lib/languages/makefile.js';
+import hljsMarkdown from 'highlight.js/lib/languages/markdown.js';
+import hljsMatlab from 'highlight.js/lib/languages/matlab.js';
+import hljsObjectivec from 'highlight.js/lib/languages/objectivec.js';
+import hljsOcaml from 'highlight.js/lib/languages/ocaml.js';
+import hljsPerl from 'highlight.js/lib/languages/perl.js';
+import hljsPhp from 'highlight.js/lib/languages/php.js';
+import hljsPowershell from 'highlight.js/lib/languages/powershell.js';
+import hljsPuppet from 'highlight.js/lib/languages/puppet.js';
+import hljsPython from 'highlight.js/lib/languages/python.js';
+import hljsR from 'highlight.js/lib/languages/r.js';
+import hljsRuby from 'highlight.js/lib/languages/ruby.js';
+import hljsRust from 'highlight.js/lib/languages/rust.js';
+import hljsScala from 'highlight.js/lib/languages/scala.js';
+import hljsScheme from 'highlight.js/lib/languages/scheme.js';
+import hljsScss from 'highlight.js/lib/languages/scss.js';
+import hljsSmalltalk from 'highlight.js/lib/languages/smalltalk.js';
+import hljsSql from 'highlight.js/lib/languages/sql.js';
+import hljsSwift from 'highlight.js/lib/languages/swift.js';
+import hljsTex from 'highlight.js/lib/languages/tex.js';
+import hljsVbnet from 'highlight.js/lib/languages/vbnet.js';
+import hljsVbscript from 'highlight.js/lib/languages/vbscript.js';
+import hljsVerilog from 'highlight.js/lib/languages/verilog.js';
+import hljsXml from 'highlight.js/lib/languages/xml.js';
+import hljsYaml from 'highlight.js/lib/languages/yaml.js';
+
+hlJS.registerLanguage('actionscript', hljsActionscript);
+hlJS.registerLanguage('applescript', hljsApplescript);
+hlJS.registerLanguage('bash', hljsBash);
+hlJS.registerLanguage('clojure', hljsClojure);
+hlJS.registerLanguage('coffeescript', hljsCoffeescript);
+hlJS.registerLanguage('cpp', hljsCpp);
+hlJS.registerLanguage('cs', hljsCs);
+hlJS.registerLanguage('css', hljsCss);
+hlJS.registerLanguage('d', hljsD);
+hlJS.registerLanguage('dart', hljsDart);
+hlJS.registerLanguage('delphi', hljsDelphi);
+hlJS.registerLanguage('diff', hljsDiff);
+hlJS.registerLanguage('django', hljsDjango);
+hlJS.registerLanguage('dockerfile', hljsDockerfile);
+hlJS.registerLanguage('erlang', hljsErlang);
+hlJS.registerLanguage('fortran', hljsFortran);
+hlJS.registerLanguage('fsharp', hljsFsharp);
+hlJS.registerLanguage('gcode', hljsGcode);
+hlJS.registerLanguage('go', hljsGo);
+hlJS.registerLanguage('groovy', hljsGroovy);
+hlJS.registerLanguage('handlebars', hljsHandlebars);
+hlJS.registerLanguage('haskell', hljsHaskell);
+hlJS.registerLanguage('haxe', hljsHaxe);
+hlJS.registerLanguage('java', hljsJava);
+hlJS.registerLanguage('javascript', hljsJavascript);
+hlJS.registerLanguage('json', hljsJson);
+hlJS.registerLanguage('julia', hljsJulia);
+hlJS.registerLanguage('kotlin', hljsKotlin);
+hlJS.registerLanguage('less', hljsLess);
+hlJS.registerLanguage('lisp', hljsLisp);
+hlJS.registerLanguage('lua', hljsLua);
+hlJS.registerLanguage('makefile', hljsMakefile);
+hlJS.registerLanguage('markdown', hljsMarkdown);
+hlJS.registerLanguage('matlab', hljsMatlab);
+hlJS.registerLanguage('objectivec', hljsObjectivec);
+hlJS.registerLanguage('ocaml', hljsOcaml);
+hlJS.registerLanguage('perl', hljsPerl);
+hlJS.registerLanguage('php', hljsPhp);
+hlJS.registerLanguage('powershell', hljsPowershell);
+hlJS.registerLanguage('puppet', hljsPuppet);
+hlJS.registerLanguage('python', hljsPython);
+hlJS.registerLanguage('r', hljsR);
+hlJS.registerLanguage('ruby', hljsRuby);
+hlJS.registerLanguage('rust', hljsRust);
+hlJS.registerLanguage('scala', hljsScala);
+hlJS.registerLanguage('scheme', hljsScheme);
+hlJS.registerLanguage('scss', hljsScss);
+hlJS.registerLanguage('smalltalk', hljsSmalltalk);
+hlJS.registerLanguage('sql', hljsSql);
+hlJS.registerLanguage('swift', hljsSwift);
+hlJS.registerLanguage('tex', hljsTex);
+hlJS.registerLanguage('vbnet', hljsVbnet);
+hlJS.registerLanguage('vbscript', hljsVbscript);
+hlJS.registerLanguage('verilog', hljsVerilog);
+hlJS.registerLanguage('xml', hljsXml);
+hlJS.registerLanguage('yaml', hljsYaml);
+
+const HighlightedLanguages = Constants.HighlightedLanguages;
+
+export function formatCode(lang, data, filename) {
+ var language = lang || '';
+ var parsed;
+ var header = '';
+
+ language = language.toLowerCase();
+
+ if (HighlightedLanguages[language]) {
+ var name = HighlightedLanguages[language].name;
+
+ if (filename) {
+ const fname = decodeURIComponent(Utils.getFileName(filename));
+ name = fname + ' - ' + name;
+ }
+
+ header = '' + name + '';
+
+ try {
+ parsed = hlJS.highlight(language, data).value;
+ } catch (e) {
+ parsed = TextFormatting.sanitizeHtml(data);
+ }
+ } else {
+ parsed = TextFormatting.sanitizeHtml(data);
+ }
+
+ const lines = data.match(/\r\n|\r|\n|$/g).length;
+ var strlines = '';
+ for (var i = 1; i <= lines; i++) {
+ if (strlines) {
+ strlines += '\n' + i;
+ } else {
+ strlines += i;
+ }
+ }
+
+ return (
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' + strlines + ' ' +
+ '' +
+ parsed +
+ ' ' +
+ ' ' +
+ '' +
+ '
' +
+ '' +
+ '' +
+ '